# Split Column Infinite Slider — Virtual Wheel Scroll, Opposite Clip-Path Reveals & Split Titles

## Goal
Build a fullscreen two-column project slider driven by a **virtual scroll** (wheel or touch drag, lerped). Each project is a **pair of photos**: the **left** one is revealed by a `clip-path` **rising from the bottom**, the **right** one by a `clip-path` **descending from the top**, while their images **drift vertically in opposite directions** (with a fixed zoom to hide the drift) and a single **100vw-wide text block is split across both columns** — half the title in each — that holds still while its slide is centred and drifts a little on the way in and out. Slides are created and removed around the current index with **modulo wrapping**, so the loop is infinite in both directions. A thin **light bar** on the right shows progress through the set. No GSAP: per-frame arithmetic.

## Tech
Vanilla HTML/CSS/JS, one module. No libraries. Font: **Inter** (500–800).

## Layout / HTML
```html
<div class="sc">
  <section class="sc-slider" aria-label="Projects">
    <div class="sc-column sc-column--left"></div>
    <div class="sc-column sc-column--right"></div>
    <div class="sc-progress" aria-hidden="true"><span class="sc-progress__bar"></span></div>
  </section>
</div>
```
Each slide created by JS:
```html
<div class="sc-slide" style="z-index:…">
  <img src="{side}_{n}.jpg" alt="" />
  <div class="sc-slide__shade"></div>
  <div class="sc-copy">
    <ul class="sc-copy__tags"><li>…</li><li>…</li><li>…</li></ul>
    <h2 class="sc-copy__title">Northline</h2>
    <a class="sc-copy__link" href="#">View project</a>
  </div>
</div>
```
Data (5 projects): Northline · Tidewater · Dune Route · Night Field · Concrete Hours, each with three short tags.

## Styling
`:root`: `--sc-bg:#0a0b0d`, `--sc-ink:#f6f5f2`, `--sc-line:rgba(255,255,255,.22)`, `--sc-shade:.45`.

- `.sc-slider` — `position:relative; width:100%; height:100svh; display:flex; overflow:hidden`. `.sc-column` — `flex:1; position:relative; height:100%; overflow:hidden`.
- `.sc-slide` — `position:absolute; inset:0; overflow:hidden`; `img` `object-fit:cover; will-change:transform`. `.sc-slide__shade` — `position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,0,0,calc(var(--sc-shade)*.6)), rgba(0,0,0,var(--sc-shade)) 60%, rgba(0,0,0,calc(var(--sc-shade)*1.3)))`.
- `.sc-copy` — `position:absolute; top:0; width:100vw; height:100%; pointer-events:none`; left column `left:0`, right column `right:0` — that is the whole trick: each column shows its half of the same 100vw block.
- `.sc-copy__title` — centred (`top:50%; left:50%; translate(-50%,-50%)`), uppercase, `clamp(2.5rem,10vw,15rem)`, weight 800, `letter-spacing:-.03em; line-height:1; white-space:nowrap`.
- `.sc-copy__tags` — at `top:31%`, centred, `display:flex; gap:.5rem`; each `li` is a glass chip (`padding:.45em .9em; border-radius:999px; border:1px solid var(--sc-line); background:rgba(255,255,255,.08); backdrop-filter:blur(12px); font-size:.72rem; weight 500; letter-spacing:.06em; uppercase`).
- `.sc-copy__link` — at `top:64%`, centred, same glass pill at `rgba(255,255,255,.1)`, weight 600, `pointer-events:auto`, hover `rgba(255,255,255,.2)`.
- `.sc-progress` — `position:absolute; right:1.25rem; top:50%; translateY(-50%); width:2px; height:120px; background:rgba(255,255,255,.15); overflow:hidden; z-index:10`; `__bar` fills it with a white→blue→pink gradient, `transform-origin:top; transform:scaleY(0)`.
- `@media (max-width:1000px)`: tags stack vertically at `.62rem`.

## Effect (be exact)
Build in `mount(config)` → `destroy()`.

```js
const DEFAULTS = { scrollSensitivity: 1200, smoothness: .05, imageShift: 25, copyShift: 15, titleHold: .1, imageZoom: 1.25, revealOverlap: .5, bufferSlides: 3 };
```
State: `position = 1`, `target = 1`, `columns = { left: { el, live: Map }, right: { el, live: Map } }`.

