# Fullscreen Click-Driven Image Slider — Clip-Path Wipe + Image Parallax + CustomEase Counter/Title/Indicators

## Goal

Build a fullscreen (100vw × 100vh) editorial image slider navigated **by clicking the left or right half of the screen** (or a preview thumbnail). On each navigation, the incoming image is revealed by a **`clip-path` wipe** that grows from the edge you came from, its inner `<img>` slides in with a **parallax offset** while the outgoing image slides off the opposite side. In sync, a **vertically sliding numeric counter**, a **vertically sliding title stack**, and **two rotating `+` indicators** all move with a shared CustomEase curve called `"hop"`. There is no scroll and no autoplay — every change is a click.

## Tech

Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the single GSAP plugin **`CustomEase`** (`import { CustomEase } from "gsap/CustomEase"`). No other libraries — **no ScrollTrigger, no SplitText, no Lenis**. Everything runs inside a `DOMContentLoaded` listener; call `gsap.registerPlugin(CustomEase)` first.

## Layout / HTML

```
nav                                  ← fixed top-center
  a#active         "Work"            (id="active")
  a                "About"

.slider                              ← fullscreen viewport, overflow hidden
  .slider-images                     ← absolute layer holding stacked slides
    .img > img                       ← ONE slide hard-coded in HTML (image #1)

  .slider-title                      ← centered, clip-path masked window
    .slider-title-wrapper            ← translated vertically
      p  "The Revival Ensemble"
      p  "Above The Canvas"
      p  "Harmony in Every Note"
      p  "Redefining Imagination"
      p  "From Earth to Expression"

  .slider-counter                    ← bottom-center, clip-path masked window
    .counter                         ← translated vertically (the digit column)
      p "1"  p "2"  p "3"  p "4"  p "5"
    div > p "—"                      ← static em dash separator
    div > p "5"                      ← static total count

  .slider-preview                    ← bottom-right thumbnail strip
    .preview.active > img            (thumb #1, starts active)
    .preview        > img            (thumb #2)
    .preview        > img            (thumb #3)
    .preview        > img            (thumb #4)
    .preview        > img            (thumb #5)

  .slider-indicators                 ← centered, spans 75% width
    p "+"                            (left indicator)
    p "+"                            (right indicator)
```

