# Mouse Trail — layered rainbow SVG ribbon that snakes after the cursor

## Goal
Build a full-viewport, playful **mouse-trail** effect. Seven stacked SVG polyline
paths in a rainbow palette chase the cursor across a bright amber background. They
read as one thick, glossy ribbon that whips and snakes behind the pointer: each
colored layer has a slightly different length, so the tails fan out and reveal the
colors underneath as the ribbon curls. The star effect is the eased, interpolated
lag of the trail as it catches up to a fast-moving cursor.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) — core only. **No
GSAP plugins, no ScrollTrigger, no lenis, no canvas, no WebGL.** The trail geometry
is drawn by hand into SVG `<path>` elements from inside a `requestAnimationFrame`
loop; `gsap` is used only to ease a single shared pointer object toward the mouse.

## Layout / HTML
`<body>` contains **seven sibling `<svg>` elements**, each holding exactly one
`<path class="trail">`. There is no wrapper, no viewBox, no width/height attributes
on the SVGs (they are sized to 100% via CSS). Each path starts with an empty `d=""`
and carries its stroke color as an inline style. Order matters — later SVGs paint on
top. From first (bottom layer) to last (top layer), the inline `stroke` colors are:

1. `#fefa02` (yellow)
2. `#9aa8e1` (periwinkle)
3. `#f57faa` (pink)
4. `#fb2832` (red-orange)
5. `#559ce2` (blue)
6. `#e27b63` (terracotta)
7. `#e82c31` (crimson)

```html
<svg><path d="" class="trail" style="stroke: #fefa02"></path></svg>
<svg><path d="" class="trail" style="stroke: #9aa8e1"></path></svg>
<svg><path d="" class="trail" style="stroke: #f57faa"></path></svg>
<svg><path d="" class="trail" style="stroke: #fb2832"></path></svg>
<svg><path d="" class="trail" style="stroke: #559ce2"></path></svg>
<svg><path d="" class="trail" style="stroke: #e27b63"></path></svg>
<svg><path d="" class="trail" style="stroke: #e82c31"></path></svg>
<script type="module" src="./script.js"></script>
```

## Styling
- Global reset: `* { margin:0; padding:0; box-sizing:border-box; }`.
- `body { background: #ffb503; }` — a saturated amber/marigold. No scrollbars needed;
  the SVGs are `position: absolute` so the page does not grow.
- Every `svg`: `position:absolute; top:0; left:0; width:100%; height:100%;
  pointer-events:none;`. All seven overlap perfectly, stacked in DOM order.
- Every `path`: `fill:none; stroke-width:50; stroke-linecap:round;
  stroke-linejoin:round;`. The fat 50px stroke plus round caps/joins is what turns a
  jagged polyline into a smooth, rounded ribbon. Stroke color comes from the inline
  `style` on each path (do **not** set stroke in CSS).

## GSAP effect (this is the important part — be exact)

### The one shared, eased pointer
There is a single plain object `smoothPointer = { x, y }` initialized to the viewport
center: `x = window.innerWidth / 2`, `y = window.innerHeight / 2`. **All seven paths
read from this same object** — there is only one eased pointer for the whole system.

On `window` `mousemove`, tween that object toward the raw cursor:

```js
window.addEventListener("mousemove", (event) => {
  gsap.to(smoothPointer, {
    x: event.clientX,
    y: event.clientY,
    duration: 0.5,
    ease: "power2.out",
  });
});
```

- Animated properties: `x`, `y` of `smoothPointer` → the mouse's `clientX`/`clientY`.
- `duration: 0.5`, `ease: "power2.out"`, no delay, no stagger.
- Each new mousemove fires a fresh `gsap.to`; GSAP overwrites the in-flight tween, so
  the pointer continuously eases toward the latest cursor position. This 0.5s eased
  lag is the source of the whip/snake feel — the trail never snaps instantly.

### Per-path point history (defines each tail's length)
Keep an array of maximum point counts, one per path, **descending**:

```js
const totalPointsArray = [40, 35, 30, 25, 20, 15, 10];
```

The bottom layer (`#fefa02`) keeps 40 points → longest tail; the top layer
(`#e82c31`) keeps 10 → shortest tail. Because shorter tails are painted last, they
sit on top, so as the ribbon curls you see the longer under-layers fan out beyond the
shorter over-layers, exposing the rainbow.

### The requestAnimationFrame draw loop
Run a continuous `requestAnimationFrame(updatePath)` loop (started once, self-
recursing). It never stops. **Every frame**, for each path (via `forEach` with
`index`):

1. Read the path's own history array (store it as a custom property on the element,
   e.g. `path.points`, defaulting to `[]`).
2. `points.unshift({ ...smoothPointer })` — prepend a **copy** of the current shared
   pointer to the FRONT of the array (index 0 = newest / cursor end).
3. Trim from the tail: `while (points.length > totalPointsArray[index]) points.pop();`
   so each array holds at most its configured number of the most-recent pointer
   positions.
4. Save it back (`path.points = points`).
5. If `points.length > 1`, rebuild the SVG path data as a straight polyline through
   the history and `path.setAttribute("d", d)`:
   ```js
   let d = `M ${points[0].x} ${points[0].y}`;
   for (let i = 1; i < points.length; i++) d += ` L ${points[i].x} ${points[i].y}`;
   ```
   Just `M` (moveTo) then `L` (lineTo) segments — no curves, no smoothing. The 50px
   round stroke does all the visual smoothing.

Because every path samples the **same** eased pointer each frame but retains a
different number of past samples, the seven layers trace the identical route and
differ only in how far back in time their tails reach — producing the nested,
staggered, multi-color ribbon. When the cursor is still, all points collapse onto one
spot and the ribbon shrinks to a stack of round dots; when it moves fast, the tails
stretch out into long colored streaks.

## Assets / images
None. Pure vector + color; no external images, icons, fonts, or textures.

## Behavior notes
- **Desktop / pointer-driven only.** The effect is entirely `mousemove`-based; on
  touch devices with no cursor there is nothing to drive it (it simply rests at
  center). No hover, click, or scroll triggers.
- Initial state: pointer at center, all `d` empty, so on load the screen is just the
  amber background until the first mouse movement seeds the histories.
- The rAF loop runs forever (no reduced-motion guard in the original). If you add one,
  gate the loop and the mousemove tween behind
  `matchMedia("(prefers-reduced-motion: reduce)")`.
- Everything is fixed to the viewport; the page does not scroll and the SVGs always
  fill 100% × 100% of the window.
