# Image Explosion Scroll Animation

## Goal

Build a 4-section scroll page whose payoff is the footer: when the footer scrolls at least half-way into view, **15 photos erupt from below the footer's bottom edge like confetti**, launched upward with randomized velocity and spin, then arc back down under simulated gravity until they fall out of sight. The whole effect is a hand-rolled particle physics system (velocity + gravity + friction + rotation) driven by `requestAnimationFrame` — **no GSAP, no libraries, zero dependencies**.

## Tech

Vanilla HTML/CSS/JS. `script.js` is loaded as an ES module (`<script type="module" src="./script.js">`) but needs **no imports whatsoever** — do not install or import GSAP, Lenis or anything else. Everything is plain DOM + rAF.

## Layout / HTML

Four blocks in `<body>`, in this order:

```
<section class="hero"></section>

<section class="about">
  <p>
    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**?
  </p>
</section>

<section class="outro"></section>

<footer>
  <h1>The future is in your hands</h1>
  <div class="copyright-info">
    <p>&copy; 2025 Oblivion Decks</p>
    <p>All rights reserved.</p>
  </div>

  <div class="explosion-container"></div>
</footer>

<script type="module" src="./script.js"></script>
```

Notes:
- The `**double asterisks**` in the paragraph are **literal text characters** (raw markdown-style emphasis left unrendered) — keep them.
- `.hero` and `.outro` are empty; their imagery comes from CSS `background`.
- `.explosion-container` starts empty; JS injects the particle `<img>` elements into it.

## Styling

