Creative Technology Lab
Home ↗ Contact Instagram YouTube
Live App

Image Snake

A pointer-driven photo cascade where images slither across the canvas, following your cursor like a living chain. Zero dependencies.

Vanilla JS Pointer Events 60fps Animation requestAnimationFrame Zero Dependencies
Year 2026
Type Web App
Status ● Live
Image Snake / Pointer-Driven Photo Cascade HAS Studio — 2026
001 Overview
Your cursor becomes the head of a living chain — 30 photo slots slithering behind it in real time, each frame recycling from front to back so your entire library loops endlessly. Images follow a bell-curve of size — small at the head, peaking mid-chain, tapering at the tail — giving the stream natural depth and a focal point that moves with you.
002 How It Works
01
Trail Recording
Every frame, the cursor position is pushed to a 3,000-point trail array. This historical path gives the snake its organic, curving shape — every twist of your wrist is preserved.
02
Position Solver
Each image walks the trail segment-by-segment, accumulating distance until it finds its target position. The angle at each point drives the rotation for that organic slither.
03
Front-to-Back Recycling
Every 120px of cursor travel, the lead image detaches and teleports to the back of the chain with a new photo. Position-snapped to the tail so it appears seamlessly.
04
Bell Curve Sizing
Images ramp from small at the head, peak at 65% of the chain, then taper at the tail. The 0.6 multiplier on taper keeps visual weight at both ends.
05
Differential Easing
Head tracks the cursor at 0.25 easing, tail lags at 0.06. This gradient creates the snake-like feel — the tail whips and oscillates on direction changes.
06
Zero-Latency Upload
Images use URL.createObjectURL() so they never leave the browser. Drag-and-drop or file picker — full privacy, instant preview, no server round-trip.
HEAD PEAK TAIL CURSOR TRAIL PATH →
Trail path with bell-curve sizing — head → peak → tail
003 Features
Rotation Toggle
Enable or disable random rotation per image. When off, all photos stay perfectly aligned — useful for clean, editorial presentations. When on, each image tilts based on trail direction.
Rounded Corners
Toggle border-radius on all photo frames. The radius scales with image size — larger mid-chain photos get proportionally more rounding for optical consistency.
Image Upload
Drag-and-drop or file picker. Uses URL.createObjectURL() so images never leave the browser — zero upload, zero latency, full privacy.
Infinite Cycling
The library loops endlessly. Whether you have 5 images or 500, the snake continuously cycles through them as you move. Modulo wrap handles the loop.
004 Key Code

01 — The Trail System

Every frame, the cursor position is pushed to the front of an array. This creates a historical path that older (further back) images follow with increasing delay.

animation.js
// Add current mouse to trail head (index 0 = newest) const last = state.trail.length > 0 ? state.trail[0] : null; if (!last || Math.abs(state.mouseX - last.x) > 1 || Math.abs(state.mouseY - last.y) > 1) { state.trail.unshift({ x: state.mouseX, y: state.mouseY }); if (state.trail.length > TRAIL_LENGTH) state.trail.length = TRAIL_LENGTH; }

The trail stores up to 3,000 points. This is the history that gives the snake its organic, curving shape — every twist of your wrist is preserved for the tail to trace through later.

02 — Trail Position Solver

Each image needs to find a point at a specific distance along the trail from the head. This function walks the trail segment-by-segment, accumulating distance until it reaches the target.

trail.js
export function getTrailPos(distance) { let remaining = distance; for (let i = 0; i < state.trail.length - 1; i++) { const dx = state.trail[i + 1].x - state.trail[i].x; const dy = state.trail[i + 1].y - state.trail[i].y; const segLen = Math.sqrt(dx * dx + dy * dy); if (segLen < 0.001) continue; if (remaining <= segLen) { const t = remaining / segLen; return { x: state.trail[i].x + dx * t, y: state.trail[i].y + dy * t, angle: Math.atan2(dy, dx), }; } remaining -= segLen; } }

The angle return value is critical — it’s what allows each photo to subtly rotate to match the direction of the trail at its position, creating the organic slither effect.

03 — Front-to-Back Recycling

Instead of shifting all images simultaneously, only the lead image moves — it detaches from the front of the chain and teleports to the back, receiving a new photo.

slots.js
export function recycleFront() { if (state.order.length < 2) return; // Take the front element, push it to back of order const frontIdx = state.order.shift(); state.order.push(frontIdx); const p = state.photoEls[frontIdx]; // Assign the next image in the library p.imgEl.setAttribute('src', state.images[state.nextImageIndex % state.images.length]); state.nextImageIndex = (state.nextImageIndex + 1) % state.images.length; // Snap position to current tail so it doesn't fly across const tailIdx = state.order[state.order.length - 2]; const tailP = state.photoEls[tailIdx]; p.cx = tailP.cx; p.cy = tailP.cy; updateSlotProperties(); }

This is triggered every 220px of cursor travel. The position snap to the previous tail prevents the recycled element from visually flying across the screen — it appears seamlessly at the end.

04 — Bell Curve Sizing

Images aren’t uniformly sized. They follow a bell curve peaking at 65% of the chain, creating a natural focal bulge in the middle with smaller ends.

slots.js
export function getSlotSize(slotIndex) { const progress = slotIndex / Math.max(SLOT_COUNT - 1, 1); const s0 = seed(slotIndex, 0); const peak = 0.65; const sizeCurve = progress <= peak ? progress / peak // ramp up : 1 - (progress - peak) / (1 - peak) * 0.6; // taper down const baseSize = 40 + sizeCurve * 210 + s0 * 35; }

The 0.6 multiplier on the taper means the tail end never shrinks back to head size — it stays at roughly 40% of peak, maintaining visual weight at both ends of the chain.

05 — Differential Easing

Each slot has its own easing speed based on position. The head tracks the cursor tightly while the tail lags behind — this single trick creates most of the snake-like feel.

animation.js
// Head follows fast (0.25), tail is lazier (0.06) const ease = 0.25 - progress * 0.12; p.cx += (pos.x - p.cx) * Math.max(ease, 0.06); p.cy += (pos.y - p.cy) * Math.max(ease, 0.06); // Size also interpolates smoothly during recycle p.cw += (p.tw - p.cw) * 0.07; p.ch += (p.th - p.ch) * 0.07;

Without this differential easing, all photos would move in lockstep and the effect would feel rigid. The easing gradient from 0.25 to 0.06 is what makes the tail whip and oscillate when you change direction quickly.

Open it up