# Full-Screen Wheel Slider — Clip-Path Upward Wipe + Image Zoom-Settle + Rolodex Numbered Type

## Goal
Build a **full-screen vertical image slider driven entirely by the mouse wheel**. One full-bleed photo fills the viewport under a dark scrim; three tiny uppercase text read-outs (slide number, slide name, year) float over it. Each wheel step plays one 2-second choreographed transition: the incoming slide is **wiped up from the bottom edge by tweening its `clip-path` polygon**, its underlying image simultaneously **zooms/settles from `scale: 2` down to `scale: 1`** (and slides up from `top: 4em` to `0`), and the three stacked text columns each **roll up by exactly 30px** inside a 30px-tall masking window (a rolodex/odometer effect) with a slight per-column stagger. Scrolling back down reverses everything. The page itself never scrolls — the wheel event is intercepted and locked while animating.

## Tech
- Vanilla HTML / CSS / JS with ES module imports, bundled by a **Vite**-style dev server (npm project).
- **`gsap` (npm)** only — **no plugins** (no ScrollTrigger, no SplitText, no smooth-scroll library, no canvas/WebGL). All motion is plain `gsap.to` / `gsap.set` tweens fired on the native `wheel` event.
- Import:
```js
import gsap from "gsap";
```
- All logic runs inside a single `document.addEventListener("DOMContentLoaded", () => { … })` in `script.js` (loaded as `<script type="module" src="./script.js">`).

## Layout / HTML
Class names are load-bearing (the CSS masks and the JS selectors depend on them). Copy is fictional/neutral — no real brands.

```html
<div class="slider-content">                <!-- fixed dark scrim + all text, sits above every slide -->
  <div class="slide-number">
    <div class="prefix">                    <!-- the moving column: 5 stacked rows -->
      <div>1</div><div>2</div><div>3</div><div>4</div><div>5</div>
    </div>
    <div class="postfix"><span>/</span> 5</div>   <!-- static "/ 5" -->
  </div>

  <div class="slide-name">
    <div class="names">                     <!-- moving column: 5 stacked names -->
      <div>Ether Shift Mode</div>
      <div>Solar Thread</div>
      <div>Quantum Sheen Veil</div>
      <div>Flux Aura</div>
      <div>Echo Nimbus</div>
    </div>
  </div>

  <div class="slide-year">
    <div class="years">                     <!-- moving column: 5 stacked years -->
      <div>2023</div><div>2021</div><div>2022</div><div>2023</div><div>2017</div>
    </div>
  </div>
</div>

<div class="slider">
  <div class="slide" id="slide-1"><img src="/c/full-screen-slider/img-1.jpg" alt="" /></div>
  <div class="slide" id="slide-2"><img src="/c/full-screen-slider/img-2.jpg" alt="" /></div>
  <div class="slide" id="slide-3"><img src="/c/full-screen-slider/img-3.jpg" alt="" /></div>
  <div class="slide" id="slide-4"><img src="/c/full-screen-slider/img-4.jpg" alt="" /></div>
  <div class="slide" id="slide-5"><img src="/c/full-screen-slider/img-5.jpg" alt="" /></div>
  <div style="height: 400vh"></div>          <!-- inert spacer, clipped by body overflow:hidden; has no visual effect -->
</div>

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

The five `.slide` divs are **absolutely-positioned and stacked**; DOM order = z-order (slide-5 paints on top of slide-1), which is exactly what the reveal relies on — no z-index juggling. `document.querySelectorAll(".slide")` returns **5** elements (the trailing spacer has no `.slide` class), so slide indices run 0–4.

## Styling
Global reset: `* { margin:0; padding:0; box-sizing:border-box; }`.
`html, body { height:100%; overflow:hidden; font-family:"Neue Montreal"; }` — **`overflow:hidden` on the whole page** (there is no native scroll; the wheel event does everything).

**Font:** `"Neue Montreal"` — a neutral grotesque sans (substitute: Inter / Neue Haas Grotesk / any clean grotesque).

**The slider stage:**
- `.slider { position:relative; width:100vw; height:100vh; }`.
- `.slide { position:absolute; bottom:0; left:0; width:100%; height:100%; overflow:hidden; clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0 100%); }` — the default clip-path is a **zero-height sliver pinned to the bottom edge** (all four vertices at `y:100%`), so a slide is fully hidden until revealed.
- `#slide-1 { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); }` — the **first slide overrides the default and is fully visible on load** (a full rectangle).
- `img { width:100%; height:100%; object-fit:cover; }` — each photo fills its slide and is hard-cropped.

**The scrim + text overlay:**
- `.slider-content { position:absolute; top:0; left:0; width:100vw; height:100vh; z-index:10000; background:rgba(0,0,0,0.5); }` — a **50% black scrim over the whole viewport**, above every slide, that all the white text sits on.
- The three read-outs are absolutely positioned, all vertically centered at `top:55%` with `transform:translate(-50%,-50%)`:
  - `.slide-number { left:10%; display:flex; gap:0.25em; }`  (`.postfix span { padding:0 0.25em; }`)
  - `.slide-name   { left:30%; }`
  - `.slide-year   { right:20%; }`
- **Shared read-out styling — this is the rolodex mask:**
  `.slide-number, .slide-name, .slide-year { font-size:18px; color:#fff; line-height:30px; clip-path: polygon(0 0, 100% 0, 100% 30px, 0 30px); text-transform:uppercase; }`.
  The `clip-path` crops each read-out to a **30px-tall window** (matching `line-height:30px`), so only **one row** of its column is visible at a time.
- `.prefix, .names, .years { position:relative; top:0; }` — these are the **inner columns of stacked 30px rows** that GSAP translates on `y`; moving one up by 30px swaps which row shows through the window.

**Responsive (`max-width: 900px`):** `.slide-name { left:50%; }` and `.slide-year { right:10%; }` (the labels re-space on narrow screens).

## GSAP effect (be exact)

### State (module-level, set once on DOMContentLoaded)
```js
const slides = document.querySelectorAll(".slide");   // 5 slides, indices 0..4
let currentSlideIndex = 0;
let isAnimating = false;
let currentTopValue = 0;                               // cumulative px offset for the text columns

const elements = [
  { selector: ".prefix", delay: 0 },
  { selector: ".names",  delay: 0.15 },
  { selector: ".years",  delay: 0.3 },
];
```

### Initial set (before any interaction)
For **every slide except index 0**, pre-position its image zoomed and pushed down:
```js
slides.forEach((slide, idx) => {
  if (idx !== 0) gsap.set(slide.querySelector("img"), { scale: 2, top: "4em" });
});
```
Slide 0's image is left untouched (`scale:1`, `top:0`) so the first slide reads normally on load.

### `showSlide(index)` — reveal the next slide (wheel down)
Guarded by the lock: `if (isAnimating) return; isAnimating = true;`. Then:
```js
const slide = slides[index];
const img   = slide.querySelector("img");

currentTopValue -= 30;                                  // roll all text columns UP one 30px row

// 1) the three text columns roll up, staggered by their per-column delay
elements.forEach((elem) => {
  gsap.to(document.querySelector(elem.selector), {
    y: `${currentTopValue}px`,                          // e.g. -30, then -60, -90 … (cumulative)
    duration: 2,
    ease: "power4.inOut",
    delay: elem.delay,                                  // 0, 0.15, 0.3
  });
});

// 2) the incoming image zooms/settles from scale 2 -> 1 and slides top 4em -> 0
gsap.to(img, {
  scale: 1,
  top: "0%",
  duration: 2,
  ease: "power3.inOut",
});

// 3) the slide wipes UP: clip-path grows from the bottom sliver to a full rectangle
gsap.to(slide, {
  clipPath: "polygon(0 0%, 100% 0%, 100% 100%, 0 100%)",
  duration: 2,
  ease: "power4.inOut",
  onComplete: () => { isAnimating = false; },
});
```
Notes:
- The clip-path tween takes the two **top vertices from `y:100%` up to `y:0%`**, so the rectangle **grows upward from the bottom edge** — an upward wipe that uncovers the new slide over the one beneath it.
- All three tweens start together (same 2s duration). In the original the clip-path tween is written with a trailing `"<"` position argument, but `gsap.to` ignores a third argument outside a timeline, so it is a **no-op** — treat these as three concurrent tweens, not a sequenced timeline.
- The image and clip-path use different eases on purpose: **image `power3.inOut`**, **text + clip-path `power4.inOut`**.

