Created
October 20, 2022 13:30
-
-
Save danielgolden/e71ad6fd19109ab34dd31f62cb25d920 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 "./custom-spring.css"; | |
// import { Pane } from "tweakpane"; | |
const box = document.querySelector(".box"); | |
interface SpringOptions { | |
stiffness: number; | |
mass: number; | |
damping: number; | |
end: number; | |
start: number; | |
velocity: number; | |
style: Keyframe; | |
} | |
// Adapted from https://blog.maximeheckel.com/posts/the-physics-behind-spring-animations | |
const generateSpringKeyframes = (springOptions: SpringOptions) => { | |
/* Object position and velocity. */ | |
let x = springOptions.start; | |
let v = springOptions.velocity; | |
/* Spring stiffness, in kg / s^2 */ | |
let k = -springOptions.stiffness; | |
/* Damping constant, in kg / s */ | |
let d = -springOptions.damping; | |
/* Framerate: we want 60 fps hence the framerate here is at 1/60 */ | |
let frameRate = 1 / 60; | |
let positions = []; | |
let i = 0; | |
/* We loop 600 times, i.e. for 600 frames which is equivalent to 10s*/ | |
while (i < 120) { | |
let Fspring = k * (x - springOptions.end); | |
let Fdamping = d * v; | |
let a = (Fspring + Fdamping) / springOptions.mass; | |
v += a * frameRate; | |
x += v * frameRate; | |
i++; | |
positions.push({ | |
position: x, | |
frame: i, | |
}); | |
} | |
const keyframes = positions.map(function (frame) { | |
const styleProperty: string = Object.keys(springOptions.style)[0]; | |
let styleValue = springOptions.style[styleProperty]; | |
styleValue = styleValue?.toString()?.replace("$", frame.position.toString()); | |
return { | |
[styleProperty]: styleValue, | |
}; | |
}); | |
return keyframes; | |
}; | |
const animationOptions: KeyframeAnimationOptions = { | |
duration: 700, | |
iterations: 1, | |
fill: "forwards", | |
}; | |
const springDepth = { value: -50 }; | |
const springPressOptions: SpringOptions = { | |
stiffness: 105, | |
mass: 2.2, | |
damping: 10.6, | |
end: springDepth.value, | |
start: 0, | |
velocity: 0, | |
style: { transform: `translate3d(0, 0, $px)` }, | |
}; | |
// const pane = new Pane(); | |
// const pressControls = pane.addFolder({ title: "Mouse down options" }); | |
// pressControls.addInput(springPressOptions, "end", { min: -150, max: 150, step: 1, label: "End" }); | |
// pressControls.addInput(springPressOptions, "stiffness", { min: 0, max: 500, step: 1, label: "Stiffness" }); | |
// pressControls.addInput(springPressOptions, "mass", { min: 0, max: 5, step: 0.1, label: "Mass" }); | |
// pressControls.addInput(springPressOptions, "damping", { min: 1, max: 50, step: 1, label: "Damping" }); | |
const springReleaseOptions: SpringOptions = { | |
stiffness: 105, | |
mass: 2.2, | |
damping: 10.6, | |
end: 0, | |
start: springDepth.value, | |
velocity: 0, | |
style: { transform: `translate3d(0, 0, $px)` }, | |
}; | |
// const releaseControls = pane.addFolder({ title: "Mouse up options" }); | |
// releaseControls.addInput(springReleaseOptions, "end", { min: -150, max: 150, step: 1, label: "End" }); | |
// releaseControls.addInput(springReleaseOptions, "stiffness", { min: 0, max: 500, step: 1, label: "Stiffness" }); | |
// releaseControls.addInput(springReleaseOptions, "mass", { min: 0, max: 5, step: 0.1, label: "Mass" }); | |
// releaseControls.addInput(springReleaseOptions, "damping", { min: 1, max: 50, step: 1, label: "Damping" }); | |
box?.addEventListener("mousedown", () => { | |
box?.animate(generateSpringKeyframes(springPressOptions), animationOptions); | |
}); | |
const springToNaturalState = (element: Element) => { | |
const matrix3d = getComputedStyle(element).transform; | |
const matrix3dArray = matrix3d.substring(9, matrix3d.length).split(","); | |
const cleanMatrix3d = matrix3dArray.map((item: string) => item.trim()); | |
const currentTranslateZ = cleanMatrix3d[14]; | |
springReleaseOptions.start = parseInt(currentTranslateZ); | |
element?.animate(generateSpringKeyframes(springReleaseOptions), animationOptions); | |
}; | |
box?.addEventListener("click", (e) => { | |
springToNaturalState(e.target as Element); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment