Skip to content

Instantly share code, notes, and snippets.

@carson-katri
Last active August 10, 2020 17:42
Show Gist options
  • Save carson-katri/4cb2b963bc23bc0d87f31712e23ceb01 to your computer and use it in GitHub Desktop.
Save carson-katri/4cb2b963bc23bc0d87f31712e23ceb01 to your computer and use it in GitHub Desktop.
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