All components

Image Explosion with JS Physics

JavaScript animation component · Published 2026-07-27 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

As the footer scrolls into view, 15 images burst upward from the bottom edge and rain back down with a hand-rolled requestAnimationFrame physics simulation (per-particle velocity, gravity, friction and rotation). A scroll listener watches the footer's bounding rect and fires the one-shot explosion, resetting after the particles fall past the container midpoint. Lenis provides the smooth scroll.

How it's built

Categoryscroll
Techlenis
Complexitypage
Performance costlight
Mobile-safeyes
Scrollhijacks scrolling

scroll-trigger particles physics explosion image-burst lenis requestanimationframe footer editorial

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Image Explosion Scroll Animation

Goal

Build a single scrollable page where, the moment the footer scrolls halfway into view, 15 images violently burst upward out of the footer's bottom edge and rain back down under gravity — a one-shot "image explosion" driven by a hand-rolled requestAnimationFrame physics simulation (per-particle velocity, gravity, friction and rotation). No GSAP is used for this effect; the physics loop IS the effect.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use lenis (npm) for smooth scrolling. No GSAP plugins are required — the explosion is pure JavaScript physics updating style.transform inside a requestAnimationFrame loop.

import Lenis from "lenis";

Wrap all JS in a DOMContentLoaded listener.

Layout / HTML

Four stacked blocks, in this order:

  1. <section class="hero"> — empty; full-screen background image.
  2. <section class="about"> — contains one <p> of editorial copy (centered). Use this fictional text: "The world collapsed, but the game survived. In the neon-lit ruins of civilization, the last remnants of power aren't in governments or corporations—they're in the Oblivion Decks. Each card carries a fragment of lost history, a code of survival, a weapon of deception. The elite hoard them. The rebels steal them. The desperate gamble their lives for them. Do you have what it takes to play the game that decides the future?" (the ** markers are literal text characters, not bold markup).
  3. <section class="outro"> — empty; full-screen background image.
  4. <footer> — contains:
  5. <h1>The future is in your hands</h1>
  6. <div class="copyright-info"> with two <p> elements: © 2025 Oblivion Decks and All rights reserved.
  7. <div class="explosion-container"></div> — empty; the JS injects the particle <img> elements here.

