Why a Physics Engine Was the Wrong Tool

When a web experience needs to feel tactile — squishy, bouncy, or responsive to every click — the conventional reflex is to pull in a physics engine. Matter.js and WebGL are standard choices for gamified sites. But there's an important difference between motion that is plausible and motion that is intentional.

For the Stress Release game, built by our team at Isadora Agency, the goal was a digital squeeze toy where users distort animated UI characters. Prototyping with physics engines quickly exposed a core problem: those tools generate realistic, emergent behavior, but the animators had designed exact, frame-by-frame reactions. Realistic rubber-ball physics wasn't the desired feel. We needed a character to react with a precise 181-frame build-up on a "mega squeeze" and a specific release sequence — not an algorithmic approximation of that motion. The architecture had to serve the art direction, not override it. So we scrapped physics entirely and relied on programmatic Lottie state controls, DOM manipulation, and distance-based math.

With Lottie's native API driving the animation layer, the interaction layer only needs to be a precise trigger. The tighter the click-to-reaction loop, the more deterministic control matters.

Hit Detection: Radial Mapping and Scoring Rings

Because rendering is handled by the Lottie runtime (which draws vector animations as SVGs), all elements remain in the standard DOM. That means we select characters by ID and class, then drive behavior via Lottie animation segments, CSS transforms, and click-event math.

To deliver a satisfying "tactile feel," we used radial input mapping. Each click is first converted from page coordinates into the character's local coordinate space. The click is then measured against the character's center point, and the distance drives score, feedback intensity, and the position of an explosion effect:

Calculating the straight-line distance from the center uses the Pythagorean theorem:

const deltaX = clickX - centerX;
const deltaY = clickY - centerY;
const distanceFromCenter = Math.sqrt(deltaX * deltaX + deltaY * deltaY);

That single number powers everything:

const score = Math.max(0, maxScore - distanceFromCenter);
const feedbackIntensity = 1 - distanceFromCenter / maxRadius;
explosionElement.style.transform = `translate(${deltaX}px, ${deltaY}px)`;

This creates a dartboard-like set of concentric scoring rings around each character's center. Notably, the visual complexity of the Lottie SVG never matters for hit detection — the hitbox is always a clean circle. The explosion animation is repositioned to the offset vector (deltaX, deltaY), the same values used for scoring, so the effect always lands exactly where the user clicked. That spatial accuracy is what produces the "I hit that" sensation.

State Control: Play Segments, Not Simulations

Handling desktop clicks and mobile taps is straightforward since the characters are DOM-managed SVG elements. Native event listeners handle both, with no raycasting or coordinate remapping layers. Lottie supplies all squash, stretch, and bounce effects internally through its animation curves.

Each character has a predefined set of animation sections — idle loops, reaction sequences, and end states — stored as frame ranges. A click simply triggers the correct segment based on the current game state:

const segments = {
  idle: [0, 90],
  reaction1: [91, 150],
  reaction2: [151, 210],
  explosion: [211, 260]
};

// On click destroy an explosion effect to keep memory low
function playExplosion(x, y) {
  // Create a new Lottie instance, play the explosion segment,
  // destroy it once complete
}

// On idle return, loop the idle segment indefinitely
animation.playSegments([0, 90], true);

On each click we advance through the defined play order and fire the appropriate segment:

let currentStateIndex = 0;
const playOrder = ['idle', 'reaction1', 'reaction2', 'explosion'];

function handleClick() {
  currentStateIndex = (currentStateIndex + 1) % playOrder.length;
  const nextState = playOrder[currentStateIndex];
  const [start, end] = segments[nextState];
  animation.playSegments([start, end], false);
}

Once a segment ends, control returns to the idle loop. The "mega squeeze" charge-up bar continues looping on a specific frame range until it is released, maintaining full control over pacing.

Responsive Scaling With CSS Variables

Staying in the DOM sidesteps the complications of scaling bounding boxes and physics colliders across screen sizes. Resizing is handled entirely through CSS custom properties. On each window resize, the updated variables flow into the layout and the Lottie SVGs scale naturally inside their containers without losing their current state:

function handleResize() {
  const container = document.querySelector('.game-container');
  const rect = container.getBoundingClientRect();
  container.style.setProperty('--game-scale', Math.min(rect.width / 1080, 1));
  container.style.setProperty('--character-size', `${Math.min(rect.width / 6, 160)}px`);
}

Everything from character size to spacing reads from these variables, so the whole experience warps gracefully without any game-logic recalculations.

Performance: Managing Lottie's Real Cost

This architecture provides total control over art direction, but it has a real downside: file size. The game ships 21 unique character animations plus multiple explosion variants. To keep the experience fluid on mobile, several aggressive optimizations were needed:

  • Connection monitoring: Asset load time is tracked from performance.now(), allowing the app to flag when total load time exceeds five seconds.
  • Sequential asset loading: Instead of initializing all 21 characters at once, they are loaded in pairs using await, moving forward only after each pair finishes. This avoids blocking the browser with a burst of simultaneous network and render work.
  • Aggressive memory management: Heavy explosion animations are destroyed and recreated on demand rather than kept in memory, trading a small instantiation cost for a much lower idle footprint.
  • Dynamic quality reduction: Different quality settings are applied per character via a single setQuality() call, depending on how close the character is to the user's focus.

Letting Design Dictate the Stack

The central lesson from this project is straightforward: let design requirements choose the technology. For bespoke, highly choreographed visual reactions, programmatic state control beats emergent simulation. Mapping Lottie's native timeline capabilities to the DOM delivers exceptionally rich, tactile interactions while leaving the animators in charge of exactly how everything feels. The code's job is narrower but just as critical: listen for input, calculate the response, and fire the right animation at the right frame.

For reference: the loadAnimation(), playSegments(), setSpeed(), and setQuality() methods — documented in the official Lottie Web docs — are the four tools that power this entire interaction model. A live example with all 21 characters is available, and a simplified demo on CodePen shows a single character reacting to clicks using playSegments().