JSFiddle - React, Tailwind, and code Playground
by bloodyowl
HTML
<div id="myElement">Press me</div>
CSS
#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;
}
JavaScript
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...