### `hideSlide(index)` — collapse the current slide (wheel up)
The exact reverse, on the slide being left behind:
```js
currentTopValue += 30;                                  // roll text columns back DOWN one row
elements.forEach((elem) => {
  gsap.to(document.querySelector(elem.selector), {
    y: `${currentTopValue}px`, duration: 2, ease: "power4.inOut", delay: elem.delay,
  });
});

// slide clip-path collapses back to the bottom sliver (downward wipe out)
gsap.to(slide, {
  clipPath: "polygon(0 100%, 100% 100%, 100% 100%, 0 100%)",
  duration: 2, ease: "power4.inOut",
});

// image zooms back out: scale 1 -> 2, top 0 -> 4em
gsap.to(img, { scale: 2, top: "4em", duration: 2, ease: "power3.inOut" });

// (original fires the same clip-path collapse a second time carrying the onComplete lock reset —
//  redundant but harmless; you only need one clip-path tween whose onComplete does isAnimating=false)
```

### Wheel driver
```js
window.addEventListener("wheel", (e) => {
  if (isAnimating) return;                              // ignore input during the 2s transition
  if (e.deltaY > 0 && currentSlideIndex < slides.length - 1) {
    showSlide(currentSlideIndex + 1);                   // scroll DOWN -> reveal next
    currentSlideIndex++;
  } else if (e.deltaY < 0 && currentSlideIndex > 0) {
    hideSlide(currentSlideIndex);                       // scroll UP -> hide current
    currentSlideIndex--;
  }
});
```
- **`isAnimating` lock**: one wheel notch = one complete 2s transition; every wheel event that arrives mid-transition is dropped. There is no queue and no momentum — it advances exactly one slide per accepted notch.
- Bounds are clamped: you cannot go past slide 5 (index 4) or before slide 1 (index 0).
- Because `currentTopValue` is cumulative and shared by all three columns, the number/name/year windows always show the row matching the current slide (row 1 at index 0, row 2 at index 1, …), staying in lockstep with the images.

## Assets / images
**5 full-bleed background photographs**, one per slide, each filling the entire viewport (`100vw × 100vh`, `object-fit: cover`, so a **landscape ~16:9 source** frames best; crops are expected). Style them as a **cohesive, cinematic, editorial set of dimly-lit interior scenes** — atmospheric gallery/museum-hall ambiences with figures, warm-to-neutral tones — so the 50% black scrim and small white type read clearly over every one. Incoming slides (2–5) are shown pre-zoomed at `scale:2` and settle to `scale:1`, so keep important subject matter away from the extreme edges. No brand marks or logos in the images. By role:
1. **img-1** (slide 1, visible on load): a quiet, dim interior scene — two small figures dwarfed by large blank/pale wall panels in a shadowy hall.
2. **img-2** (slide 2): visitors seated and standing before a large dark-walled framed picture.
3. **img-3** (slide 3): an ornate neoclassical room with two framed portraits flanking a doorway.
4. **img-4** (slide 4): motion-blurred figures walking past framed pictures in a warm-toned hall.
5. **img-5** (slide 5): a wide skylit corridor lined with large pictures and benches.

## Behavior notes
- **Wheel-only, no scroll, no autoplay, no loop, no rAF.** Page is `overflow:hidden`; the only interaction is the wheel. The trailing `400vh` spacer div is inert (clipped away) — do not rely on it for scrolling.
- **Desktop-first** — this is a `wheel`-driven interaction with no touch fallback, so it is effectively desktop-only.
- **Reduced-motion:** nothing animates until the user scrolls; if you add a `prefers-reduced-motion` path, snap `clip-path`/`scale`/`y` instantly (duration 0) instead of the 2s tweens rather than removing the slide change.
- Keep the three eases and the 2s duration exactly (`power4.inOut` for text + clip-path, `power3.inOut` for the image) and the per-column delays (`0 / 0.15 / 0.3`) — the slight offset between the number, name, and year columns is the signature texture of the effect.
