Motion Pages logo

Motion Pages

Community
jiangyurong609
motion-pages

Build production-ready immersive motion pages in the award-site mold — Three.js worlds (foggy heroes, glass product stages with sonar rings, drag-orbit dome galleries, scroll-driven camera journeys with particle morphs, walkable procedural game worlds with HUD chrome) AND non-Three.js motion (raw-WebGL liquid-glass ripple typography, springy draggable poster walls, cursor-trail image reveals, horizontal scroll-snap stories, scroll-scrubbed process reveals, cursor mask reveals, gesture control) — each a single HTML file with a crisp DOM overlay, responsive to phone/tablet, touch-aware, with a mandatory multi-viewport screenshot loop and a design-review (aesthetic + conversion) pass. Use when asked for a "3D landing page", "Three.js hero", "living/breathing homepage", a 3D product page, a 3D gallery, a scroll-story page, "liquid glass", a ripple/distortion hero, a draggable poster wall, "images that follow the cursor", a horizontal-scrolling story, a "scroll to build it" process explainer for a business, a game landing page whose hero is a playable procedural world, or to apply an award-site motion effect to a brand.

Overview

Publisherjiangyurong609
Repositorymotion-pages
Skill namemotion-pages
Stars
4
Forks
1
Bundled files
80
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 80 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by jiangyurong609 on GitHub. Read the source before you install it.

Installation

Install the Motion Pages AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/jiangyurong609/motion-pages/tree/main
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/jiangyurong609/motion-pages.git \
  .claude/skills/motion-pages
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Motion Pages in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Motion Pages on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Motion Pages is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Motion Pages — Award-Site Immersive & Motion Recipes

Recipe derived from (and verified by replicating) the "Sylva — Step into the living world" example: a monochromatic foggy 3D world fills the viewport, organic hero geometry sweeps across the lower half, and ALL text/UI lives in a crisp DOM overlay — never as 3D text. Single HTML file, < 1 MB of code, 60 fps. The same world can later be dimmed and reused as a backdrop under a normal content page.

Working reference implementations ship with this skill — read the one nearest your task before building:

  • examples/sylva-replica.html — the foggy hero (every base pattern below);
  • examples/fernline-hero.html — the hero applied to a fictional SaaS brand;
  • examples/sona-product-hero.html — glass product stage + sonar rings + orbitals;
  • examples/dome-gallery.html — drag-orbit dome gallery + click-to-focus fly;
  • examples/boreal-journey.html — scroll-scrubbed camera rail + particle morphs;
  • examples/pura-liquid-hero.html — raw-WebGL liquid-glass ripple typography (no three.js);
  • examples/paperworks-posterwall.html — springy draggable poster wall (no WebGL at all);
  • examples/halde-trail.html — cursor-trail image reveal: prints surface under the pointer (pure DOM);
  • examples/kiln-horizontal.html — horizontal scroll-snap story: wheel drives a sideways rail (pure DOM).
  • examples/alder-build.html — scroll-scrubbed process reveal: the hero builds what the business sells as you scroll, then hands off to the page (pure DOM + canvas).
  • examples/hollowmere-world.html — live game-world hero: a walkable procedural forest (canvas-painted textures, leaf-card canopies, wind, shadow maps, IBL) with the game's HUD as chrome and a trailer shot from the same scene (three.js, zero image assets).
  • examples/volera-morph.html — particle shape morph: crane → V-flock → moon gate (three.js).
  • examples/tempo-easing.html — the easing grammar as an interactive one-file reference (pure DOM).

The recipes below are the skill; the demo list above is only a snapshot. New archetypes ship between releases, so if you have network access, fetch https://motion-pages.pages.dev/llms.txt for the current index — it lists every live demo and, for each, the full build-spec prompt at https://motion-pages.pages.dev/prompts/<name>.txt. Read the prompt for the archetype nearest your task: it pins the layer stack, palette values, geometry parameters, shader math and timings that this file describes in prose. Offline, work from the bundled examples/ — they are always in sync with this copy of the skill.