- Global reset: `* { margin: 0; padding: 0; box-sizing: border-box; }`.
- `p { text-transform: uppercase; font-family: "Akkurat Mono", monospace; font-size: 14px; }` — a monospaced family; the generic `monospace` fallback is fine.
- `img { width: 100%; height: 100%; object-fit: cover; }`.
- Every `section`: `position: relative; width: 100vw; height: 100svh; padding: 2em;`.
- `.hero`: full-bleed background photo — `background: url(<hero image>) no-repeat 50% 50%; background-size: cover;`.
- `.about`: `color: #000; background-color: #e3e3db;` (warm off-white), flex, centered both axes; `.about p { width: 50%; text-align: center; }`.
- `.outro`: full-bleed background photo, same pattern as `.hero`.
- `footer`: `position: relative; width: 100vw; height: 75svh; background-color: #0f0f0f; color: #fff; padding: 2em; display: flex; flex-direction: column; justify-content: space-between; align-items: center; overflow: hidden;`. The `overflow: hidden` is essential — it clips the particles while they idle below the footer and after they fall back out.
- `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 face; if unavailable, a bold condensed sans fallback (e.g. `sans-serif`) is acceptable.
- `.copyright-info`: `width: 100%; display: flex; justify-content: space-between;` (the two `<p>`s sit at opposite edges).
- `.explosion-container`: `position: absolute; bottom: 0; left: 0; width: 100%; height: 200%; pointer-events: none;`. The 200% height (twice the footer) matters — the fall-out check below measures against half of this container's height.
- `.explosion-particle-img`: `position: absolute; bottom: -200px; left: 50%; width: 150px; height: auto; object-fit: cover; transform: translateX(-50%); will-change: transform;`. Every particle starts at the same spot: horizontally centered, 200px **below** the container's bottom edge (hidden by the footer's `overflow: hidden`).

## The explosion effect (vanilla physics — be exact)

There is no GSAP, no timeline, no ScrollTrigger. The animation is a custom particle engine. Reproduce these numbers exactly.

### Config (module-scope constants)

```
const config = {
  gravity: 0.25,        // px/frame² added to vy every frame
  friction: 0.99,       // per-frame decay multiplier on vx, vy and spin
  imageSize: 150,       // particle width in px
  horizontalForce: 20,  // spread of the random horizontal launch velocity
  verticalForce: 15,    // base upward launch velocity
  rotationSpeed: 10,    // spread of the random spin speed
  resetDelay: 500,      // ms after landing before the effect may re-fire
};
```

- `imageParticleCount = 15`; build an `imagePaths` array of the 15 particle image URLs (`img1.jpg` … `img15.jpg`).
- Module state: `explosionContainer`, `footer`, `explosionTriggered = false`, `particles = []`.

### `Particle` class

Constructor takes the `<img>` element and initializes:
- `x = 0`, `y = 0` (offsets in px from the CSS resting position)
- `vx = (Math.random() - 0.5) * config.horizontalForce` → uniform in **−10 … +10** px/frame
- `vy = -config.verticalForce - Math.random() * 10` → uniform in **−15 … −25** px/frame (negative = upward)
- `rotation = 0`
- `rotationSpeed = (Math.random() - 0.5) * config.rotationSpeed` → uniform in **−5 … +5** deg/frame

`update()` — called once per frame:
1. `this.vy += config.gravity;` (gravity pulls down)
2. `this.vx *= config.friction; this.vy *= config.friction; this.rotationSpeed *= config.friction;` (air drag on everything, including spin)
3. `this.x += this.vx; this.y += this.vy; this.rotation += this.rotationSpeed;`
4. Write straight to the element: ``this.element.style.transform = `translate(${this.x}px, ${this.y}px) rotate(${this.rotation}deg)`;`` — note this inline transform intentionally **replaces** the CSS `translateX(-50%)` once animation starts; that's how the original behaves.

Net motion: every card launches from the same point with a random up-and-out velocity, decelerates, apexes, then accelerates back down past its start point while its spin gradually damps out — a firework/confetti burst.

### `createParticles()`

1. `explosionContainer.innerHTML = ""` and reset `particles = []`.
2. For each of the 15 paths: create an `<img>`, set `src`, add class `explosion-particle-img`, set inline `style.width = "150px"`, append to the container.
3. Query all `.explosion-particle-img` inside the container and map each element to `new Particle(element)`, stored in `particles`.

### `explode()`

1. Guard: `if (explosionTriggered) return;` then set `explosionTriggered = true`.
2. Call `createParticles()` (fresh DOM elements + fresh random velocities every burst).
3. Start a rAF loop:
   - Each frame, call `update()` on every particle.
   - **Termination check**: when **every** particle satisfies `particle.y > explosionContainer.offsetHeight / 2` (i.e. it has fallen at least half the container's height — one full footer height — below its start), `cancelAnimationFrame(...)`, stop the loop with a `finished` flag, and after `config.resetDelay` (500 ms) set `explosionTriggered = false` so the effect can fire again.

### Scroll trigger

`checkFooterPosition()`:
```
const footerRect = footer.getBoundingClientRect();
if (!explosionTriggered && footerRect.top <= window.innerHeight - footerRect.height * 0.5) {
  explode();
}
```
i.e. the burst fires the moment **at least half of the footer is inside the viewport**.

### `init()`

Run on `DOMContentLoaded` (with the usual `document.readyState === "loading"` guard so it also works if the script executes late):
1. Query `.explosion-container` and `footer`.
2. **Preload** all 15 images: `new Image().src = path` for each.
3. Call `createParticles()` once up front so the images exist in the DOM at rest (invisible — parked 200px below the footer and clipped).
4. Scroll listener, debounced with a 10 ms `setTimeout` (`clearTimeout` + `setTimeout(checkFooterPosition, 10)` on every `scroll` event).
5. One initial `setTimeout(checkFooterPosition, 500)` so the effect also fires if the page loads already scrolled to the footer.
6. `resize` listener that simply resets `explosionTriggered = false`.

## Assets / images

17 images total, all landscape photos from one cohesive, moody, cinematic set (e.g. retro-futuristic concept-vehicle photography — dark, dramatic lighting; no real brands or logos):

- **1 hero background** (~16:9): full-bleed cover background of the opening section.
- **1 outro background** (~16:9): full-bleed cover background of the section just before the footer.
- **15 particle images** (~3:2 landscape): rendered as small 150px-wide cards with `height: auto` (native aspect preserved), these are the photos that explode out of the footer. They should read as a varied but matching collection.

## Behavior notes

- The explosion **re-fires**: 500 ms after all particles have fallen out, the guard flag resets, so any subsequent scroll event while the footer is still half-visible triggers a brand-new burst (with new random velocities). Resizing the window also re-arms it.
- Particles are purely decorative: `pointer-events: none` on the container, `will-change: transform` on each card.
- All physics is frame-based (per-rAF-tick), not time-based — no delta-time correction, matching the original.
- No reduced-motion handling in the original.
- The page is a normal scroll document (100svh + 100svh + 100svh + 75svh); nothing is pinned and scroll is native (no smooth-scroll library).
