Created
April 21, 2023 05:01
-
-
Save micahscopes/10b5f59724221a3611d7529af085cb51 to your computer and use it in GitHub Desktop.
Spring follower
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 requestAnimationFrame from 'raf'; | |
export default class SpringFollow { | |
constructor(target, stiffness = 15, damping = 15) { | |
this.target = target; | |
this.stiffness = stiffness; | |
this.damping = damping; | |
this.velocity = this._initializeVelocity(target); | |
this.animationFrameId = null; | |
this.currentValues = this._clone(target); | |
this.paused = false; | |
} | |
_initializeVelocity(value) { | |
if (typeof value === 'number') { | |
return 0; | |
} else if (Array.isArray(value) || ArrayBuffer.isView(value)) { | |
return value.map(() => 0); | |
} else { | |
const velocity = {}; | |
for (const key in value) { | |
velocity[key] = 0; | |
} | |
return velocity; | |
} | |
} | |
_clone(value) { | |
if (Array.isArray(value)) { | |
return [...value]; | |
} else if (ArrayBuffer.isView(value)) { | |
return value.slice(); | |
} else if (typeof value === 'object') { | |
return { ...value }; | |
} else { | |
return value; | |
} | |
} | |
updateTarget(newTarget) { | |
this.target = newTarget; | |
} | |
updateInitialValues(newInitialValues) { | |
this.currentValues = this._clone(newInitialValues); | |
} | |
_interpolateSpring(currentValue, targetValue, velocity, stiffness, damping, dt) { | |
const displacement = targetValue - currentValue; | |
const springForce = stiffness * displacement; | |
const dampingForce = -damping * velocity; | |
const totalForce = springForce + dampingForce; | |
const newVelocity = velocity + totalForce * dt; | |
const newCurrentValue = currentValue + newVelocity * dt; // Use the updated velocity | |
return { newCurrentValue, newVelocity }; | |
} | |
step(dt) { | |
if (this.paused) { | |
return this.currentValues; | |
} | |
const interpolate = (currentValue, targetValue, velocity) => | |
this._interpolateSpring( | |
currentValue, | |
targetValue, | |
velocity, | |
this.stiffness, | |
this.damping, | |
dt | |
); | |
if (typeof this.target === 'number') { | |
const { newCurrentValue, newVelocity } = interpolate( | |
this.currentValues, | |
this.target, | |
this.velocity | |
); | |
this.currentValues = newCurrentValue; | |
this.velocity = newVelocity; | |
} else if (Array.isArray(this.target) || ArrayBuffer.isView(this.target)) { | |
for (let i = 0; i < this.target.length; i++) { | |
const { newCurrentValue, newVelocity } = interpolate( | |
this.currentValues[i], | |
this.target[i], | |
this.velocity[i] | |
); | |
this.currentValues[i] = newCurrentValue; | |
this.velocity[i] = newVelocity; | |
} | |
} else { | |
for (const key in this.target) { | |
const { newCurrentValue, newVelocity } = interpolate( | |
this.currentValues[key], | |
this.target[key], | |
this.velocity[key] | |
); | |
this.currentValues[key] = newCurrentValue; | |
this.velocity[key] = newVelocity; | |
} | |
} | |
return this.currentValues; | |
} | |
animate(onUpdate) { | |
const stepDuration = 1 / 60; // 60 FPS | |
const loop = () => { | |
const interpolatedValues = this.step(stepDuration); | |
onUpdate(interpolatedValues); | |
this.animationFrameId = requestAnimationFrame(loop); | |
}; | |
this.stop(); // Stop any ongoing animation before starting a new loop | |
this.animationFrameId = requestAnimationFrame(loop); | |
} | |
pause() { | |
this.paused = true; | |
} | |
resume() { | |
this.paused = false; | |
} | |
stop() { | |
cancelAnimationFrame(this.animationFrameId); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment