# Image Reveal On Scroll — Clip-Path Unmask Gallery + Per-Item Background Color

## Goal
Build a single full-page vertical-scroll editorial gallery of 8 tall portrait images arranged in a loosely staggered column (each image nudged to a different horizontal position so the eye zig-zags down the page). As each image scrolls into view, GSAP + ScrollTrigger unmask it top-to-bottom by morphing a `clip-path` polygon from a zero-height sliver at the top edge into the full rectangle (a curtain-drop reveal, `power1.out`, 2s). Simultaneously, entering an item tweens a fixed full-viewport background gradient to that item's own accent color, so the whole page's mood shifts color as you scroll. A fixed counter in the top-left corner reads out scroll progress as a whole-number percentage.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the single GSAP plugin **`ScrollTrigger`**. Register with `gsap.registerPlugin(ScrollTrigger)`. No smooth-scroll library (native scroll). No SplitText, no other plugins.

## Layout / HTML
Class names are load-bearing — the JS and CSS query them.

```
<div class="counter"><p>0</p></div>
<div class="bg-color"></div>

<div class="items">
  <!-- repeat this block 8 times -->
  <div class="item">
    <div class="item-img">
      <img src="/path/to/1.jpg" alt="" />
    </div>
    <div class="item-info">
      <p>0.451 29 Neo-Tokyo Streetwear Ensemble</p>
    </div>
  </div>
  ...
</div>
```

- Exactly **8** `.item` blocks inside `.items`, each with an `.item-img` wrapper holding one `<img>` and an `.item-info` caption below it.
- `.counter > p` starts as the text `0`. `.bg-color` is an empty fixed layer sitting behind everything.
- Item captions (small mono-ish labels, format `<decimal> <int> <Title>`) in order 1→8:
  1. `0.451 29 Neo-Tokyo Streetwear Ensemble`
  2. `0.312 47 Cybernetic Space Suit Design`
  3. `0.678 15 Martian Explorer Gear Concept`
  4. `0.289 51 Virtual Reality Avant-Garde Apparel`
  5. `0.563 34 Post-Apocalyptic Survival Outfit`
  6. `0.409 22 Interstellar Pilot Uniform Draft`
  7. `0.522 40 Underwater City Diver's Attire`
  8. `0.601 18 Artificial Intelligence Inspired Fashion`

## Styling
Font family: **"Neue Montreal"** (a neutral grotesque). There is no `@font-face` in the source — it's just named on `.counter` and `.items`, so it falls back to the system sans if unavailable. That's fine; don't add a webfont.

Global:
- `* { margin:0; padding:0; box-sizing:border-box }`.
- `img { width:100%; height:100%; object-fit:cover }`.

Fixed layers:
- `.counter { position:fixed; top:0; left:0; padding:2em; color:#000; font-family:"Neue Montreal" }`. It holds the scroll-percentage number and stays pinned to the top-left corner over everything.
- `.bg-color { position:fixed; width:100vw; height:100vh; z-index:-1 }` with initial `background: linear-gradient(0deg, rgba(250,186,74,1) 0%, rgba(252,176,69,0) 100%)` — an amber (`#faba4a`) glow rising from the bottom edge and fading to transparent at the top. Because it's `z-index:-1`, it sits behind the page content (the page itself has no background, so this gradient is the visible backdrop).

Gallery:
- `.items { width:100%; height:100%; padding:4em 2em; font-family:"Neue Montreal" }`.
- `.item { width:40%; margin-bottom:4em }` — each item occupies 40% of the page width; the 4em bottom margin stacks them vertically with air between.
- `.item-info { padding:0.5em 0 }` — the caption sits just under the image.
- `.item-img { overflow:hidden }` — **critical**: this clips the scaled image so the clip-path reveal reads cleanly and no overflow shows.
- `.item-img img { will-change:transform; transform:scale(1.25); clip-path: polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%) }` — the image is permanently zoomed to 1.25 (the scale is **never animated**, it just guarantees a full crop with no edge gaps), and starts fully masked by a zero-height clip polygon pinned to the top edge (all four points on the top line).

Per-item accent background (set on each `.item-img` via `nth-child`, matching the JS color array index by index):
- `.item:nth-child(1) .item-img { background:#faba4a }`
- `.item:nth-child(2) .item-img { background:#bb2a26 }`
- `.item:nth-child(3) .item-img { background:#7e7d65 }`
- `.item:nth-child(4) .item-img { background:#989682 }`
- `.item:nth-child(5) .item-img { background:#5e4036 }`
- `.item:nth-child(6) .item-img { background:#e33b12 }`
- `.item:nth-child(7) .item-img { background:#252d1a }`
- `.item:nth-child(8) .item-img { background:#b04c0d }`

Per-item horizontal stagger (each item `position:relative` with a `left` offset, so the column weaves side to side):
- `.item:nth-child(1), .item:nth-child(5), .item:nth-child(8) { left:5% }`
- `.item:nth-child(3), .item:nth-child(7) { left:20% }`
- `.item:nth-child(2), .item:nth-child(4), .item:nth-child(6) { left:50% }`

