Springify.js 🌿

Springify is a tiny lil' (under 1kb gzip) utility function for creating dynamic spring animations.

It's a bare bones but flexible utility that can be used to create a range of useful and fun interactions.

Install via npm

          
npm i springify
          
        

Basic usage

          
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;
          
        

Default values

            
// 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.

Spring from --> to

Using static values

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';
});
            
          

Button press

              
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;
  });
});
              
            

Dynamic spring linked to scrolling

🕷
              
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;
});
              
            

Dynamic spring linked to mouse movements

🚁
          
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);
});