Notes:
- Only **one** `.img` slide is present in the HTML (image #1). Every subsequent slide is created in JS with `document.createElement` and appended to `.slider-images`.
- All five `.preview` thumbnails point at the five images; the first carries class `active`.
- Titles/labels are neutral fictional strings (art/music editorial tone) — no real brands.

## Styling

- Font: **Inter** (Google Fonts, full optical/weight range). `* { margin:0; padding:0; box-sizing:border-box; user-select:none }`. `html, body { width:100%; height:100% }`.
- `img { width:100%; height:100%; object-fit:cover }`.
- `a, p { text-decoration:none; color:#fff; font-size:14px }` — all text is white (assume dark imagery behind).
- `nav`: `position:fixed; top:0; left:0; width:100vw; padding:2em; display:flex; justify-content:center; gap:2em; z-index:2`. `nav a { opacity:0.5 }`, `nav a#active { opacity:1 }`.
- `.slider`: `position:relative; width:100vw; height:100vh; overflow:hidden`.
- `.slider-images`, `.img`: `position:absolute; width:100%; height:100%` (`.img` also `position:absolute` — all slides stack on top of each other).
- **Title window** — `.slider-title`: `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; height:64px; clip-path:polygon(0 0, 100% 0, 100% 100%, 0% 100%)`. The full-rectangle `clip-path` turns this box into a masked window that hides any title line translated outside it. `.slider-title-wrapper`: `position:relative; width:100%; top:0; text-align:center; will-change:transform`. Its `p`: `font-size:50px; line-height:60px` (so each title occupies a **60px** vertical step).
- **Counter window** — `.slider-counter`: `position:absolute; bottom:2em; left:50%; transform:translateX(-50%); height:24px; display:flex; gap:0.5em; clip-path:polygon(0 0, 100% 0, 100% 100%, 0% 100%)`. `.slider-counter > div { flex:1 }`, `.slider-counter p { line-height:20px }`. `.counter`: `position:relative; top:0; will-change:transform` — the stacked digit column; each digit occupies a **20px** vertical step. The layout reads `N — 5` (current digit, em dash, total).
- **Indicators** — `.slider-indicators`: `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:75%; display:flex; justify-content:space-between`. `.slider-indicators p`: `position:relative; font-size:40px; font-weight:200; will-change:transform` — two thin `+` glyphs pinned to the left/right of a 75%-wide band, centered vertically over the middle of the image.
- **Preview strip** — `.slider-preview`: `position:absolute; bottom:2em; right:2em; width:35%; height:50px; display:flex; gap:1em`. `.preview`: `position:relative; flex:1; cursor:pointer`. Each thumbnail has a dark scrim via `::after` — `content:""; position:absolute; inset:0; width:100%; height:100%; background:rgba(0,0,0,0.5); transition:0.3s ease-in-out`. The active one clears it: `.preview.active::after { background-color:rgba(0,0,0,0) }` (the current slide's thumbnail brightens, the rest stay dimmed — a pure CSS transition, not GSAP).
- Responsive `@media (max-width:900px)`: `.slider-indicators { width:90% }`; `.slider-preview { width:90%; bottom:5em }`; `.slider-title-wrapper p { font-size:30px }` (line-height stays 60px, so the 60px title step is unchanged).

## GSAP effect (exhaustive)

### CustomEase `"hop"` (shared by every tween)

```js
gsap.registerPlugin(CustomEase);
CustomEase.create("hop", "M0,0 C0.071,0.505 0.192,0.726 0.318,0.852 0.45,0.984 0.504,1 1,1");
```

This curve rises steeply and reaches ~1 very early (around x≈0.5) then flattens — a fast, slightly overshoot-free "snap-then-settle" feel. **Every** tween in this component uses `ease: "hop"`.

### State

```js
let currentImg = 1;          // 1-based index of the visible slide
const totalSlides = 5;
let indicatorRotation = 0;   // accumulated degrees for the "+" indicators
```

On load, `currentImg = 1`, so counter/title offsets are 0 and nothing animates until the first click.

### Counter + title translation — `updateCounterAndTitlePosition()`

Called after every navigation. Both columns translate straight up in fixed line-height steps:

```js
const counterY = -20 * (currentImg - 1);   // digit column: 20px per slide
const titleY   = -60 * (currentImg - 1);   // title column: 60px per slide

gsap.to(counter, { y: counterY, duration: 1, ease: "hop" });
gsap.to(titles,  { y: titleY,  duration: 1, ease: "hop" });
```

So slide 3 pulls the digit column to `y:-40` (showing "3") and the title column to `y:-120` (showing the 3rd title). The `clip-path` windows hide the off-window lines. Both tweens: `duration: 1`, `ease: "hop"`.

### Slide transition — `animateSlide(direction)`

`direction` is `"left"` (navigating **backward**, came from the left edge) or `"right"` (navigating **forward**, came from the right edge). Sequence:

1. **Grab the outgoing slide**: the **last** `.img` element currently in `.slider-images` (`document.querySelectorAll(".img")[len-1]`).
2. **Build the incoming slide**: create `div.img` and an `img` whose `src` is `img${currentImg}.jpg` (note: `currentImg` has already been updated by the click handler *before* `animateSlide` runs). Pre-offset the inner image:
   ```js
   gsap.set(slideImgElem, { x: direction === "left" ? -500 : 500 });
   ```
   Then append the `img` into the new `div.img`, and append that `div` into `.slider-images` (it stacks on top).
3. **Slide the outgoing image off** (the inner `<img>` of the previous slide):
   ```js
   gsap.to(currentSlide.querySelector("img"), {
     x: direction === "left" ? 500 : -500,
     duration: 1.5, ease: "hop"
   });
   ```
   Backward nav pushes the old image right (+500px); forward nav pushes it left (−500px).
4. **Clip-path wipe on the incoming slide `div`** — reveals from the edge you came from:
   ```js
   gsap.fromTo(slideImg,
     { clipPath: direction === "left"
         ? "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)"      // zero-width strip pinned to LEFT edge
         : "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)" // zero-width strip pinned to RIGHT edge
     },
     { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", // grows to full rectangle
       duration: 1.5, ease: "hop" }
   );
   ```
   Backward: the reveal window opens from the left edge rightward. Forward: it opens from the right edge leftward.
5. **Parallax the incoming image back to rest**:
   ```js
   gsap.to(slideImgElem, { x: 0, duration: 1.5, ease: "hop" });
   ```
   Its inner image glides from ±500 to 0 *while* the clip-path window expands over it — so the picture appears to drift into place behind an opening curtain (image and mask move at slightly different rates → parallax).
6. **Cleanup** stale slides (see below).
7. **Rotate the indicators** (see below).

All three motion tweens (steps 3–5) share `duration: 1.5, ease: "hop"`; the counter/title/indicator tweens use `duration: 1`.

### Indicators rotation

Inside `animateSlide`, after building the slide:

```js
indicatorRotation += direction === "left" ? -90 : 90;
gsap.to(indicators, { rotate: indicatorRotation, duration: 1, ease: "hop" });
```

Both `+` glyphs rotate together, accumulating **±90°** per navigation (forward = +90, backward = −90). A `+` rotated 90° looks like an `×`, so they flip between `+` and `×` on each step. `indicatorRotation` is never reset, so it keeps winding.

### Cleanup — `cleanupSlides()`

Because each transition appends a new `.img` and never deletes the old one during the tween, cap the stack:

```js
const imgElements = document.querySelectorAll(".slider-images .img");
if (imgElements.length > totalSlides) imgElements[0].remove(); // drop the oldest
```

This keeps at most `totalSlides` (5) stacked slides so the DOM doesn't grow unbounded. (No `onComplete` removal — the outgoing image just slides off and is eventually pruned once the stack exceeds 5.)

### Active thumbnail — `updateActiveSlidePreview()`

```js
prevSlides.forEach(p => p.classList.remove("active"));
prevSlides[currentImg - 1].classList.add("active");
```

Toggling `.active` lets the CSS `::after` scrim transition (0.3s) brighten the current thumbnail.

### Click routing (the only trigger) — a single listener on `document`

```js
document.addEventListener("click", (event) => {
  const sliderWidth = document.querySelector(".slider").clientWidth;
  const clickPosition = event.clientX;

  // 1) Clicked a preview thumbnail → jump directly to it
  if (slidePreview.contains(event.target)) {
    const clickedPrev = event.target.closest(".preview");
    if (clickedPrev) {
      const clickedIndex = Array.from(prevSlides).indexOf(clickedPrev) + 1;
      if (clickedIndex !== currentImg) {
        if (clickedIndex < currentImg) { currentImg = clickedIndex; animateSlide("left"); }
        else                           { currentImg = clickedIndex; animateSlide("right"); }
        updateActiveSlidePreview();
        updateCounterAndTitlePosition();
      }
    }
    return; // don't also run the left/right-half logic
  }

  // 2) Clicked left half → go back; right half → go forward (clamped at ends)
  if (clickPosition < sliderWidth / 2 && currentImg !== 1) {
    currentImg--; animateSlide("left");
  } else if (clickPosition > sliderWidth / 2 && currentImg !== totalSlides) {
    currentImg++; animateSlide("right");
  }
  updateActiveSlidePreview();
  updateCounterAndTitlePosition();
});
```

Key points:
- The listener is on `document`, so **anywhere** on the left half navigates backward and anywhere on the right half navigates forward (except clicks inside the preview strip, which jump directly).
- Navigation is **clamped**: at slide 1 a left-half click does nothing; at slide 5 a right-half click does nothing (no wrap-around).
- `currentImg` is updated **before** `animateSlide` so the new image src and the counter/title offsets use the destination index. A thumbnail jump uses `"left"`/`"right"` based on whether the target index is lower/higher than the current one (a multi-step jump still plays a single wipe).

## Assets / images

**5 full-bleed editorial art images**, each used both as a fullscreen slide (`object-fit: cover`, 100vw × 100vh) and as a small thumbnail in the bottom-right strip. Prefer **landscape ~3:2 or 16:9** so they fill the viewport cleanly; give each a **distinct dominant color/subject** so the slide change is obvious. Moody, gallery/editorial mood matching titles like "The Revival Ensemble", "Above The Canvas", "Harmony in Every Note", "Redefining Imagination", "From Earth to Expression" — e.g. abstract painterly artwork, sculptural forms, musical/performance imagery. No logos or brand marks. File naming `img1.jpg … img5.jpg`.

## Behavior notes

- **Click-only**: no scroll hijack, no autoplay, no keyboard. The whole page is exactly one viewport tall.
- **No wrap**: navigation clamps at slides 1 and 5.
- Re-clicks during an in-flight tween are **not** guarded — GSAP just retargets/overlaps tweens (rapid clicks can stack transitions). Keep this behavior; there is no `isAnimating` lock in the original.
- No `prefers-reduced-motion` branch and no desktop-only gate; the responsive breakpoint at 900px only widens the preview/indicators and shrinks the title font.
- The counter and title rely on their `clip-path` rectangles to mask everything outside the current line — keep those `clip-path` windows or the whole stacked column will show.
