@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.
Files changed (34) hide show
  1. package/README.md +344 -0
  2. package/bin/bt-agent.js +266 -0
  3. package/lib/doctor.js +59 -0
  4. package/lib/install.js +145 -0
  5. package/lib/manifest.js +46 -0
  6. package/lib/paths.js +66 -0
  7. package/lib/payload.js +56 -0
  8. package/lib/persona.js +177 -0
  9. package/lib/targets.js +105 -0
  10. package/package.json +43 -0
  11. package/persona.md +5 -0
  12. package/scripts/postinstall.js +49 -0
  13. package/skills/bt-atlas/SKILL.md +192 -0
  14. package/skills/bt-atlas/scripts/composite_skin.py +58 -0
  15. package/skills/bt-atlas/scripts/preview.py +70 -0
  16. package/skills/bt-atlas/scripts/requirements.txt +2 -0
  17. package/skills/bt-atlas/scripts/uv_island_mask.py +87 -0
  18. package/skills/bt-convert/SKILL.md +32 -0
  19. package/skills/bt-copycat/SKILL.md +184 -0
  20. package/skills/bt-design/SKILL.md +187 -0
  21. package/skills/bt-design/references/3d-hero-docs.md +976 -0
  22. package/skills/bt-design/references/3d-hero-scroll.md +269 -0
  23. package/skills/bt-design/templates/3d-hero-scroll/HeroScroll.tsx +167 -0
  24. package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.css +268 -0
  25. package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.d.ts +67 -0
  26. package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.html +78 -0
  27. package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.js +559 -0
  28. package/skills/bt-execute/SKILL.md +130 -0
  29. package/skills/bt-gauntlet/SKILL.md +335 -0
  30. package/skills/bt-hero/SKILL.md +158 -0
  31. package/skills/bt-landing/SKILL.md +126 -0
  32. package/skills/bt-plan/SKILL.md +172 -0
  33. package/skills/bt-prototype/SKILL.md +161 -0
  34. package/skills/bt-spec/SKILL.md +328 -0
