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

```js
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:
   - `<h1>The future is in your hands</h1>`
   - `<div class="copyright-info">` with two `<p>` elements: `© 2025 Oblivion Decks` and `All rights reserved.`
   - `<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

```js
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** (`img1`…`img15`) — 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.