Styling

  • Global reset: * { margin: 0; padding: 0; box-sizing: border-box; }.
  • All <p>: text-transform: uppercase; font-family: "Akkurat Mono", monospace; font-size: 14px;.
  • All <img>: width: 100%; height: 100%; object-fit: cover;.
  • Every section: position: relative; width: 100vw; height: 100svh; padding: 2em;.
  • .hero and .outro: full-bleed background image, no-repeat 50% 50%, background-size: cover (each with its own image).
  • .about: color: #000; background-color: #e3e3db; flexbox centering both axes; its p is width: 50%; text-align: center;.
  • footer: position: relative; width: 100vw; height: 75svh; background-color: #0f0f0f; color: #fff; padding: 2em; — column flexbox with justify-content: space-between; align-items: center; and, critically, overflow: hidden (particles must be clipped by the footer).
  • footer h1: text-transform: uppercase; font-family: "FK Screamer", sans-serif; font-size: 12vw; font-weight: 500; line-height: 0.85; (a very heavy condensed display sans; fall back to any ultra-condensed sans-serif).
  • .copyright-info: width: 100%; display: flex; justify-content: space-between;.
  • .explosion-container: position: absolute; bottom: 0; left: 0; width: 100%; height: 200%; pointer-events: none; (twice the footer's height so particles have vertical room).
  • .explosion-particle-img: position: absolute; bottom: -200px; left: 50%; width: 150px; height: auto; object-fit: cover; transform: translateX(-50%); will-change: transform; — all 15 particles start stacked at the same point, hidden 200px below the container's bottom edge, horizontally centered.

The effect (be precise — this is the star)

Smooth scroll

Instantiate Lenis with exactly { autoRaf: true, lerp: 0.5 }.

Config constants
const config = {
  gravity: 0.25,        // px/frame² added to vy every frame
  friction: 0.99,       // per-frame multiplier on vx, vy and rotationSpeed
  imageSize: 150,       // particle width in px
  horizontalForce: 20,  // max horizontal launch speed spread
  verticalForce: 15,    // base upward launch speed
  rotationSpeed: 10,    // max rotation speed spread (deg/frame)
  resetDelay: 500,      // ms before the explosion can re-arm
};
Particles
  • imageParticleCount = 15. Build an array of 15 image paths and preload each one by creating new Image() and assigning src (no need to attach to DOM).
  • createParticles(): set explosionContainer.innerHTML = "", then for each path create an <img> with class explosion-particle-img, set src, set inline style.width = config.imageSize + "px", and append to .explosion-container.
Physics — Particle class

Constructor (takes the element):

  • x = 0, y = 0 (positions are relative offsets from the CSS resting spot).
  • vx = (Math.random() - 0.5) * config.horizontalForce → range −10..+10 px/frame.
  • vy = -config.verticalForce - Math.random() * 10 → range −15..−25 px/frame (negative = upward).
  • rotation = 0, rotationSpeed = (Math.random() - 0.5) * config.rotationSpeed → range −5..+5 deg/frame.

update() — called once per rAF frame:

  1. vy += config.gravity (gravity pulls down).
  2. vx *= config.friction; vy *= config.friction; rotationSpeed *= config.friction;
  3. x += vx; y += vy; rotation += rotationSpeed;
  4. Write element.style.transform = "translate(" + x + "px, " + y + "px) rotate(" + rotation + "deg)". Note this inline transform replaces the CSS translateX(-50%) centering — that's intentional and matches the original look.

Net motion: each image launches upward at 15–25 px/frame with a random horizontal drift and random spin, decelerates (gravity + friction), arcs over, and falls back down out through the footer's bottom edge.

Trigger + loop — explode()
  • Guard with a module-level explosionTriggered boolean: if already true, return; else set it true.
  • Call createParticles() (fresh DOM nodes each explosion), query all .explosion-particle-img, map each to a new Particle(el).
  • Start a rAF loop: every frame, update all particles; keep looping. When every particle's y > explosionContainer.offsetHeight / 2 (i.e. all have fallen well below their start point — container is 200% of footer height, so half of it is one full footer height), cancelAnimationFrame the loop and, after config.resetDelay (500 ms), set explosionTriggered = false so the effect can fire again.
Scroll detection
  • checkFooterPosition(): read footer.getBoundingClientRect(); if !explosionTriggered && footerRect.top <= window.innerHeight - footerRect.height * 0.5, call explode(). (Fires once the footer is at least half revealed at the bottom of the viewport.)
  • Listen to window "scroll" and debounce with clearTimeout + setTimeout(checkFooterPosition, 10).
  • On window "resize", set explosionTriggered = false (re-arm).
  • On init: call createParticles() once (pre-populates the hidden stack), then setTimeout(checkFooterPosition, 500) in case the page loads already scrolled to the footer.

Assets / images

17 images total, a cohesive stylized automotive/editorial photo set (no logos or brand marks):

  • Hero background — 1 full-bleed landscape image, ~3:2 (e.g. a dramatic studio 3D render / concept-car style shot).
  • Outro background — 1 different full-bleed landscape image, ~3:2, same moody editorial style.
  • 15 particle images (img1img15) — mixed aspect ratios: mostly 16:9 landscape shots with a few portrait ones (~2:3 / ~5:6) sprinkled in. Rendered at 150px wide with natural height, so exact ratios are flexible; visually they should read as a varied editorial series (night roads, studio renders, desert scenes, close-up details, etc.).

Behavior notes

  • The explosion is one-shot per approach but re-arms itself: after all particles fall past the halfway line of the container and the 500 ms delay elapses, scrolling away and back re-triggers it. Resizing also re-arms it.
  • pointer-events: none on the container keeps the falling images from blocking clicks.
  • overflow: hidden on the footer is essential — particles must appear to erupt from and disappear behind the footer's bottom edge, never spill over other sections.
  • Works on mobile; no WebGL/canvas, just DOM images and transforms.