@@ -0,0 +1,559 @@
1
+ /* ═══════════════════════════════════════════════════════════════════
2
+ 3D-HERO-SCROLL — scroll-scrubbed cinematic hero engine
3
+ Generic, brand-agnostic, config-driven. Pairs with hero-scroll.css.
4
+
5
+ FRAMEWORK-AGNOSTIC ESM. One engine, two hosts:
6
+
7
+ • Plain HTML — load as a module; it auto-boots against `document` when
8
+ `window.HS_CONFIG` is set and a `#hs-journey` exists:
9
+ <script type="module" src="hero-scroll.js"></script>
10
+ • React / Vue / any bundler — import and mount against your own root, then
11
+ tear down on unmount (see HeroScroll.tsx):
12
+ import { initHeroScroll } from "./hero-scroll";
13
+ const hs = initHeroScroll(rootEl, config); // rootEl contains the markup
14
+ // ...later: hs.destroy();
15
+
16
+ Config (all keys optional; pass as `window.HS_CONFIG` or the 2nd arg):
17
+
18
+ {
19
+ video: "media/journey-scrub.mp4", // ALL-INTRA encoded (-g 1) mp4
20
+ smoothing: 0.16, // scrub low-pass factor (1 = none)
21
+ motionBlur: true, // scroll-velocity blur on footage
22
+ fallbackClass: "hs-no-video", // body class when footage missing
23
+ telemetry: { // omit (null) to disable HUD math
24
+ max: 250, startP: 0.286, endP: 0.95, exponent: 0.45,
25
+ segments: [[0.0,"SEG 01"], [0.25,"SEG 02"]],
26
+ },
27
+ sweep: "page", // "page" (default): PLAY glides on to
28
+ // the document bottom and END jumps
29
+ // there (full cinematic ride);
30
+ // "hero": PLAY and END stop at the
31
+ // journey's end — you land on the
32
+ // regular section right after the
33
+ // hero, same as a normal scroll.
34
+ // (Names where autoplay/jumps end;
35
+ // deliberately NOT called "reach" so
36
+ // it never collides with route/DOM
37
+ // scope in a spec/plan.)
38
+ tailRate: 0.5, // autoplay px/s after journey,
39
+ // as a fraction of the viewport
40
+ // (sweep: "page" only)
41
+ veilFadeMs: 480, // veiled-cut fade duration
42
+ veilHoldMs: 450, // hold on black (sweep plays here)
43
+ }
44
+
45
+ Required markup (inside `root`; see hero-scroll.html / HeroScroll.tsx):
46
+ #hs-journey > .hs-stage > #hs-video the scrub stage
47
+ Optional markup — each feature activates only if its element exists:
48
+ #hs-loader (+ #hs-loader-fill, #hs-loader-pct) preload screen
49
+ #hs-hud (+ #hs-hud-value, #hs-hud-progress,
50
+ #hs-hud-seg, #hs-hud-pct) telemetry HUD
51
+ #hs-play (+ .hs-play-label) film-speed autoplay
52
+ #hs-top / #hs-end veiled jump nav
53
+ .hs-ovl[data-from][data-to] choreographed overlays
54
+ [data-hs-depth] children parallax drift
55
+ [data-hs-count][data-target][data-decimals] count-up stats
56
+ The #hs-veil element is injected automatically (and removed on destroy).
57
+
58
+ initHeroScroll(root, config) → { play, stop, veilTo, progress, destroy }
59
+ ═══════════════════════════════════════════════════════════════════ */
60
+
61
+ "use strict";
62
+
63
+ const DEFAULTS = {
64
+ video: "media/journey-scrub.mp4",
65
+ smoothing: 0.16,
66
+ motionBlur: true,
67
+ fallbackClass: "hs-no-video",
68
+ telemetry: null,
69
+ sweep: "page",
70
+ tailRate: 0.5,
71
+ veilFadeMs: 480,
72
+ veilHoldMs: 450,
73
+ };
74
+
75
+ /**
76
+ * Mount the hero-scroll engine against a root element (or `document`).
77
+ * Returns a controller; call `.destroy()` to fully tear down (React unmount).
78
+ */
79
+ export function initHeroScroll(root = document, userConfig = {}) {
80
+ const CFG = { ...DEFAULTS, ...userConfig };
81
+
82
+ const scope = root === document ? document : root;
83
+ const $ = (s) => scope.querySelector(s);
84
+ const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
85
+ const linear = (t) => t;
86
+ const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
87
+ const easeInOutCubic = (t) =>
88
+ t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
89
+ const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
90
+
91
+ const journey = $("#hs-journey");
92
+ const video = $("#hs-video");
93
+ if (!journey || !video) return { destroy() {} }; // nothing to drive
94
+
95
+ // optional chrome — every feature is presence-gated
96
+ const loaderFill = $("#hs-loader-fill");
97
+ const loaderPct = $("#hs-loader-pct");
98
+ const hud = $("#hs-hud");
99
+ const hudValue = $("#hs-hud-value");
100
+ const hudProgress = $("#hs-hud-progress");
101
+ const hudSeg = $("#hs-hud-seg");
102
+ const hudPct = $("#hs-hud-pct");
103
+ const playBtn = $("#hs-play");
104
+ const playLabel = playBtn && playBtn.querySelector(".hs-play-label");
105
+ const gotoTop = $("#hs-top");
106
+ const gotoEnd = $("#hs-end");
107
+
108
+ // the veil is engine-owned — inject it
109
+ const veil = document.createElement("div");
110
+ veil.id = "hs-veil";
111
+ veil.setAttribute("aria-hidden", "true");
112
+ document.body.appendChild(veil);
113
+
114
+ document.body.dataset.hsState = "loading";
115
+
116
+ /* ── scroll container resolution ───────────────────────────────────
117
+ CRITICAL for framework hosts: the page may scroll on `document.body`
118
+ or an overflow ancestor rather than the window (e.g. apps that set
119
+ `html, body { height: 100%; overflow-y: auto }`). Assuming the window
120
+ silently breaks scrubbing. Resolve the element that actually scrolls,
121
+ and read/write scroll through it. */
122
+
123
+ function resolveScroller(node) {
124
+ let el = node && node.parentElement;
125
+ while (el && el !== document.body && el !== document.documentElement) {
126
+ const oy = getComputedStyle(el).overflowY;
127
+ if ((oy === "auto" || oy === "scroll") && el.scrollHeight > el.clientHeight)
128
+ return el;
129
+ el = el.parentElement;
130
+ }
131
+ if (
132
+ document.body.scrollHeight > document.body.clientHeight + 1 &&
133
+ getComputedStyle(document.body).overflowY !== "visible"
134
+ )
135
+ return document.body;
136
+ return document.scrollingElement || document.documentElement;
137
+ }
138
+
139
+ const scroller = resolveScroller(journey);
140
+ const isDocScroller =
141
+ scroller === document.scrollingElement ||
142
+ scroller === document.documentElement ||
143
+ scroller === document.body;
144
+
145
+ const getY = () => scroller.scrollTop;
146
+ const setY = (y) => {
147
+ scroller.scrollTop = y;
148
+ };
149
+ const viewportH = () => (isDocScroller ? innerHeight : scroller.clientHeight);
150
+ const maxY = () => scroller.scrollHeight - viewportH();
151
+
152
+ /* ── preload the footage as a blob so seeking is instant ──────── */
153
+
154
+ let videoReady = false;
155
+ let duration = 0;
156
+ let objectUrl = null;
157
+ let alive = true;
158
+
159
+ function setLoader(p) {
160
+ const pct = Math.round(clamp(p, 0, 1) * 100);
161
+ if (loaderFill) loaderFill.style.width = pct + "%";
162
+ if (loaderPct) loaderPct.textContent = String(pct).padStart(3, "0");
163
+ }
164
+
165
+ async function preload() {
166
+ try {
167
+ const res = await fetch(CFG.video);
168
+ if (!res.ok) throw new Error("HTTP " + res.status);
169
+ const total = +res.headers.get("content-length") || 0;
170
+ const reader = res.body.getReader();
171
+ const chunks = [];
172
+ let received = 0;
173
+ for (;;) {
174
+ const { done, value } = await reader.read();
175
+ if (done) break;
176
+ if (!alive) return;
177
+ chunks.push(value);
178
+ received += value.length;
179
+ setLoader(total ? received / total : 0.5);
180
+ }
181
+ setLoader(1);
182
+ objectUrl = URL.createObjectURL(new Blob(chunks, { type: "video/mp4" }));
183
+ video.src = objectUrl;
184
+ await new Promise((ok, err) => {
185
+ video.addEventListener("loadedmetadata", ok, { once: true });
186
+ video.addEventListener("error", err, { once: true });
187
+ });
188
+ if (!alive) return;
189
+ duration = video.duration;
190
+ video.currentTime = 0;
191
+ videoReady = true;
192
+ } catch (e) {
193
+ document.body.classList.add(CFG.fallbackClass); // poster still shows
194
+ setLoader(1);
195
+ }
196
+ if (alive) document.body.dataset.hsState = "ready";
197
+ }
198
+
199
+ /* ── overlay windows ───────────────────────────────────────────── */
200
+
201
+ const overlays = [...scope.querySelectorAll(".hs-ovl")].map((el) => ({
202
+ el,
203
+ from: parseFloat(el.dataset.from),
204
+ to: parseFloat(el.dataset.to),
205
+ active: false,
206
+ parallax: [...el.querySelectorAll("[data-hs-depth]")],
207
+ counters: [...el.querySelectorAll("[data-hs-count]")],
208
+ counting: false,
209
+ }));
210
+
211
+ const FADE = 0.035; // progress-units to fade an overlay in/out
212
+
213
+ function runCounters(o) {
214
+ if (o.counting || !o.counters.length) return;
215
+ o.counting = true;
216
+ const t0 = performance.now();
217
+ const DUR = reduced ? 0 : 1400;
218
+ const tick = (now) => {
219
+ if (!alive || !o.counting) return;
220
+ const k = DUR ? easeInOutCubic(clamp((now - t0) / DUR, 0, 1)) : 1;
221
+ for (const c of o.counters) {
222
+ const target = parseFloat(c.dataset.target);
223
+ const dec = +(c.dataset.decimals || 0);
224
+ let v = (target * k).toFixed(dec);
225
+ if (c.dataset.group) v = (+v).toLocaleString("en-US");
226
+ c.textContent = v;
227
+ }
228
+ if (k < 1) requestAnimationFrame(tick);
229
+ };
230
+ requestAnimationFrame(tick);
231
+ }
232
+
233
+ function armCounters(o) {
234
+ o.counting = false;
235
+ for (const c of o.counters)
236
+ c.textContent = (0).toFixed(+(c.dataset.decimals || 0));
237
+ }
238
+
239
+ function styleOverlay(o, p) {
240
+ if (p < o.from - 0.01 || p > o.to + 0.01) {
241
+ if (o.el.style.visibility !== "hidden") {
242
+ o.el.style.visibility = "hidden";
243
+ o.el.style.opacity = "0";
244
+ }
245
+ if (o.active) {
246
+ o.active = false;
247
+ o.el.classList.remove("hs-active");
248
+ armCounters(o);
249
+ }
250
+ return;
251
+ }
252
+ // an overlay whose window starts at 0 must be fully risen at p = 0
253
+ const rise = o.from <= 0 ? 1 : clamp((p - o.from) / FADE, 0, 1);
254
+ const fall = clamp((o.to - p) / FADE, 0, 1);
255
+ const vis = Math.min(rise, fall);
256
+ const drift = (1 - rise) * 34 - (1 - fall) * 34;
257
+ const blur = reduced ? 0 : (1 - vis) * 14;
258
+
259
+ o.el.style.visibility = "visible";
260
+ o.el.style.opacity = vis.toFixed(3);
261
+ o.el.style.transform = `translateY(${drift.toFixed(1)}px)`;
262
+ o.el.style.filter = blur > 0.4 ? `blur(${blur.toFixed(1)}px)` : "none";
263
+
264
+ if (o.parallax.length && !reduced) {
265
+ const local = (p - o.from) / (o.to - o.from) - 0.5;
266
+ for (const m of o.parallax) {
267
+ const depth = parseFloat(m.dataset.hsDepth || "0.5");
268
+ m.style.transform = `translateY(${(-local * 60 * depth).toFixed(1)}px)`;
269
+ }
270
+ }
271
+
272
+ if (vis > 0.5 && !o.active) {
273
+ o.active = true;
274
+ o.el.classList.add("hs-active");
275
+ runCounters(o);
276
+ }
277
+ }
278
+
279
+ /* ── telemetry value curve ─────────────────────────────────────── */
280
+
281
+ const TEL = CFG.telemetry;
282
+
283
+ function valueAt(p) {
284
+ if (!TEL || p <= TEL.startP) return 0;
285
+ const span = Math.max(0.001, (TEL.endP ?? 0.95) - TEL.startP);
286
+ return (
287
+ TEL.max *
288
+ Math.pow(clamp((p - TEL.startP) / span, 0, 1), TEL.exponent ?? 0.45)
289
+ );
290
+ }
291
+
292
+ /* ── frame loop ────────────────────────────────────────────────── */
293
+
294
+ let smoothP = 0;
295
+ let lastP = 0;
296
+ let journeyTop = 0;
297
+ let journeyScroll = 1;
298
+ let frameRaf = null;
299
+
300
+ function measure() {
301
+ const sTop = isDocScroller ? 0 : scroller.getBoundingClientRect().top;
302
+ journeyTop = journey.getBoundingClientRect().top - sTop + getY();
303
+ journeyScroll = Math.max(1, journey.offsetHeight - viewportH());
304
+ }
305
+
306
+ // where PLAY/END consider "the end": journey end (sweep:"hero") or
307
+ // document bottom (sweep:"page")
308
+ const endY = () =>
309
+ CFG.sweep === "hero"
310
+ ? journeyTop + journey.offsetHeight - viewportH()
311
+ : maxY();
312
+
313
+ function frame() {
314
+ const y = clamp(getY() - journeyTop, 0, journeyScroll);
315
+ const p = y / journeyScroll;
316
+
317
+ smoothP += (p - smoothP) * (reduced ? 1 : CFG.smoothing);
318
+ if (Math.abs(p - smoothP) < 0.0004) smoothP = p;
319
+
320
+ if (videoReady && duration) {
321
+ const t = smoothP * Math.max(0, duration - 0.05);
322
+ if (Math.abs(t - video.currentTime) > 0.02) video.currentTime = t;
323
+ }
324
+
325
+ if (CFG.motionBlur && !reduced && videoReady) {
326
+ const blur = Math.min(5, Math.abs(smoothP - lastP) * 620);
327
+ video.style.filter = blur > 0.35 ? `blur(${blur.toFixed(2)}px)` : "none";
328
+ }
329
+ lastP = smoothP;
330
+
331
+ if (hud) {
332
+ if (hudValue) hudValue.textContent = Math.round(valueAt(smoothP));
333
+ if (hudProgress)
334
+ hudProgress.style.transform = `scaleX(${smoothP.toFixed(4)})`;
335
+ if (hudPct)
336
+ hudPct.textContent =
337
+ String(Math.round(smoothP * 100)).padStart(3, "0") + "%";
338
+ if (hudSeg && TEL && TEL.segments) {
339
+ let seg = TEL.segments[0][1];
340
+ for (const [at, label] of TEL.segments) if (smoothP >= at) seg = label;
341
+ hudSeg.textContent = seg;
342
+ }
343
+ hud.classList.toggle(
344
+ "hs-hud-off",
345
+ getY() > journeyTop + journey.offsetHeight - viewportH() * 0.55
346
+ );
347
+ }
348
+
349
+ if (gotoTop) gotoTop.classList.toggle("hs-nav-dim", getY() < 4);
350
+ if (gotoEnd) gotoEnd.classList.toggle("hs-nav-dim", getY() >= endY() - 4);
351
+
352
+ for (const o of overlays) styleOverlay(o, smoothP);
353
+
354
+ if (alive) frameRaf = requestAnimationFrame(frame);
355
+ }
356
+
357
+ /* ── veiled cut + film-speed autoplay ──────────────────────────── */
358
+
359
+ let autoActive = false;
360
+ let autoRaf = null;
361
+ let autoToken = 0; // invalidates in-flight timelines/veil callbacks
362
+ const timers = new Set();
363
+ const later = (fn, ms) => {
364
+ const id = setTimeout(() => {
365
+ timers.delete(id);
366
+ fn();
367
+ }, ms);
368
+ timers.add(id);
369
+ return id;
370
+ };
371
+
372
+ function stopAuto() {
373
+ autoToken += 1;
374
+ autoActive = false;
375
+ if (autoRaf !== null) cancelAnimationFrame(autoRaf);
376
+ autoRaf = null;
377
+ veil.classList.remove("hs-on");
378
+ if (playBtn) {
379
+ playBtn.classList.remove("hs-playing");
380
+ playBtn.setAttribute("aria-pressed", "false");
381
+ if (playLabel) playLabel.textContent = playLabel.dataset.idle || "PLAY";
382
+ }
383
+ }
384
+
385
+ // Fade to black, reposition + re-sync the scrub under the veil
386
+ // (no visible fast-scroll), lift, then optionally continue.
387
+ function veilTo(targetY, done) {
388
+ const token = ++autoToken;
389
+ veil.classList.add("hs-on");
390
+ later(() => {
391
+ if (token !== autoToken) return;
392
+ setY(targetY);
393
+ const p = clamp((targetY - journeyTop) / journeyScroll, 0, 1);
394
+ smoothP = p; // snap the smoothed scrub — no reverse/fast-forward flash
395
+ lastP = p;
396
+ if (videoReady && duration)
397
+ video.currentTime = p * Math.max(0, duration - 0.05);
398
+ later(() => {
399
+ if (token !== autoToken) return;
400
+ veil.classList.remove("hs-on");
401
+ if (done)
402
+ later(() => {
403
+ if (token === autoToken) done(token);
404
+ }, 200); // continue as the veil lifts
405
+ }, CFG.veilHoldMs);
406
+ }, CFG.veilFadeMs);
407
+ }
408
+
409
+ function runFrom(token) {
410
+ if (token !== autoToken) return;
411
+ const max = maxY();
412
+ const journeyEnd = journeyTop + journey.offsetHeight - viewportH();
413
+ const filmRate = journeyScroll / (videoReady && duration ? duration : 30);
414
+ const tailRate = Math.max(420, viewportH() * CFG.tailRate);
415
+
416
+ const segs = [];
417
+ let from = getY();
418
+ if (from < journeyEnd)
419
+ segs.push({
420
+ from,
421
+ to: journeyEnd,
422
+ dur: ((journeyEnd - from) / filmRate) * 1000, // real-time film speed
423
+ ease: linear,
424
+ });
425
+ if (CFG.sweep !== "hero" && Math.max(from, journeyEnd) < max)
426
+ segs.push({
427
+ from: Math.max(from, journeyEnd),
428
+ to: max,
429
+ dur: ((max - Math.max(from, journeyEnd)) / tailRate) * 1000,
430
+ ease: easeOutCubic,
431
+ });
432
+ if (!segs.length) {
433
+ stopAuto();
434
+ return;
435
+ }
436
+
437
+ let i = 0;
438
+ let t0 = performance.now();
439
+ const step = (now) => {
440
+ if (token !== autoToken) return;
441
+ const s = segs[i];
442
+ const k = s.dur > 0 ? clamp((now - t0) / s.dur, 0, 1) : 1;
443
+ setY(s.from + (s.to - s.from) * s.ease(k));
444
+ if (k >= 1) {
445
+ i += 1;
446
+ t0 = now;
447
+ if (i >= segs.length) {
448
+ stopAuto();
449
+ return;
450
+ }
451
+ }
452
+ autoRaf = requestAnimationFrame(step);
453
+ };
454
+ autoRaf = requestAnimationFrame(step);
455
+ }
456
+
457
+ function startAuto() {
458
+ autoActive = true;
459
+ if (playBtn) {
460
+ playBtn.classList.add("hs-playing");
461
+ playBtn.setAttribute("aria-pressed", "true");
462
+ if (playLabel) playLabel.textContent = playLabel.dataset.busy || "STOP";
463
+ }
464
+ if (getY() >= endY() - 8) {
465
+ veilTo(0, (token) => runFrom(token)); // replay: cut to black, re-arm
466
+ } else {
467
+ runFrom(++autoToken);
468
+ }
469
+ }
470
+
471
+ /* ── listeners (all tracked for teardown) ──────────────────────── */
472
+
473
+ const bound = [];
474
+ const on = (target, ev, fn, opts) => {
475
+ target.addEventListener(ev, fn, opts);
476
+ bound.push([target, ev, fn, opts]);
477
+ };
478
+
479
+ if (playBtn)
480
+ on(playBtn, "click", () => (autoActive ? stopAuto() : startAuto()));
481
+ if (gotoTop)
482
+ on(gotoTop, "click", () => {
483
+ stopAuto();
484
+ veilTo(0);
485
+ });
486
+ if (gotoEnd)
487
+ on(gotoEnd, "click", () => {
488
+ stopAuto();
489
+ veilTo(endY());
490
+ });
491
+
492
+ // any manual input hands control back to the viewer
493
+ const cancelInput = (e) => {
494
+ if (!autoActive) return;
495
+ const t = e.target; // guard: target may not be a Node (window)
496
+ if (
497
+ e.type === "keydown" &&
498
+ playBtn &&
499
+ t instanceof Node &&
500
+ playBtn.contains(t)
501
+ )
502
+ return; // Enter/Space on the button toggles via its click handler
503
+ stopAuto();
504
+ };
505
+ for (const ev of ["wheel", "touchstart", "keydown"])
506
+ on(window, ev, cancelInput, { passive: true });
507
+
508
+ // scroll can happen on window or an overflow element — listen on both
509
+ // (element scroll doesn't bubble to window; capture on document catches it).
510
+ const onResize = () => measure();
511
+ on(window, "resize", onResize);
512
+
513
+ /* ── go ────────────────────────────────────────────────────────── */
514
+
515
+ measure();
516
+ preload();
517
+ frameRaf = requestAnimationFrame(frame);
518
+
519
+ /* ── teardown ──────────────────────────────────────────────────── */
520
+
521
+ function destroy() {
522
+ alive = false;
523
+ autoToken += 1;
524
+ if (frameRaf !== null) cancelAnimationFrame(frameRaf);
525
+ if (autoRaf !== null) cancelAnimationFrame(autoRaf);
526
+ for (const id of timers) clearTimeout(id);
527
+ timers.clear();
528
+ for (const [target, ev, fn, opts] of bound)
529
+ target.removeEventListener(ev, fn, opts);
530
+ if (veil.parentNode) veil.parentNode.removeChild(veil);
531
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
532
+ delete document.body.dataset.hsState;
533
+ document.body.classList.remove(CFG.fallbackClass);
534
+ }
535
+
536
+ return { play: startAuto, stop: stopAuto, veilTo, progress: () => smoothP, destroy };
537
+ }
538
+
539
+ export default initHeroScroll;
540
+
541
+ /* ── plain-HTML auto-boot ───────────────────────────────────────────
542
+ When loaded as a module in a plain HTML page that sets window.HS_CONFIG
543
+ and has a #hs-journey, boot automatically against the document. Bundler/
544
+ React hosts import initHeroScroll directly and never set HS_CONFIG, so
545
+ this stays dormant there. */
546
+ if (
547
+ typeof window !== "undefined" &&
548
+ window.HS_CONFIG &&
549
+ typeof document !== "undefined" &&
550
+ document.getElementById &&
551
+ document.getElementById("hs-journey")
552
+ ) {
553
+ const boot = () => {
554
+ window.HS = initHeroScroll(document, window.HS_CONFIG);
555
+ };
556
+ if (document.readyState === "loading")
557
+ document.addEventListener("DOMContentLoaded", boot, { once: true });
558
+ else boot();
559
+ }