gh-EmergeTools-Pow/Sources/Pow/Effects/ShineEffect.swift
Joe Fabisevich 5b95fe95b0
Moving Pow to @emergetools (#36)
Co-authored-by: Robert Böhnke <robb@robb.is>
Co-authored-by: Kasper Lahti <kasper@lahti.email>
2023-11-29 12:08:53 -03:00

156 lines
5.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
public extension AnyChangeEffect {
/// An effect that highlights the view with a shine moving over the view.
///
/// The shine moves from the top leading edge to bottom trailing edge.
static var shine: AnyChangeEffect {
shine(duration: 1)
}
/// An effect that highlights the view with a shine moving over the view.
///
/// The shine moves from the top leading edge to bottom trailing edge.
static func shine(duration: Double) -> AnyChangeEffect {
.animation({ change in
ShineModifier(angle: nil, animatableData: CGFloat(change))
}, animation: .easeInOut(duration: duration), cooldown: duration * 0.5)
}
/// An effect that highlights the view with a shine moving over the view.
///
/// The angle is relative to the current `layoutDirection`, such that 0° represents sweeping towards the trailing edge and 90° represents sweeping towards the bottom edge.
///
/// - Parameters:
/// - angle: The angle of the animation.
/// - duration: The duration of the animation.
static func shine(angle: Angle, duration: Double = 1.0) -> AnyChangeEffect {
.animation({ change in
ShineModifier(angle: angle, animatableData: CGFloat(change))
}, animation: .easeInOut(duration: duration), cooldown: duration * 0.5)
}
}
internal struct ShineModifier: ViewModifier, Animatable {
var angle: Angle?
public var animatableData: CGFloat = 0
public func body(content: Content) -> some View {
let fraction = CGFloat(fmodf(Float(animatableData), 1))
content
.overlay(
GeometryReader { proxy in
let base = sin(Double(fraction))
let frame = CGRect(origin: .zero, size: proxy.size)
let resolvedAngle = angle ?? frame.topLeft.angle(to: frame.bottomRight)
let bounds = frame.boundingBox(at: resolvedAngle)
LinearGradient(
colors: stride(from: 0.0, through: .pi, by: 0.2).map {
.white.opacity(pow(sin($0), 2) * 0.8 * base)
},
startPoint: .leading,
endPoint: .trailing
)
.frame(width: bounds.width * 2, height: bounds.height)
.position(
x: (bounds.minX - bounds.width / 2) + (fraction * bounds.width * 2),
y: bounds.midY
)
.rotationEffect(resolvedAngle)
.blendMode(.sourceAtop)
.opacity(1.0 - pow(fraction, 8.0))
}
.allowsHitTesting(false)
)
.compositingGroup()
.animation(nil, value: fraction)
}
}
#if os(iOS) && DEBUG
struct ShineChangeEffect_Previews: PreviewProvider {
struct Cart: View {
@State
var itemCount: Int = 0
@State private var degrees: Double = 45
var body: some View {
List {
HStack(alignment: .center, spacing: 16) {
AsyncImage(url: URL(string: "https://movingparts.io/frontpage/checkout-smooth-blend@3x.png")) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
}
}
.background(Color(white: 0.9))
.frame(width: 72, height: 72)
.changeEffect(.shine(angle: .degrees(180), duration: 0.5), value: itemCount, isEnabled: itemCount > 0)
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 8) {
Text("Seasonal Blend, Spring Here")
.font(.body.weight(.medium))
.lineSpacing(-10)
Text("500g")
.font(.callout)
.foregroundColor(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text("\(itemCount.formatted())× ").foregroundColor(.secondary) +
Text(9.99.formatted(.currency(code: "EUR")))
Stepper(value: $itemCount, in: 0...10) {
Text("Quantity ") + Text(itemCount.formatted()).foregroundColor(.secondary)
}
.labelsHidden()
.font(.callout)
}
.font(.callout)
}
}
Text(degrees, format: .number.precision(.fractionLength(2)))
Slider(value: $degrees, in: -360.0...360.0)
}
.listStyle(.plain)
.navigationTitle("Cart")
.safeAreaInset(edge: .bottom, spacing: 0) {
VStack(spacing: 32) {
Button {
} label: {
Label("Checkout", systemImage: "cart")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.disabled(itemCount == 0)
.animation(.default, value: itemCount == 0)
.changeEffect(
.shine(angle: .degrees(degrees)).delay(0.5),
value: itemCount,
isEnabled: itemCount > 0
)
.padding()
}
}
}
}
static var previews: some View {
NavigationView {
Cart()
}
}
}
#endif