@magic-spells/physics-engine

Physics
Engine

Spring-driven animation with real physics. These demos pair Physics Engine with @magic-spells/frame-engine to map spring motion onto any CSS property — colors, transforms, filters — all feeling alive.

Install npm i @magic-spells/physics-engine
Size (gzip) ~1.4 KB
Uses
import PhysicsEngine from '@magic-spells/physics-engine'

// Create a spring with custom tension & damping
const spring = new PhysicsEngine({
  attraction: 0.026,
  friction:   0.28
})

// Listen for physics updates every frame
spring.on('change', ({ position, progress }) => {
  element.style.transform = `translateX(${progress * 500}px)`
})

spring.on('complete', () => console.log('settled'))

// Animate from 0 → 1000 with initial velocity 20
await spring.animateTo(0, 1000, 20)

Metamorphosis

A spring drives progress through five Frame Engine keyframes. The orb morphs color, border-radius, scale, and glow — all with natural spring overshoot and settle.

view the spring
const morphSpring = new PhysicsEngine({
  attraction: 0.011,
  friction: 0.11,
});

morphSpring.on("change", ({ position, progress }) => {
  const styles = morphFrames.getFrame(progress);
  applyStyles(morphOrb, styles);
  morphOrb.style.boxShadow =
    `0 0 60px ${styles.backgroundColor}55, 0 0 120px ${styles.backgroundColor}22`;
  renderReadout(morphReadout, {
    progress: progress.toFixed(3),
    position: position.toFixed(1),
    backgroundColor: styles.backgroundColor,
    borderRadius: styles.borderRadius,
    transform: styles.transform,
  });
});

document.getElementById("morph-go").addEventListener("click", () => {
  morphSpring.animateTo(0, 2000, 10);
});

Spring Traverse

Physics spring propels a shape across the stage. Frame Engine maps the spring progress to compound transforms, color shifts, and border-radius — each interpolated independently.

view the spring
const traverseSpring = new PhysicsEngine({
  attraction: 0.03,
  friction: 0.11,
});

traverseSpring.on("change", ({ position, progress }) => {
  const frames = traverseForward
    ? traverseForwardFrames
    : traverseReturnFrames;
  const styles = frames.getFrame(progress);
  applyStyles(traverseBox, styles);
  traverseBox.style.boxShadow =
    `0 8px 40px ${styles.backgroundColor}33`;
  renderReadout(traverseReadout, {
    progress: progress.toFixed(3),
    position: position.toFixed(1),
    transform: styles.transform,
    backgroundColor: styles.backgroundColor,
    borderRadius: styles.borderRadius,
  });
});

traverseGoBtn.addEventListener("click", () => {
  traverseGoBtn.disabled = true;
  traverseSpring.animateTo(0, 1200, 20);
});

Parameter Lab

Adjust attraction and friction to feel how spring dynamics change. Higher attraction snaps faster. Higher friction damps the bounce. The shape auto-animates on each change.

Attraction 0.026
Friction 0.28
Distance 1200
Velocity 20
view the spring
function labAnimate() {
  const attraction =
    parseInt(document.getElementById("lab-attraction").value) / 1000;
  const friction =
    parseInt(document.getElementById("lab-friction").value) / 100;
  const distance = parseInt(
    document.getElementById("lab-distance").value,
  );
  const velocity = parseInt(
    document.getElementById("lab-velocity").value,
  );

  labSpring.stop();
  labSpring = new PhysicsEngine({ attraction, friction });
  wireLabSpring();
  labSpring.animateTo(0, distance, velocity);
}

document.getElementById("lab-run")
  .addEventListener("click", labAnimate);

Cascade

Eight elements, each driven by its own spring, launched in sequence. The same Frame Engine keyframes applied to every dot — the staggered timing creates a wave of color and motion.

view the spring
function cascadeLaunch(forward) {
  cascadeSprings.forEach((spring) => spring.stop());
  cascadeSprings.length = 0;

  cascadeDots.forEach((dot, i) => {
    setTimeout(() => {
      const spring = new PhysicsEngine();
      cascadeSprings.push(spring);

      spring.on("change", ({ progress }) => {
        const styles = cascadeFrames.getFrame(progress);
        applyStyles(dot, styles);
        dot.style.boxShadow =
          `0 4px 24px ${styles.backgroundColor}44`;
      });

      if (forward) {
        spring.animateTo(0, 1200, 20);
      } else {
        spring.animateTo(1200, 0, -20);
      }
    }, i * 60);
  });
}