sheleg-design-skill 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ssheleg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # SHELEG Design — agent skill
2
+
3
+ > A motion + particle interface methodology for building cinematic,
4
+ > scroll-driven landing pages — packaged as an installable agent skill for
5
+ > Cursor and Claude.
6
+
7
+ Install it into any project with one command:
8
+
9
+ ```bash
10
+ npx sheleg-design-skill
11
+ ```
12
+
13
+ That drops a `SKILL.md` + `SHELEG_DESIGN.md` bundle into your project so your
14
+ coding agent can discover the skill and build sites on its principles.
15
+
16
+ ## What is SHELEG Design?
17
+
18
+ A page feels *alive* not from many animations, but from a **single source of
19
+ truth** (scroll position) driving **many cheap, layered responses** that are
20
+ individually quiet and collectively cinematic. One scroll "clock" feeds a WebGL
21
+ particle field, a 2D fallback, attention dimming, parallax, scrubbed
22
+ instruments, and a progress rail — each an independent, degrade-to-calm layer.
23
+ Nothing crossfades; things *redeploy*. The result reads as one precision
24
+ instrument responding to your hand.
25
+
26
+ It was reverse-engineered from a production landing page (a 14-scene particle
27
+ narrative that morphs through formations and culminates in a brand "N" that
28
+ charges and bursts). The skill distills the architecture and the principles so
29
+ an agent can rebuild that level on a new site.
30
+
31
+ ### The five principles
32
+
33
+ 1. **One clock.** All motion derives from one measured scroll state.
34
+ 2. **Read per frame, notify rarely.** Hot consumers read imperatively; only
35
+ coarse changes hit the framework's render path.
36
+ 3. **Hold, then redeploy.** Hold a formation ~80% of a section, then morph in a
37
+ short, phase-staggered, arc-curved wave. No crossfades.
38
+ 4. **Earned motion.** Scrub is for instruments that narrate state over time;
39
+ entrance motion stays sub-500ms and never gates content.
40
+ 5. **Degrade to calm.** Reduced-motion / coarse pointer / no-WebGL collapse to a
41
+ static, fully-legible page.
42
+
43
+ ## Usage
44
+
45
+ ```bash
46
+ # Auto-detect (.cursor/ or .claude/), default to .cursor/skills/sheleg-design/
47
+ npx sheleg-design-skill
48
+
49
+ # Force a flavor
50
+ npx sheleg-design-skill --cursor
51
+ npx sheleg-design-skill --claude
52
+
53
+ # Custom location
54
+ npx sheleg-design-skill --dir docs/skills/sheleg-design
55
+
56
+ # Overwrite an existing install
57
+ npx sheleg-design-skill --force
58
+
59
+ # Help
60
+ npx sheleg-design-skill --help
61
+ ```
62
+
63
+ ### What gets installed
64
+
65
+ | File | Purpose |
66
+ |---|---|
67
+ | `SKILL.md` | Agent-facing skill: discovery trigger + the principles + how to apply them |
68
+ | `SHELEG_DESIGN.md` | The full reference: architecture, layer-by-layer mechanics with code, the exact morph math, the DOM↔WebGL projection bridge, a build-from-scratch recipe, and the "why it works" |
69
+
70
+ After installing, a Cursor or Claude agent in that project can discover the
71
+ skill and use it when you ask it to build or upgrade a cinematic,
72
+ scroll-driven, particle-backed page.
73
+
74
+ ## Stack-agnostic
75
+
76
+ The skill teaches **principles and architecture**, not a fixed dependency set.
77
+ The reference implementation happens to use Next.js + React + three /
78
+ react-three-fiber + GSAP ScrollTrigger + Lenis + Framer Motion, but the method
79
+ applies to any stack that can render to a canvas/WebGL surface and read scroll.
80
+
81
+ ## Zero dependencies
82
+
83
+ The installer is a single zero-dependency Node script, so `npx` runs instantly
84
+ with no install step and no supply-chain surface.
85
+
86
+ ## License
87
+
88
+ MIT © ssheleg
package/bin/cli.js ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * sheleg-design-skill installer.
6
+ *
7
+ * Copies the SHELEG Design skill bundle (SKILL.md + SHELEG_DESIGN.md) into the
8
+ * current project so a Cursor / Claude agent can discover and apply it.
9
+ *
10
+ * Zero dependencies on purpose: it must run instantly via `npx` with no install
11
+ * step and no supply-chain surface.
12
+ */
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+
17
+ const SKILL_DIR = path.join(__dirname, "..", "skill");
18
+ const SKILL_SLUG = "sheleg-design";
19
+ const FILES = ["SKILL.md", "SHELEG_DESIGN.md"];
20
+
21
+ const pkg = require(path.join(__dirname, "..", "package.json"));
22
+
23
+ const COLORS = {
24
+ reset: "\x1b[0m",
25
+ bold: "\x1b[1m",
26
+ dim: "\x1b[2m",
27
+ blue: "\x1b[38;5;75m",
28
+ green: "\x1b[38;5;42m",
29
+ yellow: "\x1b[38;5;214m",
30
+ };
31
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
32
+ const c = (color, s) => (useColor ? COLORS[color] + s + COLORS.reset : s);
33
+
34
+ function parseArgs(argv) {
35
+ const opts = {
36
+ target: null, // explicit --dir
37
+ flavor: null, // "cursor" | "claude"
38
+ force: false,
39
+ help: false,
40
+ version: false,
41
+ };
42
+ for (let i = 0; i < argv.length; i++) {
43
+ const a = argv[i];
44
+ if (a === "--help" || a === "-h") opts.help = true;
45
+ else if (a === "--version" || a === "-v") opts.version = true;
46
+ else if (a === "--force" || a === "-f") opts.force = true;
47
+ else if (a === "--cursor") opts.flavor = "cursor";
48
+ else if (a === "--claude") opts.flavor = "claude";
49
+ else if (a === "--dir") opts.target = argv[++i];
50
+ else if (a.startsWith("--dir=")) opts.target = a.slice("--dir=".length);
51
+ else {
52
+ console.error(c("yellow", `Unknown argument: ${a}`));
53
+ opts.help = true;
54
+ }
55
+ }
56
+ return opts;
57
+ }
58
+
59
+ function printHelp() {
60
+ console.log(`
61
+ ${c("bold", "SHELEG Design")} ${c("dim", "v" + pkg.version)}
62
+ Install the SHELEG Design skill into your project.
63
+
64
+ ${c("bold", "Usage")}
65
+ npx sheleg-design-skill [options]
66
+
67
+ ${c("bold", "Options")}
68
+ --cursor Install to .cursor/skills/${SKILL_SLUG}/
69
+ --claude Install to .claude/skills/${SKILL_SLUG}/
70
+ --dir <path> Install to a custom directory
71
+ --force, -f Overwrite existing files
72
+ --help, -h Show this help
73
+ --version, -v Show version
74
+
75
+ ${c("bold", "Default")}
76
+ Auto-detects: uses .cursor/ if present, else .claude/ if present,
77
+ otherwise creates .cursor/skills/${SKILL_SLUG}/.
78
+
79
+ ${c("bold", "What it installs")}
80
+ SKILL.md the agent-facing skill (discovery + principles)
81
+ SHELEG_DESIGN.md the full reference (architecture, recipes, why it works)
82
+ `);
83
+ }
84
+
85
+ function resolveTargetDir(opts, cwd) {
86
+ if (opts.target) return path.resolve(cwd, opts.target);
87
+ if (opts.flavor === "cursor")
88
+ return path.join(cwd, ".cursor", "skills", SKILL_SLUG);
89
+ if (opts.flavor === "claude")
90
+ return path.join(cwd, ".claude", "skills", SKILL_SLUG);
91
+
92
+ const hasCursor = fs.existsSync(path.join(cwd, ".cursor"));
93
+ const hasClaude = fs.existsSync(path.join(cwd, ".claude"));
94
+ if (hasCursor) return path.join(cwd, ".cursor", "skills", SKILL_SLUG);
95
+ if (hasClaude) return path.join(cwd, ".claude", "skills", SKILL_SLUG);
96
+ return path.join(cwd, ".cursor", "skills", SKILL_SLUG);
97
+ }
98
+
99
+ function main() {
100
+ const opts = parseArgs(process.argv.slice(2));
101
+
102
+ if (opts.version) {
103
+ console.log(pkg.version);
104
+ return;
105
+ }
106
+ if (opts.help) {
107
+ printHelp();
108
+ return;
109
+ }
110
+
111
+ const cwd = process.cwd();
112
+ const targetDir = resolveTargetDir(opts, cwd);
113
+
114
+ // Verify the bundle is intact before touching the filesystem.
115
+ for (const f of FILES) {
116
+ if (!fs.existsSync(path.join(SKILL_DIR, f))) {
117
+ console.error(
118
+ c("yellow", `Bundle is missing ${f}. This is a packaging bug.`),
119
+ );
120
+ process.exit(1);
121
+ }
122
+ }
123
+
124
+ const existing = FILES.filter((f) =>
125
+ fs.existsSync(path.join(targetDir, f)),
126
+ );
127
+ if (existing.length && !opts.force) {
128
+ console.error(
129
+ `\n${c("yellow", "Refusing to overwrite existing files:")}\n` +
130
+ existing.map((f) => " " + path.join(targetDir, f)).join("\n") +
131
+ `\n\nRe-run with ${c("bold", "--force")} to overwrite.\n`,
132
+ );
133
+ process.exit(1);
134
+ }
135
+
136
+ fs.mkdirSync(targetDir, { recursive: true });
137
+ for (const f of FILES) {
138
+ fs.copyFileSync(path.join(SKILL_DIR, f), path.join(targetDir, f));
139
+ }
140
+
141
+ const rel = path.relative(cwd, targetDir) || ".";
142
+ console.log(
143
+ `\n${c("green", "✓")} ${c("bold", "SHELEG Design")} installed to ${c("blue", rel + "/")}\n` +
144
+ ` ${c("dim", "SKILL.md")} the agent skill\n` +
145
+ ` ${c("dim", "SHELEG_DESIGN.md")} the full reference\n\n` +
146
+ `Your Cursor / Claude agent can now discover the skill and build\n` +
147
+ `cinematic, scroll-driven, particle-backed pages on its principles.\n\n` +
148
+ `${c("dim", "Docs: " + pkg.homepage)}\n`,
149
+ );
150
+ }
151
+
152
+ main();
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "sheleg-design-skill",
3
+ "version": "0.1.0",
4
+ "description": "SHELEG Design — an agent skill for building cinematic, scroll-driven, particle-backed landing pages. Installs a SKILL.md + reference doc into your project so Cursor/Claude agents can build sites with a single-clock motion system, a scene-formation particle engine, and degrade-to-calm fallbacks.",
5
+ "bin": {
6
+ "sheleg-design-skill": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "skill/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "type": "commonjs",
15
+ "engines": {
16
+ "node": ">=16"
17
+ },
18
+ "scripts": {
19
+ "test": "node bin/cli.js --help"
20
+ },
21
+ "keywords": [
22
+ "skill",
23
+ "cursor",
24
+ "claude",
25
+ "agent-skill",
26
+ "design-system",
27
+ "motion",
28
+ "animation",
29
+ "particles",
30
+ "webgl",
31
+ "scroll",
32
+ "gsap",
33
+ "three",
34
+ "framer-motion",
35
+ "landing-page",
36
+ "sheleg"
37
+ ],
38
+ "author": "ssheleg",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/ssheleg/sheleg-design-skill.git"
43
+ },
44
+ "homepage": "https://github.com/ssheleg/sheleg-design-skill#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/ssheleg/sheleg-design-skill/issues"
47
+ }
48
+ }
@@ -0,0 +1,643 @@
1
+ # SHELEG Design
2
+
3
+ A motion-and-particle interface system, reverse-engineered from the Nicegram
4
+ Business OS landing page. This document captures *how* the page reaches its
5
+ level — the layout discipline, the scroll narrative, the WebGL particle field,
6
+ the DOM choreography, and the registration tricks that fuse them — so you can
7
+ build new sites on the same principles and understand *why* each piece works.
8
+
9
+ > SHELEG Design is the **motion + systems** layer. It assumes a **visual**
10
+ > system already exists (color, type, elevation, components — the reference
11
+ > implementation calls its visual layer "The Instrument Console": a near-black
12
+ > aerospace console with one electric-blue signal accent). Build the visual
13
+ > system first; layer SHELEG Design on top.
14
+
15
+ ---
16
+
17
+ ## 0. The one-paragraph thesis
18
+
19
+ A landing page feels "alive" not because it has many animations, but because a
20
+ **single source of truth** (scroll position) drives **many cheap, layered
21
+ responses** that are individually quiet and collectively cinematic. SHELEG
22
+ Design centralizes scroll into one external store, then lets independent layers
23
+ (a WebGL particle field, a DOM spotlight, parallax figures, scrubbed
24
+ instruments, a progress rail) read that store **per frame** and react in their
25
+ own language. Every layer degrades to a calm static state. Nothing crossfades;
26
+ things *redeploy*. The result reads as one instrument responding to your hand,
27
+ not a pile of effects.
28
+
29
+ The five principles that make it work:
30
+
31
+ 1. **One clock.** All motion derives from a single measured scroll state. Layers
32
+ never measure scroll independently, so they can never disagree.
33
+ 2. **Read per frame, notify rarely.** Hot consumers (WebGL, canvas, rail) read
34
+ the store imperatively each frame and never trigger React renders. Only
35
+ coarse, human-visible changes (the current "act") notify React.
36
+ 3. **Hold, then redeploy.** A formation is held steady for most of a section,
37
+ then morphs in a short, phase-staggered, arc-curved wave — points fly to new
38
+ posts. Crossfades are banned.
39
+ 4. **Earned motion.** Scrubbed animation belongs to instruments that narrate
40
+ state over time. Hover/entrance motion is sub-500ms and never gates content.
41
+ 5. **Degrade to calm.** `prefers-reduced-motion` / coarse pointer / no-WebGL all
42
+ collapse to a static, fully-legible page. The effect is an enhancement, never
43
+ a dependency.
44
+
45
+ ---
46
+
47
+ ## 1. Architecture at a glance
48
+
49
+ ```
50
+ ┌─────────────────────────────┐
51
+ wheel / touch ─────▶ │ Lenis smooth scroll │ (SmoothScroll.tsx)
52
+ │ driven by the GSAP ticker │
53
+ └──────────────┬───────────────┘
54
+ │ one scroll position / frame
55
+
56
+ ┌─────────────────────────────┐
57
+ │ Scroll store (single clock) │ (scroll-progress.ts)
58
+ │ global · act · form · focusX │
59
+ │ velocity · finale │
60
+ └──────────────┬───────────────┘
61
+ per-frame reads (no React) │ coarse "act" change → React
62
+ ┌───────────────┬───────────────┼───────────────┬──────────────┐
63
+ ▼ ▼ ▼ ▼ ▼
64
+ SignalField SignalMesh FocalSpotlight ParallaxDrift ScrollRail
65
+ (WebGL field) (2D fallback) (dim off-band) (figure depth) (progress)
66
+
67
+ │ projects anchor points / reads chart progress
68
+
69
+ field-sync bridge ──▶ ConstellationOverlay (SVG drawn over clusters)
70
+ ◀── WhyNowChart scrub (particles assemble with the line)
71
+ ```
72
+
73
+ Layers are **independent**. You can delete `ParallaxDrift` or the overlay and
74
+ everything else still works. That decoupling is the point: each layer is a small
75
+ file with one responsibility, reading the same clock.
76
+
77
+ ---
78
+
79
+ ## 2. Layer 1 — The scroll store (the single clock)
80
+
81
+ **File:** `src/lib/motion/scroll-progress.ts`
82
+
83
+ This is the heart of the system. One `requestAnimationFrame` loop (scheduled on
84
+ the native `scroll` event) measures the page and writes a plain `state` object.
85
+ Everything else reads it.
86
+
87
+ ### The dual-subscription pattern
88
+
89
+ The store exposes two read paths, which is what keeps it cheap:
90
+
91
+ - `getScrollState()` — returns the **live, mutable** object. Hot per-frame
92
+ consumers (WebGL `useFrame`, the canvas loop, the rail fill) read this and
93
+ never cause a React render.
94
+ - `subscribeAct()` + `useSyncExternalStore` — fires **only when the integer act
95
+ changes** (5 acts on the whole page). React components that show act-derived
96
+ UI (nav badge, rail markers) subscribe here, so they wake up a handful of
97
+ times per full scroll, not 60×/second.
98
+
99
+ ```ts
100
+ // Per-frame, no React (rail fill, WebGL, canvas):
101
+ const s = getScrollState();
102
+ el.style.transform = `scaleY(${s.global})`;
103
+
104
+ // Coarse, React-driven (nav badge "02 / CONTROL"):
105
+ const act = useSyncExternalStore(subscribeAct, () => getScrollState().act, () => 0);
106
+ ```
107
+
108
+ > **Why it works:** the expensive consumers are exactly the ones that must run
109
+ > every frame anyway (rendering), so reading a mutable object is free. The cheap
110
+ > consumers (text labels) are the only ones in React's render path, and they
111
+ > change rarely. You get 60fps motion with almost no React churn.
112
+
113
+ ### What the state carries
114
+
115
+ | Field | Range | Meaning | Who reads it |
116
+ |---|---|---|---|
117
+ | `global` | 0..1 | whole-page scroll progress | rail fill, camera dolly |
118
+ | `act` | 0..4 | current narrative act (integer) | nav badge, rail markers |
119
+ | `actProgress` | 0..1 | progress within the act | act-level copy |
120
+ | `field` | 0..4 | continuous act scalar | 2D mesh gain, climax |
121
+ | `scene` | 0..13 | current section's scene (integer) | scene lookup |
122
+ | `form` | 0..13 | **continuous scene scalar** | particle formation morph |
123
+ | `focusX` | -1..1 | camera pan target | WebGL camera, overlay |
124
+ | `velocity` | -1..1 | smoothed scroll speed | field breathing, rail |
125
+ | `finale` | 0..1 | post-waitlist epilogue progress | N charge → burst |
126
+ | `bias` | -0.6..1 | interactive nudge added to `form` | section gestures |
127
+
128
+ ### The "hold then morph" measurement (the calm secret)
129
+
130
+ A naïve scroll-to-formation mapping changes the backdrop continuously, which
131
+ reads as nervous. The fix is a **hold window**. Within a scene's scroll span,
132
+ `form` stays pinned to the scene's integer for the first `HOLD` (82%), then
133
+ eases through the tail via smoothstep:
134
+
135
+ ```ts
136
+ const sp = spanProgress(y, sceneFrom, sceneTo); // 0..1 within the scene
137
+ const HOLD = 0.82; // formation locked 82% of the span
138
+ const tail = clamp01((sp - HOLD) / (1 - HOLD)); // 0..1 only in the last 18%
139
+ const eased = tail * tail * (3 - 2 * tail); // smoothstep
140
+ state.form = scene + eased; // integer hold → eased morph at the seam
141
+ ```
142
+
143
+ Boundaries are measured when a section crosses **65% of the viewport** (`measure()`),
144
+ so a formation "opens" as its section becomes the reading focus. A
145
+ `ResizeObserver` on `document.body` re-measures when late layout (fonts, lazy
146
+ GSAP pin spacers) shifts the page — without it, every boundary below a pinned
147
+ section would be wrong.
148
+
149
+ ### Velocity as a feeling, not a value
150
+
151
+ `setScrollVelocity()` is written from Lenis on every scroll event and normalized
152
+ to roughly -1..1. It **does not notify React** — it is read per frame by the
153
+ field (particles breathe faster while you scroll) and the rail (fill brightens
154
+ with input speed). This is the cheapest possible "the instrument is responding
155
+ to you" signal.
156
+
157
+ ---
158
+
159
+ ## 3. Layer 2 — Smooth scroll (one position per frame)
160
+
161
+ **File:** `src/components/v2/motion/SmoothScroll.tsx`
162
+
163
+ Lenis provides inertial scrolling, but the important move is **driving Lenis
164
+ from the GSAP ticker** rather than its own rAF:
165
+
166
+ ```ts
167
+ const lenis = new Lenis({ lerp: 0.09, wheelMultiplier: 1, touchMultiplier: 1.4 });
168
+ gsap.ticker.add((time) => lenis.raf(time * 1000));
169
+ gsap.ticker.lagSmoothing(0);
170
+ lenis.on("scroll", (e) => { setScrollVelocity(e.velocity); ScrollTrigger.update(); });
171
+ ```
172
+
173
+ > **Why it works:** ScrollTrigger scrubs and the particle field both read scroll
174
+ > on the same tick, so a scrubbed chart and the background particles share one
175
+ > inertia — they move as a single instrument. A low `lerp` (0.09) means more
176
+ > glide between wheel notches, which is what lets formations morph as continuous
177
+ > motion instead of discrete jumps.
178
+
179
+ Reduced-motion / coarse-pointer clients skip Lenis entirely (`shouldReduceScenes()`)
180
+ and keep native scroll. The store still runs, so the rail and nav stay in sync.
181
+
182
+ ---
183
+
184
+ ## 4. Layer 3 — The particle field (the scene-formation engine)
185
+
186
+ **File:** `src/components/v2/webgl/SignalField.tsx`
187
+
188
+ This is the showpiece: ~936 points (`24 × 13 × 3`) that narrate the page section
189
+ by section. It is a single `THREE.Points` cloud whose target positions change
190
+ per scene. All motion is CPU positional lerp — cheap, deterministic, tinted only
191
+ with the brand accent.
192
+
193
+ ### 4.1 The scene registry
194
+
195
+ **File:** `src/lib/motion/scenes.ts`
196
+
197
+ Each section "owns" a scene; a scene is a `{ anchor, formation, focusX, energy }`
198
+ record. The registry is the single place that maps DOM sections to particle
199
+ behavior:
200
+
201
+ ```ts
202
+ export const SCENES = [
203
+ { anchor: "top", formation: "frame", focusX: -0.3, energy: 0.5 },
204
+ { anchor: "telemetry", formation: "curve", focusX: 0, energy: 0.45 },
205
+ { anchor: "problem", formation: "turbulence", focusX: -0.25, energy: 0.55 },
206
+ // … one per section …
207
+ { anchor: "why-now", formation: "gapCurve", focusX: 0, energy: 0.7 },
208
+ { anchor: "waitlist", formation: "glyphN", focusX: 0, energy: 1 },
209
+ ] as const;
210
+ ```
211
+
212
+ - `formation` — which shape the points fly into (frame, curve, lattice, triad,
213
+ orbit, constellation, ecoMap, gapCurve, glyphN, …).
214
+ - `focusX` — where the camera pans (opposite the content column, so the field
215
+ frames the text instead of fighting it).
216
+ - `energy` — the brightness budget for the scene (the **One Signal Rule**: the
217
+ glow never out-shouts an on-stage demo). `sceneEnergy(form)` interpolates it
218
+ continuously.
219
+
220
+ > **Why it works:** adding/reordering a scene is a one-line data edit. The field,
221
+ > the 2D fallback, and the overlay all resolve their behavior **from the registry
222
+ > by formation name** (`SCENES.findIndex(s => s.formation === "glyphN")`), so
223
+ > nothing hard-codes an index. The data is the choreography.
224
+
225
+ ### 4.2 Formation builders (deterministic geometry)
226
+
227
+ `buildFormations()` produces each formation's target positions once, at mount.
228
+ Every builder runs its **own seeded RNG** (`mulberry32(seed)`), so:
229
+
230
+ - formations are identical across sessions and machines (no hydration drift),
231
+ - they are independent of declaration order,
232
+ - the same point index can be given a meaningful role in multiple formations
233
+ (e.g. the "tight N" variant reuses the N's per-point stroke assignment so the
234
+ charge densification reads as the letter pulling itself sharp).
235
+
236
+ Example — the "constellation" formation pulls a relaxed grid toward 8 hubs with
237
+ tight jitter so clusters read as deliberate nodes:
238
+
239
+ ```ts
240
+ const pull = 0.68; // strong pull → hubs read as clusters
241
+ out[ix] = gx + (hub[0] - gx) * pull + (rand() - 0.5) * 1.1; // small jitter
242
+ out[ix+1] = gy + (hub[1] - gy) * pull + (rand() - 0.5) * 1.1;
243
+ ```
244
+
245
+ ### 4.3 The morph (hold → smoothstep stagger → arc)
246
+
247
+ Inside `useFrame`, the render-side scalar `s.form` chases the store's `form` with
248
+ **clamped critically-damped smoothing**, so flick-scrolls and anchor jumps sweep
249
+ through intermediate formations cinematically instead of teleporting:
250
+
251
+ ```ts
252
+ const formDelta = (scroll.form + scroll.bias - s.form) * 0.028;
253
+ s.form += Math.max(-0.04, Math.min(0.04, formDelta)); // per-frame delta cap
254
+ ```
255
+
256
+ Each point then blends between the two active formations with a **per-point
257
+ phase-staggered, smoothstepped** progress — points peel off in a wave and ease
258
+ in and out of their posts:
259
+
260
+ ```ts
261
+ const m = clamp01((mix - phaseNorm * spread) / (1 - spread)); // spread = 0.5
262
+ const eased = m * m * (3 - 2 * m); // smoothstep
263
+ ```
264
+
265
+ And mid-flight, points swing **perpendicular to their travel line** — half
266
+ clockwise, half counter — so the swarm curls between formations instead of
267
+ sliding in straight lines:
268
+
269
+ ```ts
270
+ if (m > 0.001 && m < 0.999) {
271
+ const arc = Math.sin(m * Math.PI) * arcAmp * (phase > Math.PI ? 1 : -1);
272
+ px += (-dy / dist) * arc; // perpendicular offset, peaks mid-transition
273
+ py += ( dx / dist) * arc;
274
+ }
275
+ ```
276
+
277
+ > **Why it works:** crossfades look like one image fading into another (flat,
278
+ > digital). A phase-staggered arc-curved redeploy looks like a *swarm of agents*
279
+ > flying to new posts (alive, physical). It is the single biggest reason the
280
+ > field reads as premium rather than as a screensaver.
281
+
282
+ ### 4.4 Formation profiles (the glow programs)
283
+
284
+ A `PROFILES` table gives each formation a render character — how lattice-like it
285
+ is (drives wire opacity), which glow program lights it, and how much ambient
286
+ drift it keeps:
287
+
288
+ ```ts
289
+ const PROFILES = {
290
+ lattice: { lattice: 1, glow: "wave", drift: 0.12 }, // engineered grid + wave pulse
291
+ constellation: { lattice: 0, glow: "hubs", drift: 0.25 }, // hub stars twinkle, no wires
292
+ beam: { lattice: 0, glow: "beam", drift: 0.2 }, // focused vertical signal
293
+ glyphN: { lattice: 0, glow: "glyph", drift: 0.1 }, // the finale "N"
294
+ // …
295
+ };
296
+ ```
297
+
298
+ Glow programs are blended between the two active formations, so transitions
299
+ carry their lighting with them. Profiles are interpolated, never switched hard.
300
+
301
+ ### 4.5 The camera
302
+
303
+ The camera does three quiet things, all smoothed:
304
+
305
+ - **Dolly** back as `global` increases (the page pulls away over its length).
306
+ - **Pan** toward the active scene's `focusX` (attention follows the content).
307
+ - **Tilt** a fraction toward the focus side — applied *after* `lookAt` (which
308
+ resets rotation), from its own smoothed value. This faint instrument tilt is
309
+ what gives the field a sense of being observed through a lens.
310
+
311
+ ### 4.6 The finale (charge → burst), scrub-driven
312
+
313
+ Past the waitlist, a dedicated `finale` scalar (0..1) drives an epilogue that is
314
+ **reversible** because it is pure scrub:
315
+
316
+ - **Charge** (`finale 0 → 0.55`): the N densifies toward a tight core, scales up,
317
+ and trembles with rising amplitude; brightness and point size climb.
318
+ - **Burst** (`finale 0.55 → 0.85`): every point flies out along a precomputed
319
+ radial debris vector with a white-hot flash, then dims into an ember field.
320
+ - The DOM **closing line** ("Join Nicegram") fades up from the afterglow.
321
+
322
+ Because it is gated on the glyph actually holding (`finaleness`) and reads a
323
+ scrubbed scalar, scrolling back up rewinds the whole explosion frame-for-frame.
324
+
325
+ ---
326
+
327
+ ## 5. Layer 4 — The 2D fallback (SignalMesh)
328
+
329
+ **File:** `src/components/v2/atmosphere/SignalMesh.tsx`
330
+
331
+ Touch and no-WebGL clients get a canvas mesh: two depth layers of grid nodes
332
+ with hairline links and traveling pulses, plus **stroke overlays** that fade in
333
+ the curve / orbit / N around their scene windows. It reads the **same store**
334
+ (`getScrollState().form`, `sceneEnergy`, `finale`), so the escalation and the
335
+ finale charge/burst are mirrored — the page tells the same story at lower
336
+ fidelity. Only `prefers-reduced-motion` gets a single static frame.
337
+
338
+ > **Principle:** the fallback is not a stripped logo — it reads the same clock
339
+ > and tells the same narrative. That is why switching modes never feels like a
340
+ > different website.
341
+
342
+ ---
343
+
344
+ ## 6. Layer 5 — The projection bridge (fusing DOM and WebGL)
345
+
346
+ **File:** `src/lib/motion/field-sync.ts`
347
+
348
+ The hardest problem in mixing WebGL particles with DOM labels: the camera moves,
349
+ so you cannot hard-code where a cluster appears on screen. The bridge solves it
350
+ with a tiny zero-allocation, no-React module that the field writes and the DOM
351
+ reads — the same contract as `setScrollVelocity`.
352
+
353
+ ### 6.1 Constellation registration (ecosystem section)
354
+
355
+ Every frame, `SignalField` projects the constellation's world-space anchors
356
+ through its **own camera** and publishes viewport-pixel coordinates + a
357
+ visibility alpha:
358
+
359
+ ```ts
360
+ // In SignalField useFrame, after all camera moves:
361
+ PROJ_SCRATCH.set(anchor.x, anchor.y, -2).project(camera);
362
+ setEcoProjection(node,
363
+ (PROJ_SCRATCH.x * 0.5 + 0.5) * size.width,
364
+ (0.5 - PROJ_SCRATCH.y * 0.5) * size.height);
365
+ ```
366
+
367
+ `ConstellationOverlay.tsx` (a fixed SVG) reads those points in its own rAF and
368
+ positions the constellation lines, the astronomy-chart labels (with leader
369
+ ticks), and the rotating "you are here" reticle — perfectly registered over the
370
+ particle clusters at any scroll position. CSS `is-live` (toggled at `alpha > 0.5`)
371
+ sequences the draw-in: lines stroke themselves, then labels stagger, then the
372
+ reticle scales in and spins.
373
+
374
+ > **Why it works:** the SVG never guesses. It uses the exact projection the GPU
375
+ > used that frame, so the "interface drawing the constellation" illusion holds
376
+ > through dolly, pan, and tilt. Cost: 4 `Vector3.project` calls per frame ≈ zero.
377
+
378
+ ### 6.2 Chart participation (why-now section)
379
+
380
+ The reverse direction: the chart's GSAP scrub publishes its draw progress, and
381
+ the field assembles its particle copy of the curve left-to-right in lockstep:
382
+
383
+ ```ts
384
+ // WhyNowChart timeline:
385
+ onUpdate: () => setChartProgress(tl.progress())
386
+
387
+ // SignalField, per point of the gapCurve formation:
388
+ const u = (px + HALF) / (HALF * 2); // 0..1 along the curve
389
+ if (u > chartProg + 0.04) { py -= drop; lum *= 0.5; } // not yet assembled
390
+ else lum += max(0, 1 - |u - chartProg| * 16) * 0.8; // bright head at the draw front
391
+ ```
392
+
393
+ The SVG line and the particles share one scrub and one inertia, so the bright
394
+ particle "head" runs exactly where the line tip is being drawn.
395
+
396
+ ---
397
+
398
+ ## 7. Layer 6 — DOM choreography
399
+
400
+ Three small components, one mechanism each, all reading the same clock.
401
+
402
+ ### FocalSpotlight — `src/components/v2/motion/FocalSpotlight.tsx`
403
+ An `IntersectionObserver` with a generous center band (`rootMargin: -22% 0 -22% 0`)
404
+ toggles a `data-dim` attribute on sections outside the band. CSS dims them to
405
+ 62% opacity. The reader's eye is always pulled to the active section. The finale
406
+ section is excluded (`:not(#finale)`) because it runs its own fade.
407
+
408
+ ### ParallaxDrift — `src/components/v2/motion/ParallaxDrift.tsx`
409
+ Wraps **at most one figure per viewport** in a GSAP `yPercent` scrub for a few
410
+ percent of depth drift. Restraint is the rule: parallax on everything is nausea;
411
+ parallax on the one hero figure is depth.
412
+
413
+ ### ScrollRail — `src/components/v2/atmosphere/ScrollRail.tsx`
414
+ A right-edge hairline whose fill is scaled imperatively (`scaleY(global)`) and
415
+ brightened by `velocity` — a live mission timeline, updated per frame with zero
416
+ React renders. Act markers deep-link to their anchor sections and use the coarse
417
+ `subscribeAct` path for their active state.
418
+
419
+ ---
420
+
421
+ ## 8. Layer 7 — Reveal primitives (act-themed entrances)
422
+
423
+ **File:** `src/components/v2/design/Reveal.tsx`
424
+
425
+ Entrances are not generic. Each narrative act has a reveal whose *physics* match
426
+ its meaning — this is Disney's "staging" applied to a scroll page:
427
+
428
+ | Reveal | Physics | Act it serves |
429
+ |---|---|---|
430
+ | `ScatterReveal` | drifts in off-axis with a blur that resolves | **Problem** — unmanaged things settling into frame |
431
+ | `LockReveal` | scales down a hair and snaps into place | **Control** — a part machined into a slot |
432
+ | `ClipReveal` | mechanical left-to-right clip wipe | headlines, panels |
433
+ | `PulseReveal` | a single soft pulse as it "acquires lock" | **Signal** — the waitlist climax |
434
+ | `Stagger` / `StaggerItem` | children cascade with a 0.06s stagger | lists, readouts |
435
+
436
+ **Every reveal renders its final state plainly under `prefers-reduced-motion`** —
437
+ the `useReducedMotion()` branch returns the plain tag. Entrances enhance; they
438
+ never gate whether content is visible.
439
+
440
+ > **Why it works:** when the *kind* of entrance matches the *meaning* of the
441
+ > section, motion becomes narration. The page isn't decorated; it's told.
442
+
443
+ ---
444
+
445
+ ## 9. Layer 8 — Scrubbed instruments (GSAP recipe)
446
+
447
+ Diagrams that narrate state over time (growth chart, comparison table, the
448
+ pinned three-step flow, the why-now chart) use one repeatable GSAP recipe.
449
+
450
+ **Files:** `WhyNowChart.tsx`, `EcosystemDiagram.tsx`, `PinnedSteps.tsx`, `gsap-client.ts`
451
+
452
+ ```ts
453
+ useLayoutEffect(() => {
454
+ if (shouldReduceScenes()) return; // static, fully-drawn fallback
455
+ let teardown;
456
+ loadGsap().then(({ gsap, ScrollTrigger }) => { // lazy: GSAP never in initial bundle
457
+ const tl = gsap.timeline({
458
+ defaults: { ease: "none" }, // ease: 'none' is mandatory with scrub
459
+ scrollTrigger: { trigger: svg, start: "top 85%", end: "top 25%", scrub: 0.8 },
460
+ });
461
+ tl.fromTo(lines,
462
+ { strokeDasharray: 1, strokeDashoffset: 1 }, // pathLength={1} normalizes every path
463
+ { strokeDashoffset: 0, stagger: 0.08 }); // → one variable draws them all
464
+ teardown = () => { tl.scrollTrigger?.kill(); tl.kill(); }; // ALWAYS kill on cleanup
465
+ ScrollTrigger.refresh();
466
+ });
467
+ return () => teardown?.();
468
+ }, []);
469
+ ```
470
+
471
+ The non-negotiables (each learned from a real bug here):
472
+
473
+ - **Lazy-load GSAP** (`loadGsap()`), register the plugin once, keep it out of the
474
+ initial bundle.
475
+ - **`ease: 'none'`** on scrubbed tweens — easing fights the scrub.
476
+ - **`pathLength={1}`** on SVG paths so a single 0..1 variable can draw any path,
477
+ regardless of its real length.
478
+ - **Always `tl.kill()` + `scrollTrigger.kill()`** in cleanup — un-killed
479
+ timelines leak and double up on fast-refresh / route changes.
480
+ - **Reduced-motion renders the final drawn state** with no trigger attached.
481
+
482
+ ---
483
+
484
+ ## 10. Cross-cutting rules
485
+
486
+ ### Motion tokens — `src/lib/motion/tokens.ts`
487
+ No component invents its own curve. Everything uses:
488
+
489
+ - **`EASE = cubic-bezier(0.16, 1, 0.3, 1)`** (an easeOutExpo-like signature),
490
+ mirrored in CSS as `--v2-ease`.
491
+ - **`DUR`** — `fast 0.18` / `base 0.32` / `slow 0.55` / `epic 0.8` seconds.
492
+ - **`STAGGER = 0.07`** — the standard interval between sibling reveals.
493
+
494
+ One ease + a tiny duration set is what makes twelve independent animations feel
495
+ like one designed system rather than twelve developers' defaults.
496
+
497
+ ### The fallback policy (single source: `shouldReduceScenes()`)
498
+ `gsap-client.ts` centralizes the decision: `prefers-reduced-motion` **or**
499
+ `pointer: coarse` ⇒ skip motion-heavy scenes. Consequences everywhere:
500
+
501
+ - WebGL field → 2D `SignalMesh`.
502
+ - Lenis smooth scroll → native scroll.
503
+ - Pinned scrub scenes → static fully-drawn diagrams.
504
+ - Reveals → final state, instant.
505
+ - Constellation overlay → not mounted; section shows the boxed diagram.
506
+
507
+ ### Performance budget
508
+ - WebGL is **always** `dynamic(() => import(...), { ssr: false })` — never in the
509
+ first bundle, mounted one frame after hydration paints.
510
+ - Hot loops mutate typed arrays / plain objects and **never call `setState`**.
511
+ - Animate only `transform` and `opacity` in DOM/CSS; avoid `width`/`height`/
512
+ `box-shadow` transitions.
513
+ - Pinned scroll is budgeted at ≈30% of total page height.
514
+ - Particle count (936) and DPR cap (`[1, 1.75]`) keep the field on a
515
+ `low-power` GL context.
516
+
517
+ ### Accessibility parity
518
+ Tabs follow WAI-ARIA roving-tabindex (`use-tabs.ts`); the mobile nav is a
519
+ focus-trapped `role="dialog"`; every canvas/field is `aria-hidden` with a
520
+ text-equivalent in the DOM (e.g. the ecosystem stage keeps the diagram's
521
+ `aria-label` as `sr-only`). Motion is never the only carrier of meaning.
522
+
523
+ ---
524
+
525
+ ## 11. Recipe — build a new SHELEG site from scratch
526
+
527
+ A pragmatic order that front-loads the parts everything else depends on.
528
+
529
+ 1. **Lay the visual system first.** Implement `DESIGN.md`: near-black canvas,
530
+ one signal accent, hairline structure, mono telemetry labels, the panel /
531
+ button / status-pill components. Motion on top of a weak visual system just
532
+ amplifies the weakness.
533
+
534
+ 2. **Stand up the clock.** Port `scroll-progress.ts`: the `state` object, the
535
+ dual subscriptions (`getScrollState` for per-frame, `subscribeAct` for
536
+ React), the rAF `update()`, the 65%-viewport `measure()`, and the
537
+ `ResizeObserver` re-measure. Verify with a temporary readout of `global` /
538
+ `act` / `form`.
539
+
540
+ 3. **Add smooth scroll.** `SmoothScroll.tsx` with Lenis on the GSAP ticker,
541
+ feeding `setScrollVelocity`. Gate it behind `shouldReduceScenes()`.
542
+
543
+ 4. **Define scenes.** Write your `scenes.ts` registry — one `{ anchor,
544
+ formation, focusX, energy }` per section. Give each section an `id` matching
545
+ its anchor. This is your storyboard; iterate on it in data before touching
546
+ rendering.
547
+
548
+ 5. **Build the field.** `SignalField.tsx`: a seeded point cloud, one builder per
549
+ formation, the `PROFILES` table, and the `useFrame` morph (clamped smoothing
550
+ + per-point smoothstep stagger + arcs). Get *one* formation looking right,
551
+ then add the rest as data.
552
+
553
+ 6. **Mirror it in 2D.** `SignalMesh.tsx` reading the same store, so touch / no-
554
+ WebGL tells the same story. Wire the `AtmosphereField` capability probe.
555
+
556
+ 7. **Layer the DOM choreography.** `FocalSpotlight`, `ParallaxDrift` (one figure
557
+ per viewport), `ScrollRail`. Each is tiny and independent.
558
+
559
+ 8. **Theme the entrances.** Build the `Reveal` family; assign a reveal physics to
560
+ each act's meaning. Always branch reduced-motion to plain.
561
+
562
+ 9. **Add scrubbed instruments** with the Section 9 GSAP recipe for any diagram
563
+ that narrates state over time.
564
+
565
+ 10. **Fuse DOM ↔ WebGL where it earns it.** Only when you want labels exactly on
566
+ clusters or particles synced to a chart: add the `field-sync` bridge, project
567
+ anchors in `useFrame`, read them in a fixed SVG overlay.
568
+
569
+ 11. **Pay the fallback + a11y tax as you go**, not at the end. Every layer ships
570
+ with its reduced-motion branch in the same commit.
571
+
572
+ 12. **Verify like the runbook:** `tsc --noEmit`, `eslint`, `build` clean;
573
+ screenshot each scene mid-hold and mid-morph; emulate reduced-motion; check a
574
+ ~390px viewport. Then deploy.
575
+
576
+ ---
577
+
578
+ ## 12. Why it works (the deeper principles)
579
+
580
+ - **A single clock removes disagreement.** Most "janky" multi-effect pages have
581
+ each effect measuring scroll on its own schedule; they drift apart by a frame
582
+ and the eye reads chaos. One measured state, read by all, makes layers
583
+ *phase-locked* — they look intentional because they literally share a clock.
584
+
585
+ - **Cheap-but-many beats expensive-but-few.** 936 points doing simple positional
586
+ lerps, plus five DOM layers reading a mutable object, costs less than one
587
+ heavy library effect — and reads richer, because richness comes from *layers
588
+ agreeing*, not from any single layer's complexity.
589
+
590
+ - **Redeploy, don't crossfade.** Human attention tracks *moving objects*. A
591
+ phase-staggered, arc-curved migration of points gives the eye things to
592
+ follow; a crossfade gives it nothing and reads as flat compositing.
593
+
594
+ - **Hold creates legibility; morph creates delight.** Long holds let each
595
+ formation be *understood* (it's the why-now curve, it's the constellation);
596
+ the short morph at the seam is the reward. Constant motion would sacrifice
597
+ both — nothing is understood and nothing is special.
598
+
599
+ - **Earned motion respects the reader.** Scrub is reserved for instruments that
600
+ genuinely encode time/state. Decorative scrub (parallax everything, scrub the
601
+ hero) is the tell of a template; restraint is the tell of a system.
602
+
603
+ - **Degrade-to-calm is a feature, not a chore.** Because every layer has a
604
+ static state, the page is *correct* on a low-power phone or for a motion-
605
+ sensitive user, and the rich version is a pure bonus. The fallback isn't a
606
+ worse site; it's the same site, quieter.
607
+
608
+ - **Data-driven choreography scales.** The whole narrative lives in one `SCENES`
609
+ array and a `PROFILES` table. Re-storyboarding the page is editing data, not
610
+ rewriting render loops. That is why this system can keep growing (ecosystem
611
+ constellation, chart particles, the N finale) without collapsing.
612
+
613
+ ---
614
+
615
+ ## 13. File map (where each idea lives)
616
+
617
+ | Concern | File |
618
+ |---|---|
619
+ | The clock / scroll state | `src/lib/motion/scroll-progress.ts` |
620
+ | Scene storyboard | `src/lib/motion/scenes.ts` |
621
+ | Motion tokens (ease/dur/stagger) | `src/lib/motion/tokens.ts` |
622
+ | Lazy GSAP + reduced-motion gate | `src/lib/motion/gsap-client.ts` |
623
+ | DOM↔WebGL bridge | `src/lib/motion/field-sync.ts` |
624
+ | Interactive field gestures | `src/lib/motion/use-section-field.ts` |
625
+ | WebGL particle field | `src/components/v2/webgl/SignalField.tsx` |
626
+ | 2D fallback field | `src/components/v2/atmosphere/SignalMesh.tsx` |
627
+ | Field/fallback mode switch | `src/components/v2/atmosphere/AtmosphereField.tsx` |
628
+ | Constellation SVG overlay | `src/components/v2/atmosphere/ConstellationOverlay.tsx` |
629
+ | Progress rail | `src/components/v2/atmosphere/ScrollRail.tsx` |
630
+ | Smooth scroll | `src/components/v2/motion/SmoothScroll.tsx` |
631
+ | Attention dimming | `src/components/v2/motion/FocalSpotlight.tsx` |
632
+ | Figure parallax | `src/components/v2/motion/ParallaxDrift.tsx` |
633
+ | Reveal primitives | `src/components/v2/design/Reveal.tsx` |
634
+ | Epilogue runway | `src/components/v2/sections/FinaleSectionV2.tsx` |
635
+ | Scrubbed instruments (examples) | `WhyNowChart.tsx`, `EcosystemDiagram.tsx`, `PinnedSteps.tsx` |
636
+ | CSS tokens + all `v2-*` styles | `src/app/v2.css` |
637
+
638
+ ---
639
+
640
+ *SHELEG Design is the motion + systems half of this site's identity; pair it
641
+ with `DESIGN.md` for the visual half. The north star for both: it should feel
642
+ like a precision instrument responding to your hand — authority through
643
+ accuracy and restraint, not spectacle.*
package/skill/SKILL.md ADDED
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: sheleg-design
3
+ description: Use when building or upgrading a cinematic, scroll-driven landing page, marketing site, or hero experience — especially one with a particle/WebGL background, scroll-linked animation, parallax, pinned/scrubbed sections, or formation-changing motion. SHELEG Design is a motion + systems methodology: one scroll "clock" drives many cheap, layered, independently-degradable responses (a particle field, a 2D fallback, attention dimming, parallax, scrubbed instruments, a progress rail) so the page reads as a single precision instrument. Read this before designing the motion architecture of such a site; it pairs with a visual system, not replaces one.
4
+ ---
5
+
6
+ # SHELEG Design
7
+
8
+ A methodology for landing pages that feel *alive* without feeling busy. Full
9
+ reference (architecture, code-level mechanics, build-from-scratch recipe, file
10
+ map, and the deeper "why") lives in [`SHELEG_DESIGN.md`](./SHELEG_DESIGN.md) next
11
+ to this file — read it before implementing.
12
+
13
+ ## The thesis
14
+
15
+ A page feels cinematic not from many animations, but from a **single source of
16
+ truth** (scroll position) driving **many cheap, layered responses** that are
17
+ individually quiet and collectively rich. Centralize scroll into one external
18
+ store; let independent layers read it per frame and react in their own language.
19
+ Nothing crossfades — things *redeploy*. Every layer degrades to a calm static
20
+ state.
21
+
22
+ ## The five principles (apply in order)
23
+
24
+ 1. **One clock.** All motion derives from one measured scroll state. Layers never
25
+ measure scroll independently, so they can never drift out of phase.
26
+ 2. **Read per frame, notify rarely.** Hot consumers (WebGL/canvas/rail) read the
27
+ store imperatively each frame and cause zero React renders. Only coarse,
28
+ human-visible changes (the current "act"/section) notify the framework.
29
+ 3. **Hold, then redeploy.** Hold a formation steady for ~80% of a section, then
30
+ morph in a short, phase-staggered, arc-curved wave. Ban crossfades.
31
+ 4. **Earned motion.** Scrub belongs to instruments that narrate state over time
32
+ (charts, step flows). Hover/entrance motion stays sub-500ms and never gates
33
+ content visibility.
34
+ 5. **Degrade to calm.** `prefers-reduced-motion` / coarse pointer / no-WebGL all
35
+ collapse to a static, fully-legible page. The effect is a bonus, never a
36
+ dependency.
37
+
38
+ ## How to use this skill
39
+
40
+ When asked to build or upgrade such a site:
41
+
42
+ 1. **Lay the visual system first** (color, type, spacing, components). Motion on
43
+ top of a weak visual system amplifies the weakness. SHELEG Design is the
44
+ motion layer — it assumes a visual foundation exists.
45
+ 2. **Build bottom-up following the layer order** in `SHELEG_DESIGN.md` §11:
46
+ the scroll clock → smooth scroll → particle field → 2D fallback → DOM
47
+ choreography → reveal primitives → scrubbed instruments → (optional) DOM↔WebGL
48
+ bridge. Each layer is a small, single-responsibility file reading the one clock.
49
+ 3. **Storyboard in data.** Express the narrative as a `SCENES` registry (one
50
+ `{ anchor, formation, focusX, energy }` per section). Iterate on the data
51
+ before touching render loops.
52
+ 4. **Pay the fallback + a11y tax in the same commit** as each layer, never at the
53
+ end. Every animated component ships its reduced-motion branch immediately.
54
+ 5. **Verify** with typecheck/lint/build, screenshots of each scene mid-hold and
55
+ mid-morph, a reduced-motion pass, and a narrow-viewport pass.
56
+
57
+ ## Non-negotiables (each prevents a real failure mode)
58
+
59
+ - Centralize scroll in ONE store with two read paths: a live getter for
60
+ per-frame readers and a coarse subscription for framework-rendered UI.
61
+ - Hold-then-morph (long hold, short smoothstepped tail) — constant morphing
62
+ reads as nervous; this is the single biggest "calm" lever.
63
+ - Redeploy with a per-point phase-staggered, perpendicular-arc migration — this
64
+ is what makes a particle field read as premium rather than a screensaver.
65
+ - Drive smooth scroll (e.g. Lenis) from the animation library's ticker so
66
+ scrubbed instruments and the particle field share one inertia.
67
+ - Lazy-load heavy libs (GSAP/WebGL) out of the initial bundle; WebGL mounts one
68
+ frame after hydration paints.
69
+ - One ease + a tiny duration/stagger token set for the whole site; no component
70
+ invents its own curve.
71
+ - For scrubbed SVG: `ease: 'none'`, `pathLength={1}` to normalize paths, and
72
+ always kill timelines + triggers on cleanup.
73
+ - Animate only `transform` and `opacity`; reserve scrub for genuine instruments.
74
+
75
+ ## When NOT to use it
76
+
77
+ Skip for static content sites, docs, dashboards, or anything where motion is not
78
+ a goal. Do not bolt the particle field onto a page whose visual system or copy
79
+ isn't finished — fix those first.
80
+
81
+ ---
82
+
83
+ Read [`SHELEG_DESIGN.md`](./SHELEG_DESIGN.md) for the full architecture, code
84
+ mechanics, the exact morph math, the DOM↔WebGL projection bridge, the
85
+ build-from-scratch recipe, and the file map.