Architecture (non-negotiables)

  1. One .html file + a vendored vendor/three.module.js beside it. Load three local-first with a CDN fallback — never CDN-only (first paint waits on the CDN ⇒ visitors stare at a blank void), and never local-only (file:// blocks local ES module imports with a CORS "origin null" error, so standalone preview dies):
    js
    let THREE;
    try { THREE = await import("./vendor/three.module.js"); }        // instant when served
    catch { THREE = await import("https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js"); }
    Everything else inline. Card/photo art = inline SVG gradients, never external images.
  2. Three layers + a vignette, fixed, in this z-order:
    • .atmo (z 0) — 2–3 large soft radial-gradient light blooms (one behind the headline, one near the focal card, one low horizon glow). A flat single-color background reads as dead space, not atmosphere.
    • .ghost (z 0) — optional giant translucent brand wordmark, sized so the whole word fits the viewport (total width ≈ 65vw ⇒ ~10.5vw font for a 9-letter word). A wordmark that overflows crops into gibberish ("RONTD"). Place it low (top ~70%) so the 3D world overlaps it — occlusion sells the depth.
    • <canvas> (z 1) — the world, WebGLRenderer({alpha:true}) with NO scene.background, so atmo + ghost sit BEHIND the 3D world; body CSS carries the background color.
    • .vignette (z 1, after canvas) — radial darkening toward the edges; frames the shot and pushes the eye to the hero + CTA.
    • #ui (z 2) — DOM overlay; pointer-events:none on the layer, auto on interactive elements.
  3. Monochromatic fog world: scene.fog = new Fog(WORLD, ~8, ~24) where WORLD == the body background color. Geometry fading into fog at the horizon is the single trick that produces the "infinite living world" depth.
  4. Perf budget: setPixelRatio(min(devicePixelRatio,2)), ACESFilmic tone mapping, NO shadow maps, InstancedMesh for anything repeated, one hemisphere + one directional light.

⚠️ The transform trap (this WILL bite you)

Entrance animations, pointer parallax, centering, and card rotation all want the same CSS transform — and silently clobber each other. Separate them:

  • Parallax → dedicated absolutely-positioned wrapper divs (.px[data-px], inset:0), shifted via the standalone translate property from JS.
  • Entrance → .enter class on the INNER element, animating opacity + translate, flipped by adding .ready to <body>.
  • Element's own placement → nav centering via inset-inline:0; margin-inline:auto; width:fit-content (never translate:-50% — the entrance animation owns translate and will clobber it, pinning the nav at 50% and cropping it off-screen; this exact regression shipped once), card tilt via the standalone rotate property.

Never put two of these on the same element via transform.

Time rules (or headless verification lies to you)

  • Drive the intro/scan from elapsed time (clock.elapsedTime), never by accumulating per-frame dt — headless Chrome under --virtual-time-budget runs few rAF frames, so dt-accumulated state stalls forever (and real browsers hitch too).
  • Creature/state machines driven by dt are fine for live motion, but give them a snap-to-final in still mode.
  • Build a ?still mode: ?still in the URL ⇒ add ready still classes to body, force scan progress k=1, snap creatures to their perch, and CSS body.still .enter{transition:none;opacity:1;translate:0 0} + body.still .art.reveal::after{display:none}. CSS transitions/animations run on the REAL clock, not virtual time — without still mode every screenshot catches them mid-fade.

The effect catalog (compose per project)

  • Organic hero limbs — 3 sweeping CatmullRomCurve3 → TubeGeometry curves in the LOWER half only: one long limb across the bottom, one rising toward the cards, one faint and deep (z ≈ −10). Keep limbs slim (radius .35–.55 at camera z ≈ 10.5, fov 42) with air between them — fat tubes read as ropes, not landscape.
  • Fuzz (moss/texture) — one InstancedMesh of ~14k tiny cones (r .026, h .09), positions hugging each tube surface (point + dir.normalize()*r*(.85+rand*.3)), upward-biased random directions, scale .5–1.5, per-instance setColorAt with HSL jitter (hue .25±.06, sat .3–.5, light .2–.4 for moss). A second sparse InstancedMesh adds micro-details (white flowers, sparks).
  • Deep scenery silhouettes (3rd depth plane) — 4–6 bare vertical shapes at z ≈ −10…−14 so the fog almost swallows them: tree trunks with one lean limb for a forest, utility poles with a crossarm for an urban/tech world — whatever the brand's world implies. Vary height/lean, share the scan's fade-in materials. Without this plane the world reads as tubes floating in a void; with it, the fog implies a whole landscape (the reference's background trees).
  • "Orbit using mouse" — pointer → small target offset (±1.0 x, ∓0.6 y), camera.position.lerp(target,.045) every frame, lookAt fixed. DOM .px wrappers translate −pointer·(6–24 px) at different rates per layer.
  • "Particles to pointer" — 800 Points, slow upward drift + sine sway, additive blending, depthWrite:false; each frame nudge x toward the pointer's world x (x += (px*7 − x)*dt*.01) so dust condenses around the cursor; respawn at bottom.
  • Wireframe scan intro (Death Stranding) — for each limb also add a LOW-POLY wire clone (TubeGeometry(curve, 34, r*1.02, 7), MeshBasicMaterial{wireframe}) — low segment counts give the clean triangulated scan look; full-res wireframe is noise. All final materials start transparent:true, opacity:0; over ~1.4 s (elapsed-time driven, smoothstepped) fade finals 0→1 and wires .55→0. Intro budget: the page must feel settled by ~2.5 s — longer intros test as "slow", not cinematic. Flip body.ready from the FIRST RENDERED FRAME (+ ~0.9 s), never a fixed timeout: if the lib loads slowly the whole choreography just shifts later instead of UI floating over a blank void. Add a ~3 s plain-<script> fallback that adds .ready anyway so a WebGL failure never leaves the page blank.
  • Scan-line image reveal — ::after overlay on card art: dark cover with a bright 2 px line, background-size:100% 220%, keyframe sweeps background-position to −120%, forwards, staggered delays per card.
  • A living creature — butterfly: two PlaneGeometry wings hinged at the body (geometry.translate(w/2,0,0), second wing rotation.z=π), flap = sin(t*18) on rotation.y. State machine fly → rest (on a limb, +0.75 y) → flee when pointer's world-projected point comes within 1.5 units, then loop. Adapt the creature to the brand (butterfly, bird, glowing spark, paper plane).
  • Traveling pulses — glowing spheres riding curve.getPointAt((t*speed+off)%1) toward a converging point; good for "flow" metaphors (calls, data, energy).

Expanded archetypes (derived from award-site studies — AETHER:1, OpenPurpose, Igloo Inc, Lando Norris, MISC, ITOM)

Beyond the foggy hero, four more page shapes reuse the same architecture (fog world + DOM overlay + still mode). Working references ship with the skill: examples/sona-product-hero.html, examples/dome-gallery.html, examples/boreal-journey.html.

The fake-bloom kit (use it in EVERY archetype — this is the gap between "correct" and "award-site"). Post-processing bloom needs addon passes; you don't need them. What actually makes the reference sites glow:

  • Never render bare Points — default points are SQUARES and read as confetti. Always set map to a canvas radial-gradient sprite (white core → transparent edge, ~3 stops, 64px) with transparent, depthWrite:false and usually additive blending.

  • Baked halo texture = bloom. A 512px canvas radial gradient with a bright ring band (~stops .58/.68/.72/.76/.88, peak rgba(...,.95) at ~.72) on an additive plane IS the glowing-ring/portal look (AETHER's halo, Igloo's portal) — one draw call, works under SwiftShader.

  • Fresnel shader for glass — ~12-line ShaderMaterial (vF=pow(1-|dot(n,view)|,2.4), alpha vF*.9+.04, additive, DoubleSide, depthWrite:false) over a faint dark fill shell + a PointLight INSIDE the object. Bright live edges, clear center — real glass read with zero env maps. ⚠️ Never MeshPhysicalMaterial{transmission}: it renders as an OPAQUE GRAY BLOB under SwiftShader and muddy on weak GPUs.

  • Sparkle sub-cloud — alongside a big soft-particle cloud, a 2nd Points of ~500 larger glints whose positions COPY random indices of the main cloud each frame (they morph for free) with slowly pulsing opacity.

  • Film grain — a fixed CSS overlay (SVG feTurbulence data-URI, opacity:.06, mix-blend-mode:overlay) placed under the text layer; kills flat-gradient banding and adds the cinematic finish every reference site has.

  • Layered gradient atmosphere behind the canvas (2–3 radial glows + a vertical linear ramp), even for light themes — one flat bg color reads as dead space.

  • Glass product stage (AETHER:1-style earbuds/device hero) — the product floats right of the copy column inside a breathing halo + expanding sonar rings: fresnel glass case (kit above), emissive buds inside, interior point light, baked halo plane

    • a wide soft nebula plane behind it, thin geometry rings expanding past the halo, two counter-rotating wireframe icosahedron shells and a two-layer soft starfield in the deep background.
    • Sonar rings: 3 thin RingGeometry(1.94,2.0), additive, staggered phases, scale 1→2.6 with opacity (1-ph)²*.4. THIN is the trick — a fat ring reads as a donut, not a pulse. In still mode give each ring a DIFFERENT frozen phase so the screenshot shows the expanding sequence.
    • Orbital arcs: tilted EllipseCurve lines (opacity ~.3) each carrying one small glowing dot at getPointAt((t*speed+phase)%1).
    • Put product + rings + orbits in ONE stage group; reposition per breakpoint in resize() (desktop: x≈+2.3 beside the copy; phone: y≈+2.1, scale .62 above it).
    • Sound toggle homage: generate a 3-oscillator WebAudio hum (no media files), and make ring amplitude/opacity react — an honest, working "Sound: on/off" chip.
  • Dome media gallery (OpenPurpose-style) — dozens of cards on the inside of a sphere, camera at center; drag to orbit, click to fly to a card.

    • DENSITY sells it: ~100 cards in ~8 latitude bands (counts 9…19 per band), position.setFromSphericalCoords(R≈9.5,…) + lookAt(0,0,0), card height ~1.55. Five sparse bands read as floating litter, not a dome.
    • Card art: CanvasTexture fake site thumbnails from a SEEDED rand — zero image requests, deterministic screenshots. Art-direct them like a portfolio, not a wireframe: 4–5 templates (classic site, giant-typography poster, duotone image-led, dark product spotlight), ~15% dark cards, occasional accent color, and bake a soft drop shadow into each canvas (transparent padding + shadowBlur≈22, offsetY≈10) — the shadows are what make cards pop off the void.
    • Drag orbit with inertia (v*=.95 on release) + slow idle auto-rotate; disambiguate click vs drag by pointer travel (>6 px = drag, swallow the click).
    • Click focus: raycast → tween camera to cardPos*0.62, lookAt(card), dim all other cards to opacity .18, show a DOM caption pill + "Back" (Esc works too).
  • Scroll-driven camera journey (Igloo/ITOM-style) — a fixed canvas under a tall scroll track (#track{height:500vh}); progress p = scrollY/max, smoothed (p += (target-p)*.07), scrubs the camera along a CatmullRomCurve3 rail with a parallel look-at rail; DOM caption blocks fade per progress segment.

    • Add a ?p=0..1 override that pins progress — scrolled states become plain --screenshot calls (no CDP needed), and every keyframe of the journey is verifiable. Generalize this: ANY interactive state a screenshot can't reach by itself gets a forcing query param (?focus=N for the dome's fly-to, etc.).
    • ⚠️ The rail must clear the terrain: displaced-noise ground WILL rise above a low camera mid-path and fill the frame with a featureless slab. Flatten a clearing around the subject (amplitude *= clamp((d-6)/11,0,1)) and keep rail y above the local terrain max; verify with ?p= shots at 0/.25/.5/.75/.95.
    • Captions over a bright fog world need a scrim (blurred dark glass pill), not just text-shadow — white-on-white particles swallow bare text.
  • Particle shape morph (Igloo's creature / gesture demos) — ONE Points cloud (~6k, soft sprites + sparkle sub-cloud from the kit), several precomputed Float32Array target sets. Morph = per-particle smoothstepped lerp with a per-particle stagger offset (s=clamp((blend-stagger*.5)/.5)), plus sin(ss*π)-weighted scatter so particles loosen mid-flight and re-condense — that scatter is what makes it read as dissolution rather than a cheap tween.

    • Don't sample smooth surfaces — QUANTIZE targets into structure: the igloo is brick cells on a hemisphere (rows offset half-a-brick for a running bond, a skipped wedge per bottom row forming the entrance arch, plus a half-cylinder tunnel); the gaps between cells read as mortar seams. Structure > density.
    • Creature = union of ~9 offset spheres (body/head/ears/paws/snout) sampled volumetrically; silhouette matters, detail doesn't — particles forgive crude geometry. During the ring act, back the particle torus with the baked halo plane
      • a soft central burst (windowed by scroll: w=clamp(1-|p-.5|*5)) — the particles alone are a shape; the halo makes it a PORTAL.
  • Cursor mask reveal (Lando-Norris-style) — mostly DOM: stack alternate hero layers (helmet/visor variants) and drive clip-path:circle(r at x y) (or a WebGL uv-discard mask) from the pointer; snap layer choice to pointer zones so elements change the instant the cursor crosses them.

  • Live game-world hero (hollowmere-world.html, the "landing page IS level one" pattern — an indie isometric ARPG whose site opens on a walkable procedural world with the game's HUD as chrome; the reference had procedural trees/rocks/monuments/characters and a whole game under 2 MB before art and music) — Three.js, one scene, zero image assets, and the one archetype that EARNS shadow maps:

    • Every texture is painted on a canvas at load (~1 s of JS, cached by the GPU): periodic value noise (pn(x,y,P) with a lattice modulo so tiles wrap) → fbm; ground albedo 768² with grass↔moss by noise, leaf litter/dry-blade speckles, the road baked in via distance-to-polyline with a noise-wobbled edge, mud around the pond; a tiled bump map; bark (streaky low-frequency + dark seams) + bump; stone (noise + moss tint + stroked cracks) + bump; a water NORMAL map from finite differences of noise; leaf clusters (46 gradient ellipses with a midrib), grass blades (14 tapered quadratic curves), a fern frond (stem + 20 leaflet pairs), flowers — all alpha-tested cards.
    • Foliage is cards, not blobs: a canopy = 22–34 instanced leaf-card planes in a flattened ball (cbrt(rand) radius so the shell is denser), random Euler rotation, per-instance HSL that darkens toward the underside; alphaTest:.5, DoubleSide, castShadow with a customDepthMaterial (MeshDepthMaterial + the same map + alphaTest) or the shadow is a solid square. Grass = 5,600 instanced blade cards (2,600 on portrait), ferns = 3 crossed cards, flowers = 2. Wind = one onBeforeCompile that reads the instance origin, adds sin(uTime + ip.x·.6 + ip.z·.8) displacement scaled by uv.y² so roots stay planted; amplitude .16 grass, .07 fern, .05 leaves. Freeze uTime in still mode.
    • Shadow maps, but only one: a single directional light, PCFSoft, 2048 (1024 on lite), ortho frustum ±19 units that FOLLOWS the focus (sun.position = SUNDIR + target), bias −.0007, normalBias .04. Rocks/ruins/trunks/knight cast; grass only receives. Hemisphere + a painted-sky PMREM environment (PMREMGenerator.fromScene on a vertex-coloured gradient sphere + a bright sun ball; scene.environmentIntensity .55 by day, .18 by night) — the IBL is what makes steel (metalness .88) read as steel instead of black.
    • Fog must start beyond the focus distance: camera at 44 units, fog 50→84. With fog at 34 the whole clearing sat a third of the way into the haze and every frame looked washed; only the far rim should fade.
    • Camera = PerspectiveCamera fov 30 (44 on portrait) at normalize(1,1.28,1)×44, focus lerping 35 % toward the player plus pointer parallax. Leave the quadrant in front of the camera treeless (x+z > 13 → skip) or the canopies occlude the hero. Rocks = three noise-displaced icosahedra (detail 3) instanced with non-uniform scale, low-value colours (HSL L .38–.62 — pale rocks read as paper). A pond = a terrain depression + a CircleGeometry with the water normal map scrolling (offset = t·.012), roughness .1, envMapIntensity 1.3. Pollen = 320 additive soft points drifting as a function of t, faded by daylight. Light shafts = a DOM repeating-linear-gradient at 112°, mix-blend-mode:screen, radial-masked to the sun corner, drifting on a linear loop, opacity tied to daylight.
    • The character is ~25 primitives on pivots: leg pivots (mail capsule + steel greave + boot), a mail skirt cone, capsule torso + a breastplate sphere, pauldrons, a helmet sphere with a visor bar and gold crest, arm pivots carrying a tapered cylinder blade (scaled flat) with a gold crossguard and a squashed-sphere shield with a torus boss, a cloak plane hinged at the neck. Walk cycle = sin(t×9.5) on the pivots scaled by a walk blend, plus a bob; the cloak leans with speed.
    • Play, not just look: click/tap → raycast onto the y=0 plane → clamp to the clearing (and out of the pond) → the character turns (shortest-angle lerp) and walks; a pulsing ring marks the target. Three "waystones" light when reached (emissive lamp + PointLight) and drive a quest counter in the HUD; 3/3 toasts a wishlist link. Hotbar + keys map to real verbs (strike swing, guard pose, dodge dash with a stamina dent, nightfall, rest). Autopilot: after 5 s without input the character walks the quest himself — the hero is never still and the quest completes for a passive viewer.
    • Time of day as one scalar tod: sun/hemisphere/fog colours, environment intensity, exposure and fire/lamp intensities lerp from a daylight weight + a dusk gaussian; night must still read (sun ≥ .9, hemi ≥ .75, exposure ≥ .95). Set each light's base from tod, then flicker multiplicatively from t — never intensity *= … across frames. The trailer pins tod to .5 for its 12 s.
    • The trailer is the same scene: swap to a PerspectiveCamera for three 4 s shots (low orbit at the fire → dolly to the arch → crane up to a serif title card), letterbox bars, HUD fades, Esc/Exit returns. Press screenshots are rendered ONCE by the same renderer from other cameras at other hours into 2D canvases (renderer.setSize(w,h,false) → render → ctx.drawImage(renderer.domElement) → restore). No second pipeline, no images.
    • HUD = DOM: glass panels (rgba .8 over a bright world — .62 fails contrast) with a gold hairline, an inset second hairline and four corner dots; a circular minimap drawn on a 2D canvas from the same object lists; a vitality orb; inline-SVG hotbar icons with keycaps. Text over the world needs an opaque backing (chip/pill) or the contrast audit samples the grass; invisible overlays (toast, cinematic title) need visibility:hidden, not just opacity 0.
    • ?still = t frozen, tod .47, first waystone lit, character at the fire, no autopilot; ?tod= pins the hour; ?lite halves textures/shadow map/grass; portrait shrinks the walk radius so the character never leaves the frame. 60 fps on an Apple-silicon integrated GPU at DPR 2.

Motion beyond Three.js (same architecture, no 3D library)

The reference reels are NOT all Three.js — liquid-glass typography and physical poster walls are raw WebGL / pure DOM. Same non-negotiables apply (single file, DOM overlay, ?still mode, screenshot loop).

  • Liquid-glass ripple typography (pura-liquid-hero.html, the OYLA/"Ripple Distortion" look) — paint the editorial page ONCE on an offscreen 2D canvas (giant serif display type, gradient product orb, captions — this layer is what distorts; CTAs/nav stay crisp DOM above), then a raw-WebGL fragment shader over it:
    • Analytic ripples, not FBO water sim: keep a pool of ~24 vec3(x,y,age) uniforms; each fragment sums expanding ring waves sin((d-age*speed)*44) * exp(-band²*260) * exp(-age*1.6) and displaces the sample uv along the radial. Ping-pong float FBOs die on SwiftShader/older GPUs; analytic rings are deterministic (still mode = a preset ripple array) and look identical.
    • Liquid-glass lens on the cursor: pull uvs toward the pointer (-inl²*.05, inl=smoothstep(R,0,d)) + a rim highlight at the lens edge.
    • Chromatic aberration = sample R/G/B with displacement ×1.06/1.0/0.94; wet look = add white proportional to the summed wave crest (~.24 — more goes milky).
    • Cover-fit the source texture in-shader (compare canvas vs texture aspect) so resize never letterboxes; spawn ripples throttled (~70 ms) on pointermove, a burst of 3 on click; idle autopilot drifts the lens and drips ripples so the page is never dead.
  • Springy poster wall (paperworks-posterwall.html, MISC-style) — no WebGL at all; the paper physics is a spring in CSS transforms:
    • Infinite wrap: one COLS×ROWS block of posters tiled 3×3; each cell's screen position = mod(base + offset + B/2, B*3) - B*1.5 per axis — drag forever, no edges, 36 DOM nodes total.
    • Bend from lag, not velocity: smooth the offset (o += (target-o)*.14) and use the LAG (o-target) as bend energy → target tilt rotateX/rotateY (clamped ±22–26°), integrated per-poster with a spring (v += (t-r)*.16; v *= .82) and a per-poster stiffness factor so the wall ripples like sheets, not a rigid grid.
    • Poster art = seeded canvas paintings (bold palette, stacked display words, glyphs, zigzags, circled numerals + catalog footer line) — zero image requests.
    • Drag + wheel both pan; momentum on release (v *= .94); click-vs-drag gate (>6 px travel swallows the click); click → backdrop-blur modal enlarge.
    • Floating UI over busy art needs its own surface: brand chip and hint pill get a translucent bg + backdrop blur, never bare text.
  • Cursor-trail image reveal (halde-trail.html, the "images follow the mouse" portfolio hero) — pure DOM; the archive surfaces wherever the pointer goes:
    • A pool, not a stream: ~16 print nodes recycled in order, newest gets the top z-index. Never create/destroy nodes per move — GC hitches read as stutter.
    • Spawn by distance, not time: one print per ~64 px of pointer travel (52 on phones) so a slow hand leaves a sparse trail and a fast one a dense fan. Track velocity with a low-pass (v = v*.6 + d*.4).
    • Exit along the path: entrance is scale .6→1 on a soft-landing curve (~.4 s); after ~700 ms the print lets go — fades, shrinks to .92 and DRIFTS ~70 px along the pointer's own direction (+ a little lift). That directional exit is what makes it a trail instead of a pile. Reset classes + force a reflow before re-placing a node so the entrance transition always plays.
    • Prints are seeded canvas duotones (horizon / ridgeline / doorway / figure) inside a white print border with a mono caption row — zero requests, deterministic still.
    • The headline sits ABOVE the trail with mix-blend-mode:multiply so prints read through the letters; click = a five-print burst; idle autopilot (phantom cursor on a figure-eight after ~2.6 s) keeps the page alive; ?still = eight prints on a fixed S-curve.
  • Horizontal scroll-snap story (kiln-horizontal.html, the sideways chapter page) — pure DOM; vertical scroll drives a horizontal rail:
    • A tall invisible #track{height:N*100vh} gives the document its scroll; a fixed, overflow:hidden view holds a flex rail of 100vw sections translated by -p*(N-1)*100vw, p smoothed per frame (p += (t-p)*.09). The clip on the fixed view is what keeps scrollWidth == innerWidth — the phone-overflow audit rule will catch you if the rail leaks.
    • Parallax from LOCAL progress: each chapter gets --p = clamp(p*(N-1)-i,-1,1) and its layers move at different rates from it (numeral −18vw, artwork −9vw with a −6° roll, copy +6vw). Three speeds = depth; one speed = a slideshow.
    • Soft snap: ~160 ms after the last scroll input, glide scrollY to the nearest chapter (ease-out cubic, ~650 ms); ANY user scroll cancels the glide. Dots + arrow keys jump chapters; a 2 px top bar scales with p.
    • ?p=0..1 pins progress (collapse the track, lock body scroll) so every chapter is a plain screenshot; ?still = a between-chapters value (.31) so the parallax is visibly mid-flight. prefers-reduced-motion = no smoothing, no snap.
    • Artwork = seeded canvas vessels lathed from a smooth radius function (48 samples) — a distinct silhouette per chapter so the story reads at a glance.
  • Scroll-scrubbed process reveal (alder-build.html, the "scroll to build it" business hero — a pool builder's site does this with construction photos, and agencies charge for exactly this screen) — pure DOM + one canvas; scroll progress builds the thing the business sells:
    • A #track{height:500vh} with a position:sticky;top:0;height:100vh stage inside it (sticky, not fixed — the stage scrolls away by itself when the track ends, no unpin logic). p = scrollY/(track.offsetHeight−innerHeight), smoothed per frame (p += (t−p)*.1) so wheel notches read as a scrub, not steps.
    • One draw function, one timeline: drawScene(ctx,w,h,p,t); every stage is a seg(p,a,b) window (stakes .02–.09, string .06–.16, soil .14–.22, piers .18–.25 staggered, deck .28–.36, posts .36–.44, plates .42–.50, rafters .48–.56, walls .56–.64, gable .62–.68, roof .66–.74, openings .72–.78, dusk .78–.92, window glow .84–.97). Windows overlap a little so the build never pauses.
    • Nothing snaps in: piers grow from the ground, the deck drops from +1.4 units on an ease-out, posts extend, beams and rafters draw along their length (partial lineTo), walls rise with a clipped height, the roof lands from above. Grow, drop, draw — three verbs, reused for every part.
    • Scene = isometric projection [ox+(x−y)·.866k, oy+(x+y)·.5k−z·k] on a 2D canvas, no 3D library. Pitch the roof steeper than the view angle (rise > half the depth) or the camera sees the back slope over the ridge; draw the chimney BEFORE the roof so the slope hides its base. Every colour is a mix(day,dusk,d) pair, so the finale (lights on, stars, smoke) is the same scene, not a second one.
    • Captions: five pre-rendered .cap nodes, index from p thresholds, .on toggles a soft-landing crossfade — never swap text mid-transition. A 2 px bar with stage ticks, a 01 / 05 counter, a hint pill that leaves after 2 % of scroll.
    • White type over a sky needs scrims: top and bottom linear-gradient pseudo-elements on the stage (.72→0, .82→0) — without them the contrast audit fails on the day frames.
    • The hero HANDS OFF: statement → dark stats strip → gallery (the same drawScene at p=.77 / 1 / a winter palette into 640×480 minis = three "projects", zero images) → process steps → testimonial → quote form. The build is the argument; the form is the close.
    • ?p=0..1 pins (100vh track, body scroll locked); ?still = .69 (roof mid-drop) with t frozen so clouds/stars/smoke hold; prefers-reduced-motion = p follows scroll instantly, no entrances.
    • Swap the drawing for photographs when the client has them: the same timeline drives ctx.drawImage(frames[Math.round(p·(N−1))]) from a preloaded frame sequence (the Apple product-page trick) or video.currentTime = p·duration on a muted <video>; keep the seg() captions, scrims and hand-off exactly as they are.
  • Gesture control (webcam demos) — MediaPipe Hands (or FaceLandmarker) mapped onto the SAME particle-morph machinery: pinch distance → gather/scatter blend, palm x/y → group rotation. Keep it an optional progressive enhancement behind a permission prompt; pointer fallback always works.

DOM overlay kit (matches the reference look)

  • Pill navbar top-center, backdrop-filter:blur(16px), dark 50% bg — with the reference's full state system (a color-change-only nav reads as basic):
    • logo chip: white ROUNDED-SQUARE (radius ~13px, not a circle) with a monochrome brand glyph, raised shadow, playful hover (rotate(-6deg) scale(1.05));
    • EVERY link gets an 11px stroke icon + uppercase letter-spaced label (10.5px .18em);
    • active link: SOLID white pill, dark text, raised shadow — not a translucent wash;
    • hover: a raised pill materializes — vertical gradient bg, hairline rgba(255,255,255,.14) border, translateY(-1px), inner top light + drop shadow;
    • the nav's own CTA (Enter/Start free): visually distinct at rest — bordered glass pill, or a mini shader-gradient pill for conversion-focused brands.
  • Hero reads top-to-bottom in ONE left column: category badge → headline (Quicksand 500, ~56–60px, 2 lines, one value word in a gradient background-clip:text accent) → subheadline DIRECTLY UNDER the H1 (≥15px, key value phrases in <b>) → CTA row. Never park the explainer paragraph beside the headline in tiny muted type — that is where the core value goes to die.
  • CTA row: the primary CTA must be the highest-contrast element on the page — brand-gradient fill + colored glow shadow; a dark pill on a dark world is camouflage. Label = action + time-to-value ("Start free — live in 5 minutes →"). Secondary demo button beside it: glowing pulsing ring + a TEXT label naming the content and its honest length ("Hear it answer a call · 1½-minute demo") — bare play circles get ignored, and check the real media duration (mdls -name kMDItemDurationSeconds), not the filename.
  • Trust line: ✓-marked risk reversals ("No credit card · Keep your number · Cancel anytime") sit DIRECTLY UNDER the CTA row at ≥13px — risk reversal works at the point of decision, never orphaned in a far corner of the viewport.
  • Glass cards: white #f7f8f4, 22px radius, 9px padding, art block 15px radius with inline-SVG gradient art, kicker (10px uppercase) + 18.5px claim, small circular action button bottom-right; standalone rotate:±1.4deg; deep soft shadow.
  • Stat chips: frosted-glass pills (blur + border, matching the nav) with a small-caps label over a ~21px bold value — bare floating micro-text over the world is invisible.
  • Every interactive element must WORK on day 1: demo button opens a <video> modal (Esc / backdrop / × close, aria-labels, preload="metadata", <source> list = local-relative file then site-absolute fallback); nav/CTA links point at the REAL app's routes and verified anchor ids (/#how, /onboarding) — relative when the hero will be mounted in the app, absolute only for a standalone share.

Fluid UI layer (shader buttons + cursor light — the difference between "basic three.js" and the reference)

The reference's fluidity is mostly NOT in the 3D scene — it's that every interactive surface behaves like a lit object. Pure CSS + ~10 lines of JS, no libraries:

  • Shader button (primary CTA, key pills) — four layers on one element:
    1. glass gradient base stacked over the brand fill: background:linear-gradient(180deg,rgba(255,255,255,.15),transparent 40%[,rgba(0,0,0,.28)]), <brand> plus inset 0 1px 0 rgba(255,255,255,.2) top rim;
    2. idle specular sweep — a blurred diagonal white streak (::before, ~42% wide, rotate:14deg, filter:blur(5px)) gliding across every ~5.5s (@keyframes sheen{0%,55%{left:-45%}85%,100%{left:115%}}) — the button breathes even when idle;
    3. cursor-following light — ::after radial gradient centered at var(--mx) var(--my), opacity:0→1 on hover;
    4. hover bloom: lift −2px + brighter outer glow; :active{scale(.97)};
    5. backlit under-glow on hover (the reference's signature) — the button looks lit from behind its bottom edge: stack a second radial in the same ::after (radial-gradient(115% 95% at 50% 132%, <glow>, transparent 58%)) plus a bottom outer shadow (0 14px 38px -8px <glow>) and a bottom inset (inset 0 -12px 26px -14px <glow>). Glow color is brand light: warm dawn gold for nature themes, the accent (e.g. violet) for tech brands. Tint the sheen streak the same temperature.
  • Frosted-glass play disc — not a flat translucent circle: vertical gradient going milky toward the bottom (rgba(255,255,255,.03) → .16 @62% → .32), backdrop-filter:blur(7px) so the world refracts through it, bright bottom inner rim (inset 0 -9px 18px -6px rgba(255,255,255,.38)), top hairline; hover brightens the glass and scales 1.07. Needs overflow:hidden and content wrapped above the pseudos (.cta>*{z-index:2}).
  • JS — one pointermove listener per shader surface (CTA, nav bar, AND each nav link — ".cta, nav, nav a") writing element-local --mx/--my percentages from getBoundingClientRect().
  • Nav glass — cursor light on the pill bar, plus a scoped light inside each link's hover pill (nav a::after radial at --mx/--my; display:none on .active — light on a solid white pill is invisible noise). The hovered pill's fill follows the pointer instead of being a static gradient.
  • Cards — lift on the WRAPPER (.cardpos:hover{translate:0 -6px} — the wrapper has no .enter, so no transform-trap conflict), tilt flattens to rotate:0, shadow deepens, art scales 1.05 over .8s; corner icon button inverts + scale(1.12) rotate(8deg) on hover.
  • Play/demo rings — slow ripple outward (scale .92→1.16, fade), staggered ~1.7s.
  • Easing system — :root{--e:cubic-bezier(.22,1,.36,1)} (expo-out) on every micro-interaction, .35–.5s. Default ease .2s is what makes a page feel "basic".
  • ⚠️ Stagger-delay trap — entrance .d1–.d5 classes set transition-delay that would lag every hover FOREVER. Add body.settled .enter{transition-delay:0s} and flip .settled ~1.6s after .ready (immediately in still mode). Also disable sheen/ripple keyframes under body.still (they run on the REAL clock and break deterministic screenshots) and under prefers-reduced-motion.

Easing grammar (curves have semantics — don't use one for everything)

--e above is the hover curve. Pick curves by what the motion MEANS, then reuse the same handful everywhere — a consistent bezier set is what makes motion feel like one brand instead of a component kit:

  • Entering (cards/modals/toasts appearing) → ease-out (fast feedback, soft landing): cubic-bezier(0,0,.2,1) or the expo-out --e.
  • Leaving (dismiss, close, fade-away) → ease-in (cubic-bezier(.4,0,1,1)) — slow start reads as "letting go", and nobody needs to track the end of an exit.
  • Moving within the page (reorder, expand/collapse, panel slide, page transitions) → ease-in-out (cubic-bezier(.45,0,.55,1)) — the whole journey is on screen, so show both the pickup and the set-down.
  • Loops (orbit drift, marquees, rotation, pulse rings) → linear or sine — any easing in a loop reads as a stutter.
  • Brand signature: define the full set as custom properties once (--e-out/--e-in/--e-inout) and never write a raw curve at a use site. One distinctive overshoot curve (e.g. cubic-bezier(.34,1.56,.64,1)) reserved for the ONE hero moment (CTA settle, logo land) is a recognizable motion identity; overshoot everywhere is a toy.

Responsive + touch (mandatory — this ships to phones on day 1)

Absolute desktop positioning collapses into a card-pile on a 390px screen. Two breakpoints minimum:

  • Phone (max-width:740px) — single column: nav becomes a full-width bar (logo left, CTA right, middle links hidden; don't try to keep it center-pinned — left:4%;right:4%;justify-content:space-between). Two modes depending on the page:
    • One-screen standalone hero (overflow:hidden demo page): hide the stat chips, secondary cards and edge micro-labels; keep ONE card peeking from a bottom corner (right:-24px;bottom:-34px;rotate:4deg) as an artistic cue.
    • Scrolling product page: reflow, never hide — real users on real phones notice missing stats and cards. Make the hero viewport an auto-height flex column, switch the .px parallax layers and their children to position:static, and use flex order on the layers to sequence badge → H1 → sub → CTA → trust → chips → ALL cards (centered, width:min(300px,100%), slight rotate kept). Gate the DOM parallax off on phones (if (innerWidth <= 740) return) — touch-drag fires pointermove and would shift static flow blocks.
  • Tablet (741–1180px) — keep the collage but shrink cards (~215px), drop one chip, and re-check that no card covers the demo button's label.
  • Touch = no pointermove, ever. Gate on a hasPointer flag: until the first pointer event, drive the camera with a slow idle drift (pointer.x = sin(t*.22)*.35) so the world still breathes on phones (and on desktop before the first mouse move).
  • prefers-reduced-motion: reduce ⇒ treat as still mode (skip scan/entrance, no camera drift; ambient particles may remain).

Study-a-reference workflow ("make my site feel like this URL")

When the user hands you a URL they love, don't guess — browse it, measure it, then rebuild its MOTION LANGUAGE as original code themed to the user's brand. Never copy the reference's code, assets, images, fonts-by-file, copy text, or branding: the deliverable is a study of its patterns applied to the user's content. Say so in the result.

Step 0 — run the study tool first; it does steps 1–3 in ~40 s:

bash
node scripts/study.mjs https://reference.site        # → study/<host>/spec.md + storyboard PNGs

It storyboards the page at 0/25/50/75/100 % scroll on desktop and phone, measures what reacts to what (idle drift, pointer sweep, drag + inertia, hover, scroll, wheel — by pixel-diffing frames), pulls the palette from rendered pixels and the type from computed styles, greps the script bundles (following Vite/webpack chunks) for three/gsap/lenis/ shader/postprocessing signals, and maps all of it to the recipes in this file with a confidence per match. spec.md ends with a build-spec prompt — edit the brand placeholder and that prompt IS the intake. Read the storyboard PNGs yourself before building: the tool measures, you judge. Sites that seal their DOM in a closed shadow root (some award sites do, as anti-scraping) still get canvas geometry and motion probes over the protocol, but type/copy must be read from the screenshots. Manual fallbacks when you have no Node/Chrome:

  1. Capture what it looks like. Headless screenshots at desktop + phone width; for scroll-driven sites use CDP (see §Self-verify) to shoot 0/25/50/75/100% scroll — the storyboard IS the spec. If you have browser control, also record: what happens on pointer move? on hover? on click? on wheel?
  2. Read the tech signals. The HTML is usually a thin shell — fetch it, extract <script src> URLs, fetch those, and grep the BUNDLES:
    bash
    curl -sL -A "Mozilla/5.0" "$URL" -o page.html
    grep -oiE '<script[^>]*src="[^"]*"' page.html          # then fetch each src
    grep -oiE 'three|gsap|scrolltrigger|lenis|locomotive|curtains|pixi|\bogl\b|gl_FragColor|fragmentShader|createProgram|IntersectionObserver' \
      bundle.js | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn
    Vite/webpack loaders may be tiny stubs that import() more chunks — if the grep is thin on a clearly-3D site, trust the visual/runtime evidence over the grep.
  3. Map observations → recipes. Fullscreen canvas + fog + product center → §glass product stage; page scrolls but camera moves → §scroll camera journey; images wrapped on a sphere/drag → §dome gallery; typography warps under cursor → §liquid-glass ripple; cards tilt with drag momentum → §springy poster wall; layers swap under the pointer → §cursor mask reveal. Anything unmapped: describe the motion precisely (what moves, on what input, with what easing) and synthesize from the kit primitives.
  4. Rebuild + verify. Compose the mapped recipes, theme to the user's brand palette/copy, then run the full screenshot loop AGAINST the reference storyboard — compare frame by frame at the same scroll depths and fix the biggest visual gap each pass.

Prompt vocabulary: "clone the feel of " / "make my landing page feel like " → this workflow.

Intake (30 seconds, before any code)

Don't start building from the effect name alone — answer these first, from the prompt or by asking. Every rework round traces back to a skipped intake question:

  1. What kind of page? One-screen standalone hero vs scrolling product page — this forks the phone strategy (§Responsive) and the archetype.
  2. Who lands on it, feeling what? ("developers, awe", "buyers, trust") — this picks the world density and how loud the motion can be.
  3. Existing brand? Palette/fonts/logo/real copy to honor (§Theming) — or are we inventing an identity? Never invent stats either way.
  4. Upgrade or rebuild? "Make my page feel like X" on an existing site means keep their structure and content, re-skin the atmosphere — not a rewrite.
  5. Industry constraints? (finance/health = restrained motion, legible trust elements; games/fashion = go louder.)

If asked for directions ("show me 3 takes"), make them genuinely different — different archetype + palette + type + motion rhythm — not one layout in 3 tints.

Build order (follow strictly)

  1. Static DOM overlay + palette (verify with a screenshot before any 3D).
  2. Scene + fog + lights + camera. 3. Limbs + fuzz instancing. 4. Orbit parallax (canvas + DOM). 5. Particles. 6. Pulses/creature. 7. Wireframe scan intro.
  3. Card scan reveals + staggered entrance. 9. Responsive breakpoints + touch fallback.
  4. Screenshot-verify loop (all viewports). 11. Design-review pass (below).
  5. Polish: color balance, fog distances, easing.

Self-verify loop (mandatory — "self-verify until perfect")

Headless Chrome renders WebGL via SwiftShader:

bash
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"   # mac
"$CHROME" --headless=new --enable-unsafe-swiftshader --window-size=1600,900 \
  --screenshot=out.png --virtual-time-budget=8000 "file:///abs/path/page.html?still"
  • Screenshot all three viewports: 1600×900 (desktop), 820×1180 (tablet), 390×844 (phone) — plus a no-param desktop shot at --virtual-time-budget=1100 (mid-scan: wireframe should be clearly visible).
  • READ the screenshots and compare against the reference/brief: layer order, limb composition (lower half, air between), text contrast, card legibility, creature visible, nothing clipped, ghost wordmark fully readable.
  • A fully black/empty canvas ⇒ JS error before first frame: re-run with --enable-logging=stderr 2>&1 | grep -iE "error|uncaught".

Headless traps (each one produces a FALSE failure or false pass):

  • --disable-gpu throws inside new WebGLRenderer() — the whole module dies, so you get UI-less ghost-only shots. Use --enable-unsafe-swiftshader (or --use-angle=swiftshader) instead.
  • Chrome enforces a ~500px minimum window width (old AND new headless): a --window-size=390,... shot silently renders the layout at 500px and crops the PNG — the right edge looks clipped when the CSS is actually fine. For true phone renders, load the page in a <iframe style="width:390px;height:844px"> harness (needs --allow-file-access-from-files) and crop the iframe region.
  • Layout doubt ≠ screenshot truth: when a shot contradicts the CSS, inject a debug <script> logging getComputedStyle + getBoundingClientRect + innerWidth into a COPY of the page and read the console — measure before churning on "fixes".
  • Scrolled states need CDP, not --screenshot: once the page scrolls (URL fragments, anchor links, scroll-linked effects), Chrome's --screenshot CLI can return a fully blank capture even though layout and paint are fine. Drive Chrome over the DevTools Protocol instead (--remote-debugging-port, Target.createTarget → Runtime.evaluate to scroll/measure → Page.captureScreenshot), and use Emulation.setDeviceMetricsOverride for true phone viewports (also bypasses the 500px window minimum). Measure scrollY/rects in the same session so you never debug a tooling artifact as a layout bug.
  • Interactive states get forcing params: any state reached only by user input (scroll progress, a focused gallery card, a finished morph) gets a query param that pins it (?p=0.5, ?focus=40) — then it's a plain --screenshot call. Verify the journey at several ?p keyframes, not just the landing frame.
  • State-class specificity trap: a state override like body.focused .disc{opacity:0} silently LOSES to the entrance rule body.ready .disc.enter{opacity:1} (more specific). The focused screenshot still shows the disc. Make state overrides win (!important or match specificity) and screenshot the state to prove it.
  • Hover states ARE screenshotable: make a debug copy with sed 's/:hover/.dbg/g' page.html > dbg.html, append a script adding .dbg to one nav link, the CTA, and the play disc, then screenshot in ?still mode. Verify the raised nav pill, the backlit under-glow, and the glass brightening — don't ship hover styling on faith.

Design-review pass (aesthetic + conversion — after the technical loop passes)

Rendering correctly is not the same as being designed well. Re-read the settled desktop still as a principal designer + CRO lead. Every item below has caused a real revision round; check all of them:

  1. Value scan test — in 5 seconds a stranger can answer: what is it, who is it for, what do I get? (badge = category, H1 = promise, subhead = concrete mechanics).
  2. Squint test — squint at the shot: the FIRST element that pops must be the primary CTA, the second the headline. If the CTA doesn't win, raise its contrast, not its size.
  3. Trust adjacency — risk-reversal ✓s touching the CTA row, ≥13px.
  4. No dead micro-text — any number or claim worth showing gets ≥20px value type in a glass chip; if it's not worth that size, cut it.
  5. Wordmark integrity — background brand text fully readable, partially occluded by the world (depth), never cropped by the viewport (gibberish).
  6. Light exists — the frame has visible light sources (atmo blooms + vignette), not one flat fill color.
  7. One winner per region — no two elements of equal visual weight competing in the same corner; mute or move the loser.
  8. Intro stopwatch — reload and count: settled ≤ ~2.5 s, and the scene is NEVER blank while UI is visible.
  9. Interaction audit — click every link and button in the real browser: correct destinations, demo plays with sound, Esc closes, labels honest (durations, stats). Then HOVER everything: every interactive element must respond as a lit object (sheen, cursor light, eased lift — see §Fluid UI). If buttons only change color, the page reads as "basic three.js demo", not the reference.
  10. AI-slop lint — reject the page if it contains the tells every reader now recognizes as generated: Inter/system-ui as the display face, blue→purple gradient headline text, glassmorphism cards floating on a purple gradient, emoji-bulleted feature lists, three equal feature cards with icon-title-blurb, every radius 12px, ease .2s everywhere. Each is fine deliberately; together they read as "nobody designed this". The demos here use editorial serifs, ink palettes, and asymmetric collages precisely to stay out of that basin.
  11. Then open page.html for motion feel — easing, flap speed, parallax amplitude are judgment calls headless can't make.

Runtime tooling (scripts/ — the executable half of the review passes)

Three zero-dependency tools ship next to this file (Node ≥22 + any Chrome):

  • node scripts/study.mjs <url> — the §Study-a-reference workflow made executable: storyboard at five scroll depths (desktop + phone), pixel-diff probes for idle / pointer / drag / hover / scroll / wheel reactivity, palette + type extraction, bundle grep for tech signals, recipe mapping with confidence, and a build-spec prompt at the bottom of study/<host>/spec.md. Run it before the intake whenever a reference URL is in play; the human reads the storyboard, the agent reads the spec.
  • node scripts/audit.mjs page.html — headless design lint: ~22 rules across desktop/tablet/phone covering the AI-slop lint, easing grammar, reduced-motion, the stagger-delay trap, console errors, blank-frame/canvas-alive pixel checks, rendered text contrast, phone overflow, tap targets, ?still=1 determinism, and a11y names. FAIL = ship blocker (exit 1 — wire it into CI as a pre-deploy gate); WARN = judgment call to carry into the human design-review read. --json for machine output, --viewport=phone to scope. Run it after the self-verify loop passes and again before shipping — screenshots prove it renders; the audit proves nobody will clock it as generated.
  • scripts/picker.js — live "dial mode": the human opens the page in a real browser, pastes this file into the DevTools console, clicks any element, and gets a precise context block (selector + markup + computed styles) on the clipboard to paste back to you. Treat that block as authoritative targeting — no guessing which element they meant.

Command vocabulary — when the user says one of these words, it maps to a specific pass, not a vibe:

  • study <url> / "clone the feel of " → run scripts/study.mjs, read the storyboard + spec, confirm the recipe mapping with the user (one line), then build from the spec's prompt themed to their brand.
  • audit → run scripts/audit.mjs, fix every FAIL, then walk the §Design-review pass and report both.
  • variants / "3 directions" → §Intake rule: three genuinely distinct directions (archetype + palette + type + motion rhythm), compact diffs, wait for the pick, apply only that one, re-audit.
  • typeset → type-only pass: display face, scale, tracking, measure, micro-text cull — no layout changes.
  • distill → reduce: cut the weakest chips/cards/labels until every region has one winner (§Design-review #7), no new elements.
  • bolder → raise contrast of hierarchy (scale jump, color pop, motion accent on the ONE hero moment) without adding elements.

Ship checklist (mounting into a real webapp)

  • node scripts/audit.mjs page.html exits 0 (no FAILs on any viewport).
  • Copy vendor/three.module.js + media into the app's static dir (public/); keep the local-first import path valid from the mounted route.
  • Links: relative, pointing at anchor ids/routes that exist in the app (grep them).
  • Meta: <title>, meta description, OG/Twitter image, favicon (inherit the app's).
  • A11y: aria-label on icon-only buttons, Esc/backdrop close on modals, visible focus, prefers-reduced-motion handled, text contrast ≥ 4.5:1 over the world (text-shadow counts).
  • Perf sanity: pixelRatio clamped, no shadow maps, instancing for repeats, video preload="metadata" — then Lighthouse the mounted route.
  • Media has CONTENT, not just playback: a UI probe proves the player runs, not that anything is audible/visible. Before shipping audio/video, verify the asset itself: ffmpeg -i f -af volumedetect -f null /dev/null (max_volume ≈ -91dB = digital silence), and Range-request the deployed URL (expect 206). ffmpeg trap that caused this: -ss AFTER -i keeps source timestamps, so time-anchored filters (afade st=…) act on the original timeline and can fade the whole clip to zero — put -ss BEFORE -i (resets pts to 0) when combining trims with timed filters.

Theming a brand

Pick ONE world hue from the brand palette, mute it hard for bg/fog; keep brand accents for glows, pulses, CTA. Keep marketing copy truthful — pull claims from the brand's real site, never invent stats. Light-theme brands: pale "dawn fog" world, or ship the hero deliberately as a premium dark section.

Prompt vocabulary that maps to this skill

"orbit using mouse" → §orbit · "add particles to pointer" → §particles · "wireframe intro like Death Stranding" → §scan intro · "scan effect on card images while they load" → §scan-line reveal · "butterfly that lands and flies away on mouse-over" → §creature · "3D product page like AETHER / glass earbuds hero" → §glass product stage · "gallery dome / sphere of screenshots like OpenPurpose" → §dome media gallery · "scroll tells the story like Igloo Inc" / "camera walks through the scene" → §scroll-driven camera journey · "particles form a shape / logo / creature" → §particle shape morph · "image changes as the mouse moves over it like Lando Norris" → §cursor mask reveal · "liquid glass" / "ripple distortion over the headline" → §liquid-glass ripple typography · "draggable poster wall" / "posters that flex like paper" → §springy poster wall · "images follow the mouse" / "photos appear where the cursor goes" → §cursor-trail image reveal · "scrolls sideways" / "horizontal chapters" / "scroll-snap story" → §horizontal scroll-snap story · "control it with hand gestures" → §gesture control · "recreate this in a single HTML file, self-verify until perfect" → the whole recipe · "show me 3 directions" → §Intake (three genuinely distinct art directions, then build the picked one).

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

and 20 more files.

Frequently asked questions

What does the Motion Pages AI skill do?

Build production-ready immersive motion pages in the award-site mold — Three.js worlds (foggy heroes, glass product stages with sonar rings, drag-orbit dome galleries, scroll-driven camera journeys with particle morphs, walkable procedural game worlds with HUD chrome) AND non-Three.js motion (raw-WebGL liquid-glass ripple typography, springy draggable poster walls, cursor-trail image reveals, horizontal scroll-snap stories, scroll-scrubbed process reveals, cursor mask reveals, gesture control) — each a single HTML file with a crisp DOM overlay, responsive to phone/tablet, touch-aware, with...

Why use Motion Pages on TypingMind?

Because you install it once and use it with any model. Motion Pages is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Motion Pages in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/jiangyurong609/motion-pages/tree/main. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Motion Pages?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Motion Pages?

As many as you like. As long as a model supports skills, you can use Motion Pages with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Motion Pages AI skill free?

Yes. It is published on GitHub by jiangyurong609 under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