forked from bchanot/claude
- lib/effort-pins.txt (map) + lib/effort-pins.sh (idempotent re-apply) replace the hardcoded brainstorming/writing-plans loop; called after the last vendoring step of install-plugins.sh AND update-all.sh (the resync dropped the pins until the next make plugin) - design stack high uniform (last loaded wins), superpowers, agent-skills, 21st pack pinned from the map; skills-perso low, pdf-translate medium, site-motion high - doctrine: design stack loads paired with the first Read; one level per stack (CLAUDE.global.md, lib/effort-shift.md) - lib/effort-audit.py prints thinking coverage per scope (sub-agent records carry no thinking count on ~94 % of requests) - census map-driven + fixture suite lib/tests/effort-pins.test.sh; docs README/USAGE/CHANGELOG; contract + TODO plan
187 lines
9.4 KiB
Markdown
187 lines
9.4 KiB
Markdown
---
|
|
name: site-motion
|
|
effort: high
|
|
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: <page, section or route to choreograph>
|
|
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 `<p>` 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.
|