diff --git a/CLAUDE.global.md b/CLAUDE.global.md index aa5bb4d..d2b8d97 100644 --- a/CLAUDE.global.md +++ b/CLAUDE.global.md @@ -269,7 +269,8 @@ design routing; the design-toolchain hook reinforces it. - Trivial (≤2 files, one cosmetic value) → /hotfix, no toolchain. - Build UI (component, page, redesign) → ui-ux-pro-max + frontend-design (anti-slop) + 21st-ui-build (catalog + generation) + emil-design-eng - (polish) + design-motion-principles (motion) + design-html (static). + (polish) + design-motion-principles (motion) + design-html (static) + + site-motion (site-level scroll/page choreography, personal skill). Post-build floor when impeccable is installed: `npx impeccable detect ` (45 deterministic anti-slop rules, exit 2 = findings). - Design system / brand → design-consultation first, then the build tools. diff --git a/lib/design-gate.md b/lib/design-gate.md index ed385af..e6dd5bd 100644 --- a/lib/design-gate.md +++ b/lib/design-gate.md @@ -47,6 +47,9 @@ browser/plan/shotgun tooling and graphify for convenience; those never trip the gate. Motion (`design-motion-principles`) and static-HTML (`design-html`) are already in the core set — checked regardless; their CLAUDE.md "+motion / +static" notes say which tool you'll lean on, not a separate activation step. +`site-motion` (personal skill, site-level scroll/page choreography) rides the +same Build chain but isn't on the GATE-BLOCK list: it ships with the repo, +nothing to install or verify. ### 2. State — run the deterministic check diff --git a/skills/site-motion/SKILL.md b/skills/site-motion/SKILL.md new file mode 100644 index 0000000..caa80b3 --- /dev/null +++ b/skills/site-motion/SKILL.md @@ -0,0 +1,185 @@ +--- +name: site-motion +description: | + Site-level motion choreography: scroll engine choice, page-transition + rules, and pin/scrub sequencing across a whole page or Astro route — + not one component's hover or enter/exit (that's design-motion-principles + or emil-design-eng). Distills the invariants behind smooth scroll, + scroll storytelling, sticky card stacks, video/image scrubbing, and + WebGL hero lanes into gates, numbers, and pitfalls. + Triggers: "site mouvementé", "scroll storytelling", "smooth scroll", + "hero WebGL", "transitions de page", "page transitions", "Lenis", + "ScrollTrigger", "Awwwards". +argument-hint: +allowed-tools: + - Read + - Edit + - Write + - Bash + - Grep + - Glob +--- + +# Site motion — page-level scroll and transition choreography + +Invariants, not machinery: numbers and gates that hold across whichever +scroll library the project already runs (LRN-141). Defaults follow +`rules/web-building.md`; this skill only adds the site-level layer on +top of it. + +## When to use this, not the component skills + +- One component's hover, tap feedback, or enter/exit → the target feels + small and self-contained → `skills-external/design-motion-principles/SKILL.md` + (component motion, frequency/duration framework) or + `skills-external/emil-design-eng/SKILL.md` (taste, polish). +- The choice is page-wide: which scroll engine, whether to pin a section, + how a route transition should morph, how a WebGL hero should degrade → + this skill. +- Linting already-shipped motion against anti-slop rules → + `skills/impeccable/reference/animate.md` (`impeccable detect`) as the + deterministic floor, or design-motion-principles' own audit workflow + (`skills-external/design-motion-principles/workflows/audit.md`). +- Non-motion defaults (fonts, color, spacing, the public-site checklist) + stay in `rules/web-building.md` — read it, don't restate it here. + +## Gates first (fail closed, not invisible) + +- Under `prefers-reduced-motion: reduce`, every animation renders its + FINAL state, never a shortened version of the same tween — jump, don't + rush. +- Content is visible with JavaScript disabled; no permanent + `opacity: 0` gated only by a script that might fail to run. +- Any `html.js` (or `.has-motion`) class that hides the pre-animation + state is added only AFTER `gsap.registerPlugin(...)` and the reveal + setup both succeed — never before. An error between the two otherwise + leaves real content stuck invisible with no JS path left to reveal it. +- Animate compositor-only properties: `transform`, `opacity`, short-lived + `filter`/`clip-path`. Never a layout property during scroll. +- `will-change` only while an element is actively animating; drop it + once the animation ends. +- Every RAF loop, CSS animation, and WebGL render loop pauses when its + section leaves the viewport and resumes on re-entry. +- The first-viewport CTA is never covered by a preloader. +- No preloader on a fixed timer — tie its exit to real load state. + +## Engine choice + +- Exactly one smooth-scroll engine per page. A second scroller (native + plus Lenis, or two libraries) fights the first over wheel/touch input + and desyncs from anything watching scroll position. +- Lenis feeds `ScrollTrigger` through `gsap.ticker`, with + `gsap.ticker.lagSmoothing(0)` — without it, a tab-switch catch-up jump + throws scrub position out of sync with the visuals. +- Reach for CSS `animation-timeline: scroll()` / `view()` first when the + effect is a plain progress mapping with no pin and no cross-timeline + coordination. GSAP/Lenis earn their cost on pin, scrub-linked + sequencing, or a timeline shared across sections (BDR-005: `motion` is + the default library; GSAP is allowed once the project already uses it + or the effect needs pin/scrub). + +## Astro lifecycle (ClientRouter) + +Route swaps replace the DOM without a full reload, so `DOMContentLoaded` +fires once and never again. + +- Init scroll engines, `ScrollTrigger` instances, and RAF loops on + `astro:page-load` — it fires after every swap, including the first. +- Teardown on `astro:before-swap`: kill ScrollTriggers, stop the + scroller, cancel RAF, disconnect observers, before the old DOM is + replaced — otherwise the previous route's loop keeps running detached. +- `transition:persist` on a WebGL canvas or renderer that should survive + the swap instead of losing its context every navigation; pair it with + hooks that update the scene, not rebuild it. +- `transition:name` for element morphs (hero image to detail image) + across routes; leave unrelated elements unnamed to avoid accidental + cross-fades. +- Test every scene with JavaScript on and off — without it, the + ClientRouter falls back to a normal navigation and the page still has + to make sense. + +## Recipes (the numbers) + +- Reveal: trigger at `top 82%`, once; ease the entrance tween itself, + never the trigger point. +- Scrub scenes: `scrub: 0.8` to `1.4`, `ease: "none"` on the + scroll-driven tween — ease the child tweens inside it instead, so the + outer timeline stays scroll-linear while the content still feels eased. +- Sticky card stack: the receding card scales to `0.92 + i * 0.015` + (`i` = card index), scrubbed from the next card crossing `top 78%` to + `top 24%`. +- Story pacing: budget `0.7` to `1.8` viewport heights of scroll per + story beat — below that it reads as a flicker, above it as a stall. +- Video scrub: encode with + `ffmpeg -g 8 -keyint_min 8 -sc_threshold 0 -movflags +faststart` — a + keyframe every 8 frames so scrubbed `currentTime` seeks land on-frame. +- Image sequences: preload the current frame first, prefetch neighbors, + and cancel stale in-flight requests on a fast scroll so a slow response + can't paint an out-of-order frame. +- Word-level reveal on marked-up text (links, `em`, `strong` inside): + walk it with `TreeWalker`, wrap non-whitespace tokens in spans, keep + the original text and its inline markup intact. +- Progressive blur: stack `backdrop-filter` layers from `0.5px` to + `64px`, each masked to a `12.5%` band of the gradient; add the + `-webkit-backdrop-filter` prefix for Safari; cap the band at `12%` of + viewport height from the top edge, `65%` from the bottom. +- Marquee: duplicate the track, animate `translateX(-50%)` linear, mark + the duplicate `aria-hidden`, pause the track while its section is + offscreen. +- Magnetic and cursor motion: drive with `gsap.quickTo()` so a pointer + move updates the existing tween instead of creating a new one per + event. +- WebGL hero, one lane per page: + - A pricing or checkout page keeps the WebGL lane decorative only. + - Build the poster fallback first, enhance after — the poster is the + page when WebGL fails or the tab throttles. + - Handle `webglcontextlost`/`webglcontextrestored` explicitly. + - Mobile budget: DPR `1.25` to `1.5`, `150k` to `300k` visible + triangles, `50` to `90` draw calls. + - Track two progress values: exact scroll progress for navigation and + ARIA state, a damped one for the camera — the camera can lag, the + nav state cannot. +- Leak census: sample `document.getAnimations()` before and after a + route round-trip. A count that grows across `astro:page-load` cycles + means a teardown is missing, not that more is animating. + +## Upstream pitfalls + +- `clearProps` combined with a visibility override in the same + reduced-motion call clears that override before the CSS-hidden class + it was meant to defeat ever lifts — text stays hidden. Clear props or + drop the hiding class; don't do both in one step. +- `registerPlugin` running after the `html.js` gate is already set: see + Gates above, this is the concrete failure it prevents. +- A per-frame increment (`phi += 0.01`) with no delta-time factor ties + rotation speed to frame rate — the same globe spins at a different + real-world speed on a 30 Hz and a 120 Hz display. +- A WebGL context torn down and rebuilt on every `resize` event: expensive, + and rapid resizing (mobile keyboard, orientation flicker) can trigger a + real context loss. Resize the renderer/camera in place; debounce first. +- A preloader gated on a fixed timer exits early on a slow connection and + late on a fast one; gate it on real asset load state instead. +- `aria-label` set on a `

` after flattening it to plain text: any + inline link, `em`, or `strong` it contained is gone for assistive tech, + which now hears the label instead of the real content. Use `TreeWalker` + splitting on paragraphs with inline markup; `aria-label` is fine only on + a plain-text heading with nothing inside to lose. +- Critical CSS starting elements at `opacity: 0` with no fallback: if the + script errors, is blocked, or never loads, the section stays invisible + forever. Pair every such rule with the gates above. + +## Verification checklist + +- Reload each scene with reduced motion forced on: final states, no + smooth scroll, no pinning. +- Disable JavaScript: every section is present and readable in order. +- Scrub through fast and reversed: same scroll position always yields + the same visual state. +- Resize mid-scene, including orientation change on a real WebGL scene. +- Navigate the Astro route twice: `document.getAnimations()` count + returns to baseline, no duplicate ScrollTriggers, no orphaned RAF loop. +- Throttle to a slow connection: preloader and poster still make sense, + CTA is reachable immediately. +- Tab away mid-scrub, come back: no jump beyond what `lagSmoothing(0)` + already accounts for. +- Run `impeccable detect` on the touched files as the anti-slop floor. diff --git a/skills/site-motion/test-prompts.json b/skills/site-motion/test-prompts.json new file mode 100644 index 0000000..129c4ff --- /dev/null +++ b/skills/site-motion/test-prompts.json @@ -0,0 +1,6 @@ +[ + {"id": 1, "prompt": "Build a scroll storytelling landing page with Lenis smooth scroll and a sticky project card stack", "expected": "Route to site-motion: one smooth-scroll engine, Lenis->ScrollTrigger sync via gsap.ticker + lagSmoothing(0), sticky stack scale 0.92+i*0.015 recipe, reduced-motion gate"}, + {"id": 2, "prompt": "Ajoute des transitions de page fluides avec un hero WebGL qui doit survivre à la navigation", "expected": "Route to site-motion: Astro ClientRouter lifecycle, astro:page-load/astro:before-swap, transition:persist on the canvas, WebGL poster fallback + context-loss handling"}, + {"id": 3, "prompt": "Add a hover scale effect and a fade-in on this pricing card component", "expected": "Component-level, not site-motion: route to design-motion-principles or emil-design-eng instead"}, + {"id": 4, "prompt": "Make our site feel like an Awwwards ScrollTrigger showcase, with a scrubbed video sequence", "expected": "Route to site-motion: scrub 0.8-1.4 with ease none + eased children, ffmpeg -g 8 -keyint_min 8 encoding recipe, offscreen-pause and reduced-motion gates before any pin/scrub work"} +]