Last active
August 10, 2020 17:42
-
-
Save carson-katri/4cb2b963bc23bc0d87f31712e23ceb01 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import Combine | |
import PlaygroundSupport | |
struct SpringSolver { | |
let ƛ: CGFloat | |
let w0: CGFloat | |
let wd: CGFloat | |
/// Initial velocity | |
let v0: CGFloat | |
/// Target value | |
let s0: CGFloat = 1 | |
init(mass: CGFloat, stiffness: CGFloat, damping: CGFloat, initialVelocity: CGFloat) { | |
ƛ = (damping * 0.755) / (mass * 2) | |
w0 = sqrt(stiffness / 2) | |
wd = sqrt(abs(pow(w0, 2) - pow(ƛ, 2))) | |
v0 = initialVelocity | |
} | |
func solve(at t: CGFloat) -> CGFloat { | |
let y: CGFloat | |
if ƛ < w0 { | |
y = pow(CGFloat(M_E), -(ƛ * t)) * ((s0 * cos(wd * t)) + ((v0 + s0) * sin(wd * t))) | |
// } else if ƛ > w0 { // Skip overdamping | |
} else { | |
y = pow(CGFloat(M_E), -(ƛ * t)) * (s0 + ((v0 + (ƛ * s0)) * t)) | |
} | |
return 1 - y | |
} | |
func restingPoint(precision y: CGFloat) -> CGFloat { | |
log(y) / -ƛ | |
} | |
} | |
struct AnimationCurve: GeometryEffect { | |
let onChange: (CGFloat) -> Void | |
var animatableData: CGFloat = 0 { | |
didSet { | |
onChange(animatableData) | |
} | |
} | |
func effectValue(size: CGSize) -> ProjectionTransform { | |
.init() | |
} | |
} | |
final class AnimationObserver: ObservableObject { | |
@Published var curve = [(CFTimeInterval, CGFloat)]() | |
init() {} | |
func onChange(_ data: CGFloat) { | |
curve.append((CACurrentMediaTime(), data)) | |
} | |
} | |
struct Curve: Shape { | |
let curve: [(CFTimeInterval, CGFloat)] | |
func path(in rect: CGRect) -> Path { | |
guard let first = curve.first, | |
let last = curve.last else { | |
return Path() | |
} | |
let offset = CGFloat(first.0) | |
let scale = CGFloat(last.0) - CGFloat(first.0) | |
return Path { path in | |
path.move(to: .init(x: rect.minX, y: rect.maxY)) | |
for point in curve { | |
path.addLine(to: .init(x: rect.minX + ((CGFloat(point.0) - offset) / scale) * rect.width, y: rect.maxY - point.1 * rect.width)) | |
} | |
} | |
} | |
} | |
struct CurveGraph: View { | |
let curve: [(CFTimeInterval, CGFloat)] | |
let startColor: Color | |
let endColor: Color | |
var body: some View { | |
HStack(alignment: .bottom) { | |
VStack(alignment: .trailing) { | |
Text("\(curve.sorted { $0.1 > $1.1 }.first?.1 ?? 0, specifier: "%.2f")") | |
Spacer() | |
Text("0") | |
} | |
VStack(alignment: .leading) { | |
Curve(curve: curve) | |
.stroke(LinearGradient( | |
gradient: .init(colors: [startColor, endColor]), | |
startPoint: .leading, | |
endPoint: .trailing), | |
lineWidth: 2) | |
.frame(width: 100, height: 100) | |
.border(Color.primary.opacity(0.5)) | |
HStack(alignment: .bottom) { | |
Spacer() | |
Text("\((curve.last?.0 ?? 0) - (curve.first?.0 ?? 0), specifier: "%.2f")") | |
} | |
} | |
} | |
.font(.caption) | |
} | |
} | |
struct AnimationGraph: View { | |
let animation: Animation | |
@ObservedObject var observer = AnimationObserver() | |
@State private var animate = false | |
var body: some View { | |
Text("") | |
.modifier(AnimationCurve(onChange: observer.onChange, animatableData: animate ? 1 : 0)) | |
.onAppear { | |
withAnimation(animation) { | |
animate = true | |
} | |
} | |
CurveGraph(curve: observer.curve, startColor: .blue, endColor: .purple) | |
} | |
} | |
struct AnimationKind: Identifiable { | |
let name: String | |
let animation: Animation | |
let timingCurve: [(CFTimeInterval, CGFloat)] | |
let id: String | |
init(_ name: String, _ animation: Animation, _ timingCurve: [(CFTimeInterval, CGFloat)]) { | |
id = name | |
(self.name, self.animation, self.timingCurve) = (name, animation, timingCurve) | |
} | |
} | |
struct ContentView: View { | |
static func interpolatingSpring(mass: CGFloat = 1, | |
stiffness: CGFloat, | |
damping: CGFloat, | |
initialVelocity: CGFloat = 0) -> [(CFTimeInterval, CGFloat)] { | |
let solver = SpringSolver(mass: mass, stiffness: stiffness, damping: damping, initialVelocity: initialVelocity) | |
let detail: CGFloat = 10 | |
let steps = Array(0..<Int(ceil(solver.restingPoint(precision: 0.01)) * detail)) | |
return steps.map(CGFloat.init).map { | |
(CFTimeInterval($0 / 10), solver.solve(at: $0 / detail)) | |
} | |
} | |
static func spring(response: Double = 0.55, | |
dampingFraction: Double = 0.825, | |
blendDuration: Double = 0) -> [(CFTimeInterval, CGFloat)] { | |
if response == 0 { | |
return interpolatingSpring(stiffness: 999, damping: 999) | |
} else { | |
return interpolatingSpring(mass: 1, | |
stiffness: pow(2 * .pi / CGFloat(response), 2), | |
damping: 4 * .pi * CGFloat(dampingFraction) / CGFloat(response)) | |
} | |
} | |
let animations: [AnimationKind] = [ | |
// .init("easeInOut", .easeInOut, .timingCurve(0.25, 0.1, 0.25, 1)), | |
// .init("default", .default, .timingCurve(0.25, 0.1, 0.25, 1)), | |
// .init("easeIn", .easeIn, .timingCurve(0.42, 0, 1, 1)), | |
// .init("easeOut", .easeOut, .timingCurve(0, 0, 0.58, 1)), | |
// .init("linear", .linear, .timingCurve(0, 0, 1, 1)), | |
.init("interpolatingSpring (1, 0.825)", | |
.interpolatingSpring(stiffness: 1, damping: 0.825), | |
interpolatingSpring(stiffness: 1, damping: 0.825)), | |
.init("interpolatingSpring (0.5, 0.825)", | |
.interpolatingSpring(stiffness: 0.5, damping: 0.825), | |
interpolatingSpring(stiffness: 0.5, damping: 0.825)), | |
.init("spring()", | |
.spring(), | |
spring()), | |
.init("damping: 0.25", | |
.spring(dampingFraction: 0.25), | |
spring(dampingFraction: 0.25)), | |
.init("response: 0", | |
.spring(response: 0), | |
spring(response: 0)) | |
] | |
var body: some View { | |
VStack { | |
Text("SpringSolver Tests") | |
.font(.title) | |
.bold() | |
Divider() | |
ForEach(animations) { anim in | |
Text(anim.name) | |
.font(.headline) | |
HStack { | |
VStack { | |
AnimationGraph(animation: anim.animation) | |
Text("SwiftUI") | |
.bold() | |
.padding() | |
} | |
VStack { | |
CurveGraph(curve: anim.timingCurve, startColor: .orange, endColor: .red) | |
Text("SpringSolver") | |
.bold() | |
.padding() | |
} | |
} | |
Divider() | |
} | |
} | |
} | |
} | |
// Present the view in Playground | |
PlaygroundPage.current.liveView = NSHostingView(rootView: ContentView()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment