mirror of
https://github.com/EmergeTools/Pow.git
synced 2026-03-25 08:55:50 +00:00
175 lines
4.8 KiB
Swift
175 lines
4.8 KiB
Swift
import SwiftUI
|
|
|
|
public extension AnyChangeEffect {
|
|
/// An effect that wiggles the view when a change happens.
|
|
static var wiggle: AnyChangeEffect {
|
|
wiggle(rate: .default)
|
|
}
|
|
|
|
/// The rate of the wiggle effect.
|
|
enum WiggleRate {
|
|
case `default`
|
|
case fast
|
|
case phaseLength(CGFloat)
|
|
|
|
fileprivate var phaseLength: CGFloat {
|
|
switch self {
|
|
case .default: return 0.8
|
|
case .fast: return 0.3
|
|
case .phaseLength(let phaseLength): return phaseLength
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An effect that wiggles the view when a change happens.
|
|
///
|
|
/// - Parameter rate: The rate of the wiggle.
|
|
static func wiggle(rate: WiggleRate) -> AnyChangeEffect {
|
|
.simulation({ change in
|
|
WiggleSimulationModifier(impulseCount: change, phaseLength: rate.phaseLength)
|
|
})
|
|
}
|
|
}
|
|
|
|
internal struct WiggleSimulationModifier: ViewModifier, Simulative {
|
|
// TODO: Not used, remove from protocol
|
|
var initialVelocity: CGFloat = 0
|
|
|
|
var impulseCount: Int
|
|
|
|
var phaseLength: CGFloat
|
|
|
|
@Environment(\.isConditionalEffect)
|
|
private var isConditionalEffect
|
|
|
|
@State
|
|
private var wiggleCount: CGFloat = 0
|
|
|
|
@State
|
|
private var displacement: CGFloat = 0
|
|
|
|
@State
|
|
private var integrator: SecondOrderDynamics<CGFloat> = SecondOrderDynamics(
|
|
f: 3,
|
|
zeta: 0.85,
|
|
r: -0.2
|
|
)
|
|
|
|
fileprivate var target: CGFloat {
|
|
16 * sin(2 * .pi * wiggleCount)
|
|
}
|
|
|
|
private var isSimulationPaused: Bool {
|
|
displacement == .zero && wiggleCount <= 0
|
|
}
|
|
|
|
public func body(content: Content) -> some View {
|
|
let t = Transform3DEffect(
|
|
angle: .degrees(displacement / 2),
|
|
axis: (0, 0, 1)
|
|
)
|
|
|
|
TimelineView(.animation(paused: isSimulationPaused)) { context in
|
|
content
|
|
.modifier(t)
|
|
.onChange(of: context.date) { (newValue: Date) in
|
|
let duration = Double(newValue.timeIntervalSince(context.date))
|
|
withAnimation(nil) {
|
|
update(max(0, min(duration, 1 / 30)))
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: impulseCount) { newValue in
|
|
withAnimation(nil) {
|
|
wiggleCount += 2
|
|
|
|
if wiggleCount > 3 {
|
|
wiggleCount = 2 + fmod(wiggleCount, 1)
|
|
}
|
|
|
|
if isConditionalEffect {
|
|
wiggleCount += 4
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func update(_ step: Double) {
|
|
displacement = integrator.update(target: target, timestep: step)
|
|
|
|
if !displacement.isNormal {
|
|
displacement = 0
|
|
}
|
|
|
|
wiggleCount = clamp(0, wiggleCount - 2 * (step / phaseLength), .infinity)
|
|
}
|
|
}
|
|
|
|
|
|
#if os(iOS) && DEBUG
|
|
struct WiggleEffect_Previews: PreviewProvider {
|
|
struct Preview: View {
|
|
@State
|
|
var value: Int = 0
|
|
|
|
var body: some View {
|
|
VStack(spacing: 8) {
|
|
Spacer()
|
|
|
|
VStack(spacing: 32) {
|
|
Label("Answer", systemImage: "phone.fill")
|
|
.foregroundColor(.white)
|
|
.padding()
|
|
.background(.green, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
.changeEffect(.wiggle(rate: .fast), value: value)
|
|
.changeEffect(.shine(angle: .degrees(90), duration: 0.75), value: value)
|
|
.tint(.green)
|
|
.font(.largeTitle)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Stepper(value: $value) {
|
|
Text("Value ") + Text("(\(value.formatted()))").foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
struct Preview2: View {
|
|
@State
|
|
var isCalling: Bool = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: 8) {
|
|
Spacer()
|
|
|
|
VStack(spacing: 32) {
|
|
Label("Answer", systemImage: "phone.fill")
|
|
.foregroundColor(.white)
|
|
.padding()
|
|
.background(.green, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
.conditionalEffect(.repeat(.wiggle(rate: .fast), every: 2), condition: isCalling)
|
|
.tint(.green)
|
|
.font(.largeTitle)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Toggle("Calling", isOn: $isCalling)
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
static var previews: some View {
|
|
Preview()
|
|
.preferredColorScheme(.dark)
|
|
.previewDisplayName("Change Effect")
|
|
Preview2()
|
|
.preferredColorScheme(.dark)
|
|
.previewDisplayName("Conditional Effect")
|
|
}
|
|
}
|
|
#endif
|