const secondsPerFrame = 16.666 / 1000 const precision = 0.01 const step = ( lastValue, lastVelocity, toValue, stiffness, damping ) => { const spring = -stiffness * (lastValue - toValue) const damper = -damping * lastVelocity const all = spring + damper const nextVelocity = lastVelocity + all * secondsPerFrame const nextValue = lastValue + nextVelocity * secondsPerFrame const shouldRest = Math.abs(nextVelocity) < precision && Math.abs(nextValue - toValue) < precision return { velocity: shouldRest ? 0 : nextVelocity, value: shouldRest ? toValue : nextValue, } } const spring = ({ stiffness = 180, damping = 12, onRest = (() => {}), toValue }) => onUpdate => { let frame const tick = (curr) => { const { velocity, value } = step(curr.value, curr.velocity, toValue, stiffness, damping) onUpdate({ velocity, value }) if(velocity !== 0 && value !== toValue) { frame = requestAnimationFrame(() => tick({ value, velocity })) } else { onRest() } } return { start: (value) => { frame = requestAnimationFrame(() => tick(value)) }, stop: () => { cancelAnimationFrame(frame) }, } } class Value { constructor(initialValue) { this.value = { value: initialValue, velocity: 0, } this.listeners = new Set() this.animation = null } addListener(listener) { this.listeners.add(listener) return () => { this.listeners.delete(listener) } } updateValue(nextValue) { this.value = nextValue this.listeners.forEach(listener => listener(this.value.value)) } animate(animation) { if(this.animation) { this.animation.stop() } this.animation = animation((v) => this.updateValue(v)) this.animation.start(this.value) } } const scale = new Value(1) scale.addListener(value => myElement.style.transform = `scale(${ value })`) myElement.onmousedown = () => { scale.animate(spring({ toValue: 2 })) } myElement.onmouseup = () => { scale.animate(spring({ toValue: 1 })) }
<div id="myElement">Press me</div>
#myElement { width: 100px; height: 100px; background: blue; color: white; text-align: center; font-family: sans-serif; } body { margin: 0; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; }