- **Input**: wheel (passive) `target += deltaY / scrollSensitivity`; touch `target += (lastY − y) * 8 / scrollSensitivity`.
- **create(side, index)**: `n = ((index % 5) + 5) % 5`; build the slide with `img src = ${side}_${n+1}.jpg`, `z-index = index + 1000` (later slides stack on top), append, store `{ el, img, copy }` in `live`.
- **clip(side, reveal)**: `d = clamp(reveal, 0, 1) * (100 + revealOverlap)`; left → `polygon(0% ${100−d}%, 100% ${100−d}%, 100% 100%, 0% 100%)`; right → `polygon(0% 0%, 100% 0%, 100% ${d}%, 0% ${d}%)`.
- **titlePos(progress)**: `from = progress − 1`; `past = |from| − titleHold`; if `past ≤ 0` return 1; `t = past / (1 − titleHold)`; return `1 + sign(from) * t*t*(3 − 2t)` (smoothstep away from the plateau).
- **update()**: `first = floor(position) − bufferSlides`, `last = floor(position) + bufferSlides + 1`; for each side (`dir = left ? 1 : −1`): create missing indices in `[first, last]`, remove the rest; for each live slide `reveal = position − index`, `progress = clamp(reveal, 0, 2)`; set `clipPath = clip(side, reveal)`; `img.transform = translateY(${(1 − progress) * imageShift * dir}%) scale(${imageZoom})`; `copy.transform = translateY(${(1 − titlePos(progress)) * copyShift * dir}%)`. Finally `bar.transform = scaleY(frac)` with `frac = ((position mod 5) + 5) mod 5 / 5`.
- **Loop** (rAF): `position += (target − position) * smoothness`; `update()`.

**destroy()** — cancel rAF, remove listeners, remove all live slides, reset the bar.

## Assets / images
Ten large photos (≥1200px tall — each fills half the screen at full height), five matched pairs with a shared mood per project (e.g. mountains/cliffs, sea/ice, road/dunes, milky way/aurora, two buildings). Files `left_{1–5}.jpg`, `right_{1–5}.jpg`.

## Behavior notes
- The section captures the wheel for its own virtual scroll; use it as a standalone screen, not inside a normally scrolling page.
- Because the copy block is `100vw` and clipped by each `50vw` column, the title is literally cut in half and the two halves move with different slides during a transition — that is the signature.
- Touch works (drag = scroll); there is no pagination.
- Only the prefixed variables are global.

## Images

This component ships with 10 reference assets, served publicly.
Use them as-is to reproduce the demo faithfully, then swap in your own — the layout expects the
same aspect ratios.

```
https://motionprompts.dev/c/split-column-infinite-slider/left_1.jpg
https://motionprompts.dev/c/split-column-infinite-slider/left_2.jpg
https://motionprompts.dev/c/split-column-infinite-slider/left_3.jpg
https://motionprompts.dev/c/split-column-infinite-slider/left_4.jpg
https://motionprompts.dev/c/split-column-infinite-slider/left_5.jpg
https://motionprompts.dev/c/split-column-infinite-slider/right_1.jpg
… 4 more under https://motionprompts.dev/c/split-column-infinite-slider/
```

They are hotlinkable for prototyping. For anything you ship, replace them: they are licensed for
demonstration of this component, not for redistribution.

## Using this outside its demo page

This component is written as a complete page — that is how the demo is meant to look. If you are dropping it into an existing project, or combining it with other components, these are the things it declares at document level and that you need to move or reconcile first.

- **Palette on `:root`** — `--sc-bg`, `--sc-ink`, `--sc-line`, `--sc-shade`. These names are not namespaced and they collide: `--ink` is defined by 164 of the 219 components in this catalogue, `--paper` by 94, `--muted` by 80, each with different values — and they will also collide with whatever your own project defines. Move them onto the component's wrapper (`.my-section { --ink: … }`) or rename them with a prefix.
- **Rules on `html, body`** — the demo owns the whole document, so these set the page background, typography and resets. Dropped into an existing project they restyle the entire page, not just this section. Re-target them at the component's wrapper before using it.
