@babylonjs-toolkit/agent 1.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/README.md +344 -0
- package/bin/bt-agent.js +266 -0
- package/lib/doctor.js +59 -0
- package/lib/install.js +145 -0
- package/lib/manifest.js +46 -0
- package/lib/paths.js +66 -0
- package/lib/payload.js +56 -0
- package/lib/persona.js +177 -0
- package/lib/targets.js +105 -0
- package/package.json +43 -0
- package/persona.md +5 -0
- package/scripts/postinstall.js +49 -0
- package/skills/bt-atlas/SKILL.md +192 -0
- package/skills/bt-atlas/scripts/composite_skin.py +58 -0
- package/skills/bt-atlas/scripts/preview.py +70 -0
- package/skills/bt-atlas/scripts/requirements.txt +2 -0
- package/skills/bt-atlas/scripts/uv_island_mask.py +87 -0
- package/skills/bt-convert/SKILL.md +32 -0
- package/skills/bt-copycat/SKILL.md +184 -0
- package/skills/bt-design/SKILL.md +187 -0
- package/skills/bt-design/references/3d-hero-docs.md +976 -0
- package/skills/bt-design/references/3d-hero-scroll.md +269 -0
- package/skills/bt-design/templates/3d-hero-scroll/HeroScroll.tsx +167 -0
- package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.css +268 -0
- package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.d.ts +67 -0
- package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.html +78 -0
- package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.js +559 -0
- package/skills/bt-execute/SKILL.md +130 -0
- package/skills/bt-gauntlet/SKILL.md +335 -0
- package/skills/bt-hero/SKILL.md +158 -0
- package/skills/bt-landing/SKILL.md +126 -0
- package/skills/bt-plan/SKILL.md +172 -0
- package/skills/bt-prototype/SKILL.md +161 -0
- package/skills/bt-spec/SKILL.md +328 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# 3D Scrolling Hero Section With Optional Cinematic Controls (3D-Hero-Scroll)
|
|
2
|
+
|
|
3
|
+
A scroll-scrubbed cinematic hero: the product moves through real terrain as the
|
|
4
|
+
user scrolls — the scroll position IS the film's playhead. Proven pattern
|
|
5
|
+
(Scout-Motors-style site-of-the-year mechanics) with optional chrome: telemetry
|
|
6
|
+
HUD, film-speed autoplay, veiled jump cuts.
|
|
7
|
+
|
|
8
|
+
**Triggers:** "3D scrolling hero", "scroll-scrub", "cinematic scroll",
|
|
9
|
+
"redesign the hero with 3D scrolling", "the product drives/flies/moves as you
|
|
10
|
+
scroll", "Scout Motors style".
|
|
11
|
+
|
|
12
|
+
**Human quickstart:** `3d-hero-docs.md` (this directory)
|
|
13
|
+
maps every prompt field to the exact code it generates — hand it to developers
|
|
14
|
+
writing invocation prompts.
|
|
15
|
+
|
|
16
|
+
**Templates (drop-in, in this skill) — `templates/3d-hero-scroll/`:**
|
|
17
|
+
`hero-scroll.js` is one framework-agnostic ESM engine shared by both hosts;
|
|
18
|
+
copy the files that match the target:
|
|
19
|
+
- **Non-React (plain HTML/static/Astro/Vue/plain JS):** `hero-scroll.html` +
|
|
20
|
+
`hero-scroll.css` + `hero-scroll.js`. The engine **auto-boots** against the
|
|
21
|
+
document when `window.HS_CONFIG` is set, loaded via
|
|
22
|
+
`<script type="module" src="hero-scroll.js">`.
|
|
23
|
+
- **React / TypeScript:** `HeroScroll.tsx` + `hero-scroll.js` +
|
|
24
|
+
`hero-scroll.d.ts` + `hero-scroll.css`. `HeroScroll.tsx` renders the markup
|
|
25
|
+
as JSX and mounts the SAME engine via `initHeroScroll(rootEl, config)` in a
|
|
26
|
+
`useEffect` (destroy on unmount). Props gate the optional chrome; import the
|
|
27
|
+
scrub mp4 + poster as assets.
|
|
28
|
+
|
|
29
|
+
The engine is config-driven (`HS_CONFIG` / the config arg) and presence-gated:
|
|
30
|
+
delete an optional element (or omit a `HeroScroll` prop) and its feature
|
|
31
|
+
vanishes cleanly. It resolves the real scroll container (window / `body` /
|
|
32
|
+
overflow ancestor) so body-scroll apps work. Do NOT rewrite the engine from
|
|
33
|
+
memory in any framework; copy it and configure it.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1 · Intake — resolve these BEFORE coding
|
|
38
|
+
|
|
39
|
+
1. **Footage source** (pick one):
|
|
40
|
+
- **(a) Provided video** — skip to §3 (still needs the scrub re-encode).
|
|
41
|
+
- **(b) Generate the footage** with whatever image/video generation is
|
|
42
|
+
configured — KIE MCP servers (`kie-image-mcp` / `kie-video-mcp`),
|
|
43
|
+
Higgsfield MCP, the model's own built-in image/video generation, or any
|
|
44
|
+
other configured image/video tool — §2.
|
|
45
|
+
- **(c) No footage possible** — this pattern is wrong; use a static hero.
|
|
46
|
+
2. **Brand tokens** — map `--hs-bg / --hs-ink / --hs-dim / --hs-accent /
|
|
47
|
+
--hs-display / --hs-mono` to the host site's design system (DESIGN.md or
|
|
48
|
+
existing CSS custom properties). Never ship the template defaults into a
|
|
49
|
+
branded site.
|
|
50
|
+
3. **Optional controls** — ask or infer which to include:
|
|
51
|
+
HUD (telemetry value + segments) · PLAY (film-speed autoplay) ·
|
|
52
|
+
TOP/END (veiled jumps) · overlays (copy choreographed over the film) ·
|
|
53
|
+
loader gate. Each is one HTML block; the engine self-configures.
|
|
54
|
+
4. **Telemetry metric** — mph is only the obvious one. Altitude, depth, RPM,
|
|
55
|
+
temperature, distance, watts — pick what fits the product. Configure
|
|
56
|
+
`max`, `unit` (in markup), and the curve in `HS_CONFIG.telemetry`.
|
|
57
|
+
5. **Scroll length** — 15–20vh of journey height per second of footage
|
|
58
|
+
(32s film → 560–640vh). Shorter feels rushed; longer feels sticky.
|
|
59
|
+
6. **Sweep** — the engine is page-aware by default, hero-scoped by option
|
|
60
|
+
(`HS_CONFIG.sweep`). It only ever owns the journey block + fixed chrome;
|
|
61
|
+
it never touches the host page's DOM below the hero. `sweep: "page"`
|
|
62
|
+
(default): PLAY glides on through the rest of the page to the bottom and
|
|
63
|
+
END jumps there — the full cinematic ride, for when the whole landing
|
|
64
|
+
page shares the film's design system and should be swept through.
|
|
65
|
+
`sweep: "hero"`: PLAY and END stop at the journey's end — you land on the
|
|
66
|
+
regular HTML section right after the hero, exactly as a normal scroll
|
|
67
|
+
would, the polite mode that never drives the user through the rest of
|
|
68
|
+
the page uninvited; opt into this explicitly in the prompt (or
|
|
69
|
+
`HS_CONFIG`) when only the hero should be owned.
|
|
70
|
+
Scrub smoothness is identical in both modes; sweep only changes where
|
|
71
|
+
autoplay/jumps consider "the end". (Named `sweep`, never `reach`, so it
|
|
72
|
+
can't be confused with a spec/plan's route/DOM-scope terminology.)
|
|
73
|
+
|
|
74
|
+
## 2 · Footage pipeline (generation path)
|
|
75
|
+
|
|
76
|
+
The generation backend is pluggable — the pipeline below is the same whatever
|
|
77
|
+
produces the frames. Route the image/video calls to whatever is configured:
|
|
78
|
+
KIE MCP servers (`kie-image-mcp` / `kie-video-mcp`), Higgsfield MCP, the model's
|
|
79
|
+
own built-in image/video generation, or any other configured image/video tool.
|
|
80
|
+
The parameter names below (`reference_paths`, `image_paths`) are KIE/Kling's;
|
|
81
|
+
other backends expose the equivalent reference-image and first/last-frame inputs
|
|
82
|
+
under their own names — map to whichever you're using.
|
|
83
|
+
|
|
84
|
+
> **If using the KIE MCP servers** (Babylon Toolkit projects): fetch the
|
|
85
|
+
> Babylon Toolkit Agent Reference and its `web-kie-servers.md` sub-document
|
|
86
|
+
> first (per CLAUDE.md). Those servers read the key from `.env` — a file named
|
|
87
|
+
> `env` is ignored.
|
|
88
|
+
|
|
89
|
+
1. **One hero anchor image first** (image model, e.g. nano-banana-pro, 16:9,
|
|
90
|
+
2K). Every other asset references it (reference/anchor image input) so it is
|
|
91
|
+
the same product everywhere.
|
|
92
|
+
2. **Chain clips start→end frame** so the journey is continuous (pin each
|
|
93
|
+
clip's first frame, and where supported its last frame — KIE/Kling exposes
|
|
94
|
+
this as `image_paths`: `[0]` = first frame, `[1]` = last frame):
|
|
95
|
+
- Clip 1: first = a generated "concealed" variant (dust/fog/dark), last =
|
|
96
|
+
the hero anchor → the reveal.
|
|
97
|
+
- Clip N: first = **ffmpeg-extracted last frame of clip N-1**:
|
|
98
|
+
`ffmpeg -y -sseof -0.1 -i clipN-1.mp4 -frames:v 1 -update 1 -q:v 2 last.jpg`
|
|
99
|
+
- Final clip: pin the last frame to a generated destination still.
|
|
100
|
+
- Big lighting changes (day→night) must happen INSIDE a clip's prompt
|
|
101
|
+
("light dies to dusk as…"), never across a cut — chaining can't bridge a
|
|
102
|
+
lighting jump.
|
|
103
|
+
3. **Know your video model's real output** (example — KIE/Kling 3: `std` mode
|
|
104
|
+
returns 1284×716, not 1080p — use `pro` if that matters; clips run ~8.04s
|
|
105
|
+
for a requested 8s). Whatever the backend, ffprobe everything; never assume
|
|
106
|
+
resolutions or durations.
|
|
107
|
+
4. **Concat + scrub encode** (the scrub encode is NON-NEGOTIABLE):
|
|
108
|
+
```
|
|
109
|
+
ffmpeg -y -f concat -safe 0 -i list.txt -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p -an journey.mp4
|
|
110
|
+
ffmpeg -y -i journey.mp4 -vf "scale=1280:720,fps=24" -c:v libx264 -preset slow -crf 21 \
|
|
111
|
+
-g 1 -keyint_min 1 -pix_fmt yuv420p -movflags +faststart -an journey-scrub.mp4
|
|
112
|
+
```
|
|
113
|
+
`-g 1` makes every frame a keyframe → `currentTime` seeks are instant.
|
|
114
|
+
Without it, scrubbing stutters and the whole illusion dies. Expect the
|
|
115
|
+
scrub file to be LARGER than the source (~20MB for 32s @720p — fine, it's
|
|
116
|
+
blob-preloaded once).
|
|
117
|
+
5. **Poster** = the FIRST frame of journey.mp4 (`-frames:v 1`), not a glamour
|
|
118
|
+
still — otherwise the video swap after preload is a visible jump-cut.
|
|
119
|
+
|
|
120
|
+
## 3 · Wire-up
|
|
121
|
+
|
|
122
|
+
0. **Pick the host path (§ Templates above).** Non-React: copy
|
|
123
|
+
`hero-scroll.{html,css,js}`. React/TS: copy `HeroScroll.tsx` +
|
|
124
|
+
`hero-scroll.js` + `hero-scroll.d.ts` + `hero-scroll.css`, render
|
|
125
|
+
`<HeroScroll config={…} … />` where the hero goes, and import the scrub
|
|
126
|
+
mp4 + poster as assets. Either way the engine (`hero-scroll.js`) is
|
|
127
|
+
identical — do not fork it per framework. In React, `HeroScroll.tsx`
|
|
128
|
+
already handles mount (`initHeroScroll`) and unmount (`.destroy()`); under
|
|
129
|
+
StrictMode the init/destroy cycle runs twice in dev — that's fine, teardown
|
|
130
|
+
is complete.
|
|
131
|
+
1. Copy the files; rename/prefix if the host has conflicts (everything is
|
|
132
|
+
`hs-` namespaced already).
|
|
133
|
+
2. Replace the placeholder 100vh hero with the `#hs-journey` block (or the
|
|
134
|
+
`<HeroScroll>` element); with `sweep: "page"` the rest of the page below it
|
|
135
|
+
automatically becomes the autoplay "tail". **Sticky constraint:** the
|
|
136
|
+
journey block must sit in plain document flow — ideally a direct child of
|
|
137
|
+
`<body>` (in React, avoid wrapping it in transformed/overflow-clipped
|
|
138
|
+
layout containers). Any ancestor with `transform`, `filter`, `perspective`,
|
|
139
|
+
or `overflow: hidden/auto` breaks `position: sticky` and kills the entire
|
|
140
|
+
effect. Check for this FIRST when retrofitting. (The engine auto-resolves
|
|
141
|
+
whether the window or `document.body`/an overflow ancestor is the scroller,
|
|
142
|
+
so body-scroll apps scrub correctly without config.)
|
|
143
|
+
3. **Handoff rule:** the first section below the journey must tonally
|
|
144
|
+
continue the film's final frame (e.g. a film that ends dark must hand off
|
|
145
|
+
into a dark section, not a bright one).
|
|
146
|
+
If the host page goes bright the pixel after the sticky stage releases,
|
|
147
|
+
the cut is jarring and no engine work hides it. Either restyle that first
|
|
148
|
+
section to match the film's exit tone, insert a short bridge section that
|
|
149
|
+
grades from the film's last frame to the host palette, or use
|
|
150
|
+
`sweep: "hero"` and let the user cross the seam themselves.
|
|
151
|
+
4. Author overlays as `.hs-ovl` sections with `data-from`/`data-to` progress
|
|
152
|
+
windows. Leave gaps between windows — moments of pure film are what make
|
|
153
|
+
it cinematic. Overlay copy over bright footage gets `.hs-scrim`.
|
|
154
|
+
5. Set the config: `window.HS_CONFIG` in the page (non-React) or the
|
|
155
|
+
`config` / control props on `<HeroScroll>` (React) — video path, telemetry
|
|
156
|
+
curve, segments, sweep.
|
|
157
|
+
6. The engine blob-preloads the footage with a loading bar, locks scroll
|
|
158
|
+
until ready (`body[data-hs-state]`), degrades to the poster if footage is
|
|
159
|
+
missing, and honors `prefers-reduced-motion`.
|
|
160
|
+
|
|
161
|
+
## 4 · Calibration (what makes it feel expensive)
|
|
162
|
+
|
|
163
|
+
- **Telemetry launch point:** extract frames around the moment the subject
|
|
164
|
+
first MOVES (`ffmpeg -ss <t> -i journey-scrub.mp4 -frames:v 1 f.jpg`, eye
|
|
165
|
+
them) and set `telemetry.startP = t_launch / film_duration`. Generated
|
|
166
|
+
clips hold the subject still for a beat after a cut — the clip boundary is
|
|
167
|
+
NOT the launch. A needle pinned at 0 until the exact frame motion begins,
|
|
168
|
+
then ripping (exponent ≈ 0.45), reads as real telemetry; starting early
|
|
169
|
+
reads as fake and users notice immediately.
|
|
170
|
+
- **Segments** align to the source clips (equal clips → quarters).
|
|
171
|
+
- **Autoplay speed is film speed:** the engine converts journey pixels ÷ film
|
|
172
|
+
seconds so PLAY scrubs at exactly 1× real time. Do not "improve" this with
|
|
173
|
+
a fixed px/s rate.
|
|
174
|
+
- **Veiled cuts, never visible rewinds:** replay/jumps fade to black, snap
|
|
175
|
+
scroll + smoothed progress + video time UNDER the veil, then lift. The
|
|
176
|
+
smoothed-progress snap is mandatory — skipping it causes a reverse-scrub
|
|
177
|
+
flash when the veil lifts.
|
|
178
|
+
|
|
179
|
+
## 5 · Verification protocol (do not skip; run in a real browser)
|
|
180
|
+
|
|
181
|
+
Drive the page with chrome-devtools MCP and require ALL of:
|
|
182
|
+
|
|
183
|
+
1. **Scrub sync sweep** — for p in {0, .1, .25, .34, .5, .62, .75, .87, 1}:
|
|
184
|
+
scroll to `p × (journeyHeight − innerHeight)`, wait ~1.5s for the lerp,
|
|
185
|
+
assert `|video.currentTime − p × duration| ≤ 0.05s`.
|
|
186
|
+
2. **Telemetry** — value is 0 for all p ≤ startP (sample densely around
|
|
187
|
+
startP: e.g. startP − .01, startP + .01), monotonic after, max at endP,
|
|
188
|
+
segment labels flip at their thresholds.
|
|
189
|
+
3. **Autoplay** — after PLAY, video.currentTime advances ≈1.0s per real
|
|
190
|
+
second; reaches document bottom; button resets.
|
|
191
|
+
4. **Cancel** — dispatch a wheel event mid-autoplay: scroll halts within a
|
|
192
|
+
frame, button resets. (Dispatch on `document.body`, not `window` — and the
|
|
193
|
+
engine's guard must survive non-Node targets.)
|
|
194
|
+
5. **Veiled cut timeline** — sample during replay-from-end: scrollY must NOT
|
|
195
|
+
change until veil computed opacity = 1; video time snaps while opacity =
|
|
196
|
+
1; veil fully lifts; film rolls.
|
|
197
|
+
6. **Console** — zero errors across the whole run.
|
|
198
|
+
|
|
199
|
+
## 6 · What the prompt supplies vs. what this skill decides
|
|
200
|
+
|
|
201
|
+
The mechanics are deterministic — the invoking prompt should NEVER need to
|
|
202
|
+
specify scrub encoding, preloading, autoplay speed, veils, cancel semantics,
|
|
203
|
+
or verification. It supplies only the creative decisions; everything missing
|
|
204
|
+
falls back to the defaults in parentheses:
|
|
205
|
+
|
|
206
|
+
- **Product + footage beats** — what moves through what terrain, per clip
|
|
207
|
+
(or an existing video file path).
|
|
208
|
+
- **Sweep** — `page` or `hero` (default `page`: PLAY/END sweep through to the
|
|
209
|
+
document bottom; specify `sweep: hero` explicitly to have PLAY/END stop at
|
|
210
|
+
the journey's end so the next HTML section follows normally instead).
|
|
211
|
+
- **Controls** — which of HUD / PLAY / TOP+END to include (default: all
|
|
212
|
+
three on `sweep: page`, HUD + PLAY only on `sweep: hero`).
|
|
213
|
+
- **Telemetry metric** — value, unit, max, segment names (default: none —
|
|
214
|
+
omit the HUD rather than invent a metric that doesn't fit the product).
|
|
215
|
+
- **Overlays** — the marketing copy and which film moment each block rides
|
|
216
|
+
(default: a single title overlay at the start).
|
|
217
|
+
- **Brand** — tokens/typography, mapped onto `--hs-*` (default: inherit the
|
|
218
|
+
host site's design system; never ship the template placeholders).
|
|
219
|
+
|
|
220
|
+
The subject is whatever the prompt names — the mechanics are identical for a
|
|
221
|
+
watch, a submarine, a building, a person, a dashboard, a landscape. Do NOT
|
|
222
|
+
default to any one look (a vehicle, a desert, cyan-on-black); derive every
|
|
223
|
+
visual from the prompt's own subject. The skeletons below are product-agnostic.
|
|
224
|
+
|
|
225
|
+
> **Easy front door:** the sibling `bt-hero` skill automates this intake — it
|
|
226
|
+
> takes an idea / image / partial answers in any combination, asks only the
|
|
227
|
+
> unanswered questions, applies the defaults, writes a repeatable
|
|
228
|
+
> `_hero-brief.md`, and builds in one shot (or contributes intake when named
|
|
229
|
+
> inside a bt-spec brief). The skeletons below are the manual alternative.
|
|
230
|
+
> Note the skeletons carry ONLY creative slots — pipeline facts (aspect, clip
|
|
231
|
+
> duration, muting, encode, verification) are skill/backend-owned and never
|
|
232
|
+
> belong in an invoking prompt.
|
|
233
|
+
|
|
234
|
+
**Example — greenfield, full-page (skeleton):**
|
|
235
|
+
> Redesign this starter template as a cinematic 3D-scroll site for `<PRODUCT>` —
|
|
236
|
+
> `<one-line description>` (bt-design → 3D-Hero-Scroll, sweep: page).
|
|
237
|
+
> FOOTAGE (generate) — hero anchor first: `<the product, one
|
|
238
|
+
> clear look>`; chain: ① `<reveal beat>` ② `<motion beat>` ③ `<terrain/scene
|
|
239
|
+
> change>` ④ `<finale beat>`. CONTROLS — HUD: `<METRIC> 0→<MAX> <UNIT>`,
|
|
240
|
+
> segments `<A/B/C/D>` · PLAY (`"<label>"`) · TOP/END. OVERLAYS — `<wordmark>`
|
|
241
|
+
> → count-up stats (`<A · B · C>`) → `<feature moment>` → `<closing line>`.
|
|
242
|
+
> BELOW THE JOURNEY — `<sections>` → `<CTA>`. BRAND — `<palette>`, `<accent>`,
|
|
243
|
+
> `<type>` (derived from the subject, not a fixed house style).
|
|
244
|
+
|
|
245
|
+
**Example — retrofit, hero only (skeleton):**
|
|
246
|
+
> Redesign the hero section with 3D scrolling (sweep: hero). Footage:
|
|
247
|
+
> `<existing file | generate: anchor + beats 1..N>`. HUD: `<METRIC> 0→<MAX>
|
|
248
|
+
> <UNIT>` (or none). PLAY only, no jump nav. One headline overlay at the start,
|
|
249
|
+
> `<N>` stats mid-run. Match our existing design tokens; don't touch anything
|
|
250
|
+
> below the hero.
|
|
251
|
+
|
|
252
|
+
Anything below the journey (configurators, CTAs, footers) is ordinary
|
|
253
|
+
bt-design work — this reference governs only the journey block, its
|
|
254
|
+
overlays, and the fixed chrome. Apply the handoff rule (§3) at the seam.
|
|
255
|
+
|
|
256
|
+
## 7 · Known pitfalls (all hit in production; all handled by the template)
|
|
257
|
+
|
|
258
|
+
- Overlay windows starting at 0 must skip their fade-in (`rise = 1` when
|
|
259
|
+
`from ≤ 0`) or the hero overlay renders invisible at page load.
|
|
260
|
+
- Cancel-input handlers must tolerate non-Node event targets, and must
|
|
261
|
+
exempt keydown on the play button itself (else Enter = stop-then-restart).
|
|
262
|
+
- All veil/autoplay callbacks are token-guarded: any stop/cancel increments
|
|
263
|
+
the token, so a cancel during a fade can't leave a ghost timeline running.
|
|
264
|
+
- Blob-preload (not streaming) is what makes seeking instant even without
|
|
265
|
+
range-request support on the dev server.
|
|
266
|
+
- Count-up stats re-arm when their overlay fully exits, so they replay on
|
|
267
|
+
re-entry in either scroll direction.
|
|
268
|
+
- The HUD hides itself when the journey hands off to normal-flow sections;
|
|
269
|
+
jump-nav buttons dim at the end they're already at.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
2
|
+
HeroScroll.tsx — React wrapper for the framework-agnostic hero-scroll
|
|
3
|
+
engine. Renders the `hs-` markup as JSX and mounts `initHeroScroll`
|
|
4
|
+
against its own root, tearing down cleanly on unmount.
|
|
5
|
+
|
|
6
|
+
The engine (hero-scroll.js) is the SAME code the plain-HTML template
|
|
7
|
+
uses — do NOT re-implement the scrubbing/autoplay/veil logic in React.
|
|
8
|
+
Copy this file + hero-scroll.js + hero-scroll.d.ts + hero-scroll.css
|
|
9
|
+
into the app, import your scrub mp4 + poster as assets, and configure.
|
|
10
|
+
|
|
11
|
+
Example (Vite/React):
|
|
12
|
+
|
|
13
|
+
import HeroScroll from "./HeroScroll";
|
|
14
|
+
import scrub from "./assets/journey-scrub.mp4";
|
|
15
|
+
import poster from "./assets/poster.jpg";
|
|
16
|
+
|
|
17
|
+
// Values below are placeholders — derive every label, metric, segment,
|
|
18
|
+
// and overlay from the prompt's own subject; do not copy these verbatim.
|
|
19
|
+
<HeroScroll
|
|
20
|
+
config={{ video: scrub, sweep: "page",
|
|
21
|
+
telemetry: { max: MAX, startP: START_P, endP: 0.95,
|
|
22
|
+
segments: [[0,"SEG A"],[0.25,"SEG B"],
|
|
23
|
+
[0.5,"SEG C"],[0.75,"SEG D"]] } }}
|
|
24
|
+
posterSrc={poster}
|
|
25
|
+
journeyVh={640}
|
|
26
|
+
hud={{ unit: "UNIT" }}
|
|
27
|
+
play={{ idle: "PLAY" }}
|
|
28
|
+
jumpNav
|
|
29
|
+
overlays={
|
|
30
|
+
<>
|
|
31
|
+
<section className="hs-ovl" data-from="0" data-to="0.115">
|
|
32
|
+
<h1>WORDMARK</h1>
|
|
33
|
+
</section>
|
|
34
|
+
<section className="hs-ovl hs-scrim" data-from="0.26" data-to="0.46">
|
|
35
|
+
<span data-hs-count data-target={STAT} data-group="1">0</span> STAT
|
|
36
|
+
</section>
|
|
37
|
+
</>
|
|
38
|
+
}
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
Brand: map --hs-* tokens to the host design system in hero-scroll.css
|
|
42
|
+
(or override on a wrapper). Sticky constraint: no ancestor of this
|
|
43
|
+
component may have transform / filter / perspective / overflow:hidden —
|
|
44
|
+
it breaks position:sticky and kills the effect.
|
|
45
|
+
═══════════════════════════════════════════════════════════════════ */
|
|
46
|
+
|
|
47
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
48
|
+
import { initHeroScroll, type HeroScrollConfig } from "./hero-scroll";
|
|
49
|
+
import "./hero-scroll.css";
|
|
50
|
+
|
|
51
|
+
export interface HeroScrollProps {
|
|
52
|
+
/** Engine config — video path (imported asset URL), telemetry, sweep, etc. */
|
|
53
|
+
config: HeroScrollConfig;
|
|
54
|
+
/** Poster = the FIRST frame of the scrub file (not a glamour still). */
|
|
55
|
+
posterSrc?: string;
|
|
56
|
+
/** Scroll length in vh: ~15–20vh per second of footage (default 640). */
|
|
57
|
+
journeyVh?: number;
|
|
58
|
+
/** Preload gate. Pass a node for the loader mark, `true` for a bare bar,
|
|
59
|
+
* or omit/false to skip the gate. */
|
|
60
|
+
loader?: ReactNode | boolean;
|
|
61
|
+
/** Telemetry HUD (requires config.telemetry). `unit` is the label text. */
|
|
62
|
+
hud?: { unit: string } | false;
|
|
63
|
+
/** Film-speed autoplay control with its idle/busy labels. */
|
|
64
|
+
play?: { idle: string; busy?: string } | false;
|
|
65
|
+
/** Veiled TOP/END jump nav. */
|
|
66
|
+
jumpNav?: boolean;
|
|
67
|
+
/** `.hs-ovl[data-from][data-to]` overlay sections (author as children). */
|
|
68
|
+
overlays?: ReactNode;
|
|
69
|
+
/** Extra class on the journey wrapper. */
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function HeroScroll({
|
|
74
|
+
config,
|
|
75
|
+
posterSrc,
|
|
76
|
+
journeyVh = 640,
|
|
77
|
+
loader,
|
|
78
|
+
hud,
|
|
79
|
+
play,
|
|
80
|
+
jumpNav,
|
|
81
|
+
overlays,
|
|
82
|
+
className,
|
|
83
|
+
}: HeroScrollProps) {
|
|
84
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!rootRef.current) return;
|
|
88
|
+
const hs = initHeroScroll(rootRef.current, config);
|
|
89
|
+
return () => hs.destroy();
|
|
90
|
+
// Re-init only if the video source changes; other config is read once.
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
+
}, [config.video]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div ref={rootRef} className={className}>
|
|
96
|
+
{loader ? (
|
|
97
|
+
<div id="hs-loader" role="status" aria-label="Loading">
|
|
98
|
+
{typeof loader !== "boolean" ? (
|
|
99
|
+
<div className="hs-loader-mark">{loader}</div>
|
|
100
|
+
) : null}
|
|
101
|
+
<div className="hs-loader-bar">
|
|
102
|
+
<span id="hs-loader-fill" />
|
|
103
|
+
</div>
|
|
104
|
+
<span id="hs-loader-pct">000</span>
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
|
|
108
|
+
{play ? (
|
|
109
|
+
<button id="hs-play" aria-pressed="false" title="Play — cinematic auto-scroll">
|
|
110
|
+
<span className="hs-play-icon" aria-hidden="true" />
|
|
111
|
+
<span
|
|
112
|
+
className="hs-play-label"
|
|
113
|
+
data-idle={play.idle}
|
|
114
|
+
data-busy={play.busy ?? "STOP"}
|
|
115
|
+
>
|
|
116
|
+
{play.idle}
|
|
117
|
+
</span>
|
|
118
|
+
</button>
|
|
119
|
+
) : null}
|
|
120
|
+
|
|
121
|
+
{jumpNav ? (
|
|
122
|
+
<nav id="hs-jump-nav" aria-label="Quick navigation">
|
|
123
|
+
<button id="hs-top" title="Go to top">
|
|
124
|
+
<span className="hs-arrow" aria-hidden="true">↑</span>
|
|
125
|
+
<span>TOP</span>
|
|
126
|
+
</button>
|
|
127
|
+
<button id="hs-end" title="Skip to the end">
|
|
128
|
+
<span className="hs-arrow" aria-hidden="true">↓</span>
|
|
129
|
+
<span>END</span>
|
|
130
|
+
</button>
|
|
131
|
+
</nav>
|
|
132
|
+
) : null}
|
|
133
|
+
|
|
134
|
+
{hud ? (
|
|
135
|
+
<aside id="hs-hud" aria-label="Telemetry">
|
|
136
|
+
<div>
|
|
137
|
+
<span id="hs-hud-value">0</span>{" "}
|
|
138
|
+
<span className="hs-hud-unit">{hud.unit}</span>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="hs-hud-bar">
|
|
141
|
+
<span id="hs-hud-progress" />
|
|
142
|
+
</div>
|
|
143
|
+
<div className="hs-hud-meta">
|
|
144
|
+
<span id="hs-hud-seg" />
|
|
145
|
+
<span id="hs-hud-pct">000%</span>
|
|
146
|
+
</div>
|
|
147
|
+
</aside>
|
|
148
|
+
) : null}
|
|
149
|
+
|
|
150
|
+
<div id="hs-journey" style={{ height: `${journeyVh}vh` }}>
|
|
151
|
+
<div className="hs-stage">
|
|
152
|
+
<video
|
|
153
|
+
id="hs-video"
|
|
154
|
+
muted
|
|
155
|
+
playsInline
|
|
156
|
+
preload="none"
|
|
157
|
+
poster={posterSrc}
|
|
158
|
+
/>
|
|
159
|
+
<div className="hs-stage-shade" aria-hidden="true" />
|
|
160
|
+
{overlays}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export default HeroScroll;
|