It's a bare bones but flexible utility that can be used to create a range of useful and fun interactions.
npm i springify
import { Springify } from 'springify';
const spring = new Springify({
input: 0,
onFrame: (output, velocity) => {
// output is the output value from the spring
// velocity is the velocity our spring is moving at
console.log(output);
console.log(velocity);
}
}
);
// Update the value springify will spring from the initial input value to the new one. It automatically starts the animation running when input is set.
spring.input = 500;
// stiffness: effective range from 0 - 100;
// damping: effective range from 0 - 100;
// mass: effective range from 0 - 100;
const spring = new Springify(
{
input: 0,
stiffness: 0,
damping: 30,
mass: 20,
onFrame: (output, velocity) => {},
onFinished: () => {},
}
);
Pass an input value into springify for the initial value to start from.
The onFrame callback function will receive properties for the springified output and velocity values. This function is executed every frame so put the logic in here to animate with the output and velocity values.
The onFrame callback uses requestAnimationFrame internally and won't start more than one animation loop at a time. So no need to throttle or debounce updating the input value.
The onFinished function will run once each time the spring comes to rest after animating.
Try interrupting the animation mid way through.
import { Springify } from 'springify';
const sailboat = document.querySelector('.sailboat');
const sailAwayButton = document.querySelector('.sailboat--away');
const sailBackButton = document.querySelector('.sailboat--back');
let direction = 'right';
const springySailboat = new Springify({
stiffness: 10,
damping: 80,
mass: 50,
onFrame: (output, velocity) => {
if (sailboat === null) return;
sailboat.style.transform = `rotate(${velocity * -0.3}deg) scaleX(${direction === 'right' ? -1 : 1})`;
sailboat.style.left = `${output}%`;
},
});
sailAway.addEventListener('click', () => {
springySailboat.input = 100;
direction = 'right';
});
sailBack.addEventListener('click', () => {
springySailboat.input = 0;
direction = 'left';
});
import { Springify } from 'springify';
const buttons = document.querySelectorAll('.demo-button');
buttons.forEach((button) => {
const springyButton = new Springify({
stiffness: 20,
damping: 30,
mass: 10,
input: 1,
onFrame: (output, velocity) => {
button.style.transform = `scaleX(${output + velocity * 0.1}) scaleY(${output})`;
},
});
button.addEventListener('mousedown', () => {
springyButton.input = 0.75;
});
button.addEventListener('touchstart', (e) => {
e.preventDefault();
springyButton.input = 0.75;
});
button.addEventListener('mouseup', () => {
springyButton.input = 1.1;
});
button.addEventListener('mouseenter', () => {
springyButton.input = 1.1;
});
button.addEventListener('mouseout', () => {
springyButton.input = 1;
});
button.addEventListener('touchend', () => {
springyButton.input = 1;
});
});
import { Springify } from 'springify';
const spider = document.querySelector('.spider');
const spiderArea = document.querySelector('.section--example-spider');
const springySpider = new Springify({
stiffness: 30,
damping: 50,
mass: 10,
onFrame: (output) => {
spider.style.transform = `translateY(${output}px)`;
},
});
window.addEventListener('scroll', () => {
springySpider.input = window.scrollY - spiderArea.offsetTop + window.innerHeight * 0.5;
});
import { Springify } from 'springify';
const helicopter = document.querySelector('.helicopter');
const helicopterDemo = document.querySelector('.section--example-helicopter');
const springResults = {
x: 0,
y: 0,
velocity: 0,
};
const helicopterTransform = (x: number, y: number, rotation: number) => {
return `translate(${x}px, ${y}px) rotate(${rotation * 0.05}deg)`;
};
const springyHelicopter = {
x: new Springify({
onFrame: (output, velocity) => {
springResults.x = output;
springResults.velocity = velocity;
if (!helicopter) return;
helicopter.style.transform = helicopterTransform(springResults.x, springResults.y, springResults.velocity);
},
}),
y: new Springify({
onFrame: (output) => {
springResults.y = output;
},
}),
};
const helicopterMove = (clientX, clientY) => {
// normalize the mouse coordinates to the helicopter demo area
const helicopterDemoRect = helicopterDemo.getBoundingClientRect();
const relativeX = clientX - (helicopterDemoRect.left + helicopterDemoRect.width * 0.5);
const relativeY = clientY - (helicopterDemoRect.top + helicopterDemoRect.height * 0.5);
// Send our updated values as the inputs to the spring
springyHelicopter.x.input = relativeX;
springyHelicopter.y.input = relativeY;
};
if (helicopterDemo === null) return;
helicopterDemo.addEventListener('mousemove', (e) => {
helicopterMove(e.clientX, e.clientY);
});
helicopterDemo.addEventListener('touchmove', (e) => {
helicopterMove(e.touches[0].clientX, e.touches[0].clientY);
});