## GSAP effect (be exact)

Two independent pieces of JS: (A) the per-item scroll-reveal + background tween, (B) the scroll-percentage counter.

### A. Per-item clip-path reveal + background color tween

Define the accent color array (index-aligned with the CSS `nth-child` backgrounds above):
```js
const bgColors = ["#faba4a","#bb2a26","#7e7d65","#989682","#5e4036","#e33b12","#252d1a","#b04c0d"];
const bgColorElement = document.querySelector(".bg-color");
```

Loop every `.item` with `gsap.utils.toArray(".item").forEach((item, index) => { ... })`. Inside, grab `const img = item.querySelector(".item-img img")` and create ONE `gsap.fromTo` per image:

```js
gsap.fromTo(
  img,
  { clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)" },
  {
    clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
    ease: "power1.out",
    duration: 2,
    scrollTrigger: {
      trigger: item,
      start: "center bottom",
      end: "bottom top",
      toggleActions: "play none none none",
      onEnter: () => updateBackground(bgColors[index]),
      onEnterBack: () => updateBackground(bgColors[index]),
    },
  }
);
```

Exact semantics to preserve:
- **Animated property:** only `clipPath`, from `polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)` (zero-height sliver at top — image hidden) → `polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)` (full rectangle — image fully shown). The bottom two points travel from `y=0%` down to `y=100%`, so the image unmasks like a curtain dropping from the top edge to the bottom. Both polygons have 4 points so the morph interpolates cleanly.
- **`ease: "power1.out"`, `duration: 2`.** This is a **time-based tween triggered by scroll, NOT a scrubbed tween** — there is no `scrub`. When the trigger fires, the 2s reveal plays on its own timeline regardless of scroll speed.
- **ScrollTrigger:** `trigger: item`; `start: "center bottom"` (fires when the item's vertical center reaches the viewport bottom); `end: "bottom top"`; `toggleActions: "play none none none"` (play once on enter; do nothing on leave / enter-back / leave-back — so once revealed it stays revealed and never reverses).
- **Background callbacks:** `onEnter` and `onEnterBack` both call `updateBackground(bgColors[index])`. So scrolling down into an item, or scrolling back up into it, both retint the page to that item's accent.

Background tween helper:
```js
function updateBackground(color) {
  gsap.to(bgColorElement, {
    background: `linear-gradient(0deg, ${color} 0%, rgba(252,176,69,0) 100%)`,
    duration: 2,
    ease: "power1.out",
  });
}
```
It tweens `.bg-color`'s `background` to a fresh vertical gradient: the current item's accent solid at the bottom (`0%`) fading to transparent amber `rgba(252,176,69,0)` at the top (`100%`). `duration: 2`, `ease: "power1.out"`, so the color cross-fades smoothly as you move between items. (GSAP animates the gradient via its CSS plugin string interpolation.)

### B. Scroll-percentage counter

Inside a `DOMContentLoaded` handler:
```js
const counterElement = document.querySelector(".counter p");
const docHeight = document.documentElement.scrollHeight - window.innerHeight;

function updateScrollPercentage() {
  const scrollPosition = window.scrollY;
  const scrolledPercentage = Math.round((scrollPosition / docHeight) * 100);
  counterElement.textContent = `${scrolledPercentage}`;
}
window.addEventListener("scroll", updateScrollPercentage);
```
`docHeight` (total scrollable distance) is measured **once** on DOMContentLoaded; on every native `scroll` event the counter text is set to `round(scrollY / docHeight * 100)` — a plain integer, no `%` sign. It reads `0` at the top and `100` at the very bottom.

## Assets / images
**8 tall portrait images, 2:3 aspect ratio** (source is 896×1344), one per `.item`. They should read as a matched editorial set: stylized fashion / concept portraits — a single figure per frame, dramatic art-directed lighting, each on a bold saturated color field, in a futuristic sci-fi-couture register (streetwear, spacesuit, explorer gear, VR apparel, survival outfit, pilot uniform, diver's attire, AI-inspired look). High contrast, punchy color per image. `object-fit: cover` inside the `.item-img` (which is 40% page width) with a permanent `scale(1.25)` zoom, so give margin around the subject. No brand marks, no logos.

## Behavior notes
- **Load state:** every image starts fully masked (zero-height clip) and reveals only when its ScrollTrigger fires; the first item(s) near the top reveal almost immediately since their center is already near the viewport bottom.
- **Not scrubbed / not reversible:** `toggleActions: "play none none none"` means the reveal plays once and holds — scrolling back up does not re-hide an image (but the background still retints via `onEnterBack`).
- **`overflow:hidden` on `.item-img`** and **`will-change:transform`** on the img are required for a clean clip reveal with the 1.25 zoom.
- **Counter caveat (keep as-is for fidelity):** `docHeight` is computed once at `DOMContentLoaded`; if images load after that and change page height, the percentage denominator won't update — this matches the original.
- Light, mobile-safe, no WebGL/canvas. The layout is percentage-based; on narrow screens the 40%-wide items and `left` offsets still apply (no dedicated media query in the source).
