react-scroll-media 1.0.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/dist/index.js ADDED
@@ -0,0 +1,843 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
31
+
32
+ // src/index.ts
33
+ var src_exports = {};
34
+ __export(src_exports, {
35
+ ImageController: () => ImageController,
36
+ ScrollSequence: () => ScrollSequence,
37
+ ScrollText: () => ScrollText,
38
+ ScrollTimeline: () => ScrollTimeline,
39
+ ScrollTimelineProvider: () => ScrollTimelineProvider,
40
+ ScrollWordReveal: () => ScrollWordReveal,
41
+ clamp: () => clamp,
42
+ resolveSequence: () => resolveSequence,
43
+ useScrollSequence: () => useScrollSequence,
44
+ useScrollTimeline: () => useScrollTimeline
45
+ });
46
+ module.exports = __toCommonJS(src_exports);
47
+
48
+ // src/react/ScrollSequence.tsx
49
+ var import_react4 = __toESM(require("react"));
50
+
51
+ // src/react/useScrollSequence.ts
52
+ var import_react2 = require("react");
53
+
54
+ // src/controllers/imageController.ts
55
+ var ImageController = class {
56
+ constructor(config) {
57
+ __publicField(this, "canvas");
58
+ __publicField(this, "ctx");
59
+ __publicField(this, "frames");
60
+ __publicField(this, "imageCache", /* @__PURE__ */ new Map());
61
+ __publicField(this, "loadingPromises", /* @__PURE__ */ new Map());
62
+ __publicField(this, "currentFrameIndex", -1);
63
+ __publicField(this, "strategy");
64
+ __publicField(this, "bufferSize");
65
+ /**
66
+ * Create a new ImageController instance.
67
+ *
68
+ * @param config - Configuration object
69
+ * @throws If canvas doesn't support 2D context
70
+ */
71
+ __publicField(this, "isDestroyed", false);
72
+ this.canvas = config.canvas;
73
+ this.frames = config.frames;
74
+ this.strategy = config.strategy || "eager";
75
+ this.bufferSize = config.bufferSize || 10;
76
+ const ctx = this.canvas.getContext("2d");
77
+ if (!ctx) {
78
+ throw new Error("Failed to get 2D context from canvas");
79
+ }
80
+ this.ctx = ctx;
81
+ if (this.strategy === "eager") {
82
+ this.preloadAll();
83
+ } else {
84
+ this.ensureFrameWindow(0);
85
+ }
86
+ }
87
+ // ... preloadAll omitted for brevity if unchanged, but let's include for completeness if needed.
88
+ // Actually, we need to add guards to preloadFrame, so let's check it.
89
+ preloadAll() {
90
+ this.frames.forEach((_, index) => this.preloadFrame(index));
91
+ }
92
+ ensureFrameWindow(currentIndex) {
93
+ if (this.isDestroyed) return;
94
+ const radius = this.bufferSize;
95
+ const start = Math.max(0, currentIndex - radius);
96
+ const end = Math.min(this.frames.length - 1, currentIndex + radius);
97
+ const needed = /* @__PURE__ */ new Set();
98
+ for (let i = start; i <= end; i++) {
99
+ needed.add(this.frames[i]);
100
+ }
101
+ for (const [src] of this.imageCache) {
102
+ if (!needed.has(src)) {
103
+ this.imageCache.delete(src);
104
+ this.loadingPromises.delete(src);
105
+ }
106
+ }
107
+ for (let i = start; i <= end; i++) {
108
+ void this.preloadFrame(i);
109
+ }
110
+ }
111
+ async preloadFrame(index) {
112
+ if (this.isDestroyed || index < 0 || index >= this.frames.length) return;
113
+ const src = this.frames[index];
114
+ if (this.imageCache.has(src)) return;
115
+ if (!this.loadingPromises.has(src)) {
116
+ this.loadingPromises.set(src, this.loadImage(src));
117
+ }
118
+ try {
119
+ await this.loadingPromises.get(src);
120
+ } catch {
121
+ if (!this.isDestroyed) {
122
+ }
123
+ }
124
+ }
125
+ loadImage(src) {
126
+ return new Promise((resolve, reject) => {
127
+ const img = new Image();
128
+ img.onload = () => {
129
+ if (this.isDestroyed) return;
130
+ img.decode().then(() => {
131
+ if (this.isDestroyed) return;
132
+ this.imageCache.set(src, img);
133
+ resolve(img);
134
+ }).catch(() => {
135
+ if (this.isDestroyed) return;
136
+ this.imageCache.set(src, img);
137
+ resolve(img);
138
+ });
139
+ };
140
+ img.onerror = () => {
141
+ if (this.isDestroyed) return;
142
+ reject(new Error(`Failed to load image: ${src}`));
143
+ };
144
+ img.src = src;
145
+ });
146
+ }
147
+ update(progress) {
148
+ if (this.isDestroyed || this.frames.length === 0) return;
149
+ const frameIndex = Math.floor(progress * (this.frames.length - 1));
150
+ if (this.strategy === "lazy") {
151
+ this.ensureFrameWindow(frameIndex);
152
+ }
153
+ if (frameIndex === this.currentFrameIndex) return;
154
+ this.currentFrameIndex = frameIndex;
155
+ this.drawFrame(frameIndex);
156
+ }
157
+ drawFrame(index) {
158
+ if (this.isDestroyed || index < 0 || index >= this.frames.length) return;
159
+ const src = this.frames[index];
160
+ const img = this.imageCache.get(src);
161
+ if (!img) {
162
+ const promise = this.loadingPromises.get(src);
163
+ if (promise) {
164
+ promise.then(() => {
165
+ if (this.currentFrameIndex === index) {
166
+ this.drawFrame(index);
167
+ }
168
+ }).catch(() => {
169
+ });
170
+ }
171
+ return;
172
+ }
173
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
174
+ const scale = Math.min(
175
+ this.canvas.width / img.width,
176
+ this.canvas.height / img.height
177
+ );
178
+ const scaledWidth = img.width * scale;
179
+ const scaledHeight = img.height * scale;
180
+ const x = (this.canvas.width - scaledWidth) / 2;
181
+ const y = (this.canvas.height - scaledHeight) / 2;
182
+ this.ctx.drawImage(img, x, y, scaledWidth, scaledHeight);
183
+ }
184
+ setCanvasSize(width, height) {
185
+ if (this.isDestroyed) return;
186
+ this.canvas.width = width;
187
+ this.canvas.height = height;
188
+ if (this.currentFrameIndex >= 0) {
189
+ this.drawFrame(this.currentFrameIndex);
190
+ }
191
+ }
192
+ destroy() {
193
+ this.isDestroyed = true;
194
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
195
+ this.imageCache.clear();
196
+ this.loadingPromises.clear();
197
+ }
198
+ };
199
+
200
+ // src/sequence/sequenceResolver.ts
201
+ async function resolveSequence(source) {
202
+ switch (source.type) {
203
+ case "manual":
204
+ return processManualFrames(source.frames);
205
+ case "pattern":
206
+ return processPatternMode(source.url, source.start ?? 1, source.end, source.pad);
207
+ case "manifest":
208
+ return processManifestMode(source.url);
209
+ default:
210
+ return { frames: [], frameCount: 0 };
211
+ }
212
+ }
213
+ function processManualFrames(frames) {
214
+ const sorted = [...frames].sort((a, b) => {
215
+ const numA = extractNumber(a);
216
+ const numB = extractNumber(b);
217
+ return numA - numB;
218
+ });
219
+ return {
220
+ frames: sorted,
221
+ frameCount: sorted.length
222
+ };
223
+ }
224
+ function processPatternMode(pattern, start, end, pad) {
225
+ const frames = [];
226
+ for (let i = start; i <= end; i++) {
227
+ let indexStr = i.toString();
228
+ if (pad) {
229
+ indexStr = indexStr.padStart(pad, "0");
230
+ }
231
+ frames.push(pattern.replace("{index}", indexStr));
232
+ }
233
+ return {
234
+ frames,
235
+ frameCount: frames.length
236
+ };
237
+ }
238
+ var manifestCache = /* @__PURE__ */ new Map();
239
+ async function processManifestMode(url) {
240
+ if (manifestCache.has(url)) {
241
+ return manifestCache.get(url);
242
+ }
243
+ const promise = (async () => {
244
+ try {
245
+ const res = await fetch(url);
246
+ if (!res.ok) {
247
+ throw new Error(`Failed to fetch manifest: ${res.statusText}`);
248
+ }
249
+ const data = await res.json();
250
+ if (data.frames && Array.isArray(data.frames)) {
251
+ return processManualFrames(data.frames);
252
+ }
253
+ if (data.pattern && typeof data.end === "number") {
254
+ const start = data.start ?? 1;
255
+ const pad = data.pad;
256
+ return processPatternMode(data.pattern, start, data.end, pad);
257
+ }
258
+ return { frames: [], frameCount: 0 };
259
+ } catch (err) {
260
+ manifestCache.delete(url);
261
+ throw err;
262
+ }
263
+ })();
264
+ manifestCache.set(url, promise);
265
+ return promise;
266
+ }
267
+ function extractNumber(filename) {
268
+ const match = filename.match(/\d+/);
269
+ return match ? parseInt(match[0], 10) : -1;
270
+ }
271
+
272
+ // src/core/clamp.ts
273
+ function clamp(value, min = 0, max = 1) {
274
+ return Math.max(min, Math.min(max, value));
275
+ }
276
+
277
+ // src/react/scrollTimelineContext.ts
278
+ var import_react = require("react");
279
+ var ScrollTimelineContext = (0, import_react.createContext)({
280
+ timeline: null
281
+ });
282
+ function useTimelineContext() {
283
+ return (0, import_react.useContext)(ScrollTimelineContext);
284
+ }
285
+
286
+ // src/react/useScrollTimeline.ts
287
+ function useScrollTimeline() {
288
+ const { timeline } = useTimelineContext();
289
+ const subscribe = (callback) => {
290
+ if (!timeline) return () => {
291
+ };
292
+ return timeline.subscribe(callback);
293
+ };
294
+ return { subscribe, timeline };
295
+ }
296
+
297
+ // src/react/useScrollSequence.ts
298
+ function useScrollSequence({
299
+ source,
300
+ debugRef,
301
+ memoryStrategy = "eager",
302
+ lazyBuffer = 10,
303
+ onError
304
+ }) {
305
+ const canvasRef = (0, import_react2.useRef)(null);
306
+ const controllerRef = (0, import_react2.useRef)(null);
307
+ const { subscribe } = useScrollTimeline();
308
+ const [isLoaded, setIsLoaded] = (0, import_react2.useState)(false);
309
+ const [error, setError] = (0, import_react2.useState)(null);
310
+ (0, import_react2.useEffect)(() => {
311
+ let active = true;
312
+ let currentController = null;
313
+ let unsubscribeTimeline = null;
314
+ const init = async () => {
315
+ setIsLoaded(false);
316
+ setError(null);
317
+ const canvas = canvasRef.current;
318
+ if (!canvas) return;
319
+ try {
320
+ if (typeof window === "undefined") return;
321
+ const sequence = await resolveSequence(source);
322
+ if (!active) return;
323
+ if (sequence.frames.length === 0) {
324
+ return;
325
+ }
326
+ if (typeof window !== "undefined") {
327
+ canvas.width = window.innerWidth;
328
+ canvas.height = window.innerHeight;
329
+ }
330
+ currentController = new ImageController({
331
+ canvas,
332
+ frames: sequence.frames,
333
+ strategy: memoryStrategy,
334
+ bufferSize: lazyBuffer
335
+ });
336
+ controllerRef.current = currentController;
337
+ unsubscribeTimeline = subscribe((progress) => {
338
+ if (!currentController) return;
339
+ const clamped = clamp(progress);
340
+ currentController.update(clamped);
341
+ if (debugRef?.current) {
342
+ const frameIndex = Math.floor(clamped * (sequence.frames.length - 1));
343
+ debugRef.current.innerText = `Progress: ${clamped.toFixed(2)}
344
+ Frame: ${frameIndex + 1} / ${sequence.frames.length}`;
345
+ }
346
+ });
347
+ if (active) setIsLoaded(true);
348
+ } catch (err) {
349
+ if (active) {
350
+ const e = err instanceof Error ? err : new Error("Unknown initialization error");
351
+ setError(e);
352
+ if (onError) onError(e);
353
+ }
354
+ }
355
+ };
356
+ init();
357
+ return () => {
358
+ active = false;
359
+ currentController?.destroy();
360
+ controllerRef.current = null;
361
+ if (unsubscribeTimeline) unsubscribeTimeline();
362
+ };
363
+ }, [source, memoryStrategy, lazyBuffer, subscribe]);
364
+ return {
365
+ canvasRef,
366
+ isLoaded,
367
+ error
368
+ };
369
+ }
370
+
371
+ // src/react/ScrollTimelineProvider.tsx
372
+ var import_react3 = __toESM(require("react"));
373
+
374
+ // src/core/loopManager.ts
375
+ var _ScrollLoopManager = class _ScrollLoopManager {
376
+ constructor() {
377
+ __publicField(this, "callbacks", /* @__PURE__ */ new Set());
378
+ __publicField(this, "rafId", null);
379
+ __publicField(this, "isActive", false);
380
+ __publicField(this, "tick", () => {
381
+ if (!this.isActive) return;
382
+ this.callbacks.forEach((cb) => {
383
+ try {
384
+ cb();
385
+ } catch (e) {
386
+ }
387
+ });
388
+ this.rafId = requestAnimationFrame(this.tick);
389
+ });
390
+ }
391
+ static getInstance() {
392
+ if (!_ScrollLoopManager.instance) {
393
+ _ScrollLoopManager.instance = new _ScrollLoopManager();
394
+ }
395
+ return _ScrollLoopManager.instance;
396
+ }
397
+ /**
398
+ * Register a callback to be called on every animation frame.
399
+ */
400
+ register(callback) {
401
+ if (this.callbacks.has(callback)) return;
402
+ this.callbacks.add(callback);
403
+ if (this.callbacks.size === 1) {
404
+ this.start();
405
+ }
406
+ }
407
+ /**
408
+ * Unregister a callback.
409
+ */
410
+ unregister(callback) {
411
+ this.callbacks.delete(callback);
412
+ if (this.callbacks.size === 0) {
413
+ this.stop();
414
+ }
415
+ }
416
+ start() {
417
+ if (this.isActive) return;
418
+ this.isActive = true;
419
+ if (typeof window !== "undefined") {
420
+ this.tick();
421
+ }
422
+ }
423
+ stop() {
424
+ this.isActive = false;
425
+ if (this.rafId !== null && typeof window !== "undefined") {
426
+ cancelAnimationFrame(this.rafId);
427
+ this.rafId = null;
428
+ }
429
+ }
430
+ };
431
+ __publicField(_ScrollLoopManager, "instance");
432
+ var ScrollLoopManager = _ScrollLoopManager;
433
+
434
+ // src/constants.ts
435
+ var SCROLL_THRESHOLD = 1e-4;
436
+
437
+ // src/core/scrollTimeline.ts
438
+ var ScrollTimeline = class {
439
+ constructor(container) {
440
+ __publicField(this, "container");
441
+ __publicField(this, "subscribers", /* @__PURE__ */ new Set());
442
+ __publicField(this, "currentProgress", 0);
443
+ // Caching for performance
444
+ __publicField(this, "cachedRect", null);
445
+ __publicField(this, "cachedScrollParent", null);
446
+ __publicField(this, "cachedScrollParentRect", null);
447
+ __publicField(this, "cachedViewportHeight", 0);
448
+ __publicField(this, "cachedOffsetTop", 0);
449
+ __publicField(this, "isLayoutDirty", true);
450
+ __publicField(this, "resizeObserver", null);
451
+ __publicField(this, "id", typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2, 9));
452
+ __publicField(this, "onWindowResize", () => {
453
+ this.isLayoutDirty = true;
454
+ });
455
+ __publicField(this, "tick", () => {
456
+ const progress = this.calculateProgress();
457
+ if (Math.abs(progress - this.currentProgress) > SCROLL_THRESHOLD) {
458
+ this.currentProgress = progress;
459
+ this.notify();
460
+ }
461
+ });
462
+ this.container = container;
463
+ if (typeof window !== "undefined") {
464
+ this.resizeObserver = new ResizeObserver(() => {
465
+ this.isLayoutDirty = true;
466
+ });
467
+ this.resizeObserver.observe(this.container);
468
+ if (document.body) {
469
+ this.resizeObserver.observe(document.body);
470
+ }
471
+ window.addEventListener("resize", this.onWindowResize);
472
+ }
473
+ }
474
+ /**
475
+ * Subscribe to progress updates.
476
+ * Returns an unsubscribe function.
477
+ */
478
+ subscribe(callback) {
479
+ this.subscribers.add(callback);
480
+ try {
481
+ callback(this.currentProgress);
482
+ } catch (e) {
483
+ }
484
+ if (this.subscribers.size === 1) {
485
+ ScrollLoopManager.getInstance().register(this.tick);
486
+ }
487
+ return () => {
488
+ this.subscribers.delete(callback);
489
+ if (this.subscribers.size === 0) {
490
+ ScrollLoopManager.getInstance().unregister(this.tick);
491
+ }
492
+ };
493
+ }
494
+ unsubscribe(callback) {
495
+ this.subscribers.delete(callback);
496
+ if (this.subscribers.size === 0) {
497
+ ScrollLoopManager.getInstance().unregister(this.tick);
498
+ }
499
+ }
500
+ /**
501
+ * Start is now handled by LoopManager via subscriptions
502
+ * Deprecated but kept for API stability if needed.
503
+ */
504
+ start() {
505
+ }
506
+ stop() {
507
+ ScrollLoopManager.getInstance().unregister(this.tick);
508
+ }
509
+ notify() {
510
+ this.subscribers.forEach((cb) => {
511
+ try {
512
+ cb(this.currentProgress);
513
+ } catch (e) {
514
+ }
515
+ });
516
+ }
517
+ updateCache() {
518
+ if (!this.isLayoutDirty && this.cachedRect) return;
519
+ this.cachedRect = this.container.getBoundingClientRect();
520
+ if (!this.cachedScrollParent) {
521
+ this.cachedScrollParent = this.getScrollParent(this.container);
522
+ }
523
+ if (this.cachedScrollParent instanceof Element) {
524
+ this.cachedScrollParentRect = this.cachedScrollParent.getBoundingClientRect();
525
+ this.cachedViewportHeight = this.cachedScrollParentRect.height;
526
+ this.cachedOffsetTop = this.cachedScrollParentRect.top;
527
+ } else if (typeof window !== "undefined") {
528
+ this.cachedViewportHeight = window.innerHeight;
529
+ this.cachedOffsetTop = 0;
530
+ }
531
+ this.isLayoutDirty = false;
532
+ }
533
+ calculateProgress() {
534
+ if (this.isLayoutDirty || !this.cachedRect) {
535
+ this.updateCache();
536
+ }
537
+ const currentRect = this.container.getBoundingClientRect();
538
+ const scrollDist = (this.cachedRect?.height || currentRect.height) - this.cachedViewportHeight;
539
+ if (scrollDist <= 0) return 1;
540
+ const relativeTop = currentRect.top - this.cachedOffsetTop;
541
+ const rawProgress = -relativeTop / scrollDist;
542
+ const clamped = Math.min(Math.max(rawProgress, 0), 1);
543
+ return Math.round(clamped * 1e6) / 1e6;
544
+ }
545
+ getScrollParent(node) {
546
+ if (typeof window === "undefined") return node;
547
+ let current = node.parentElement;
548
+ while (current) {
549
+ const style = getComputedStyle(current);
550
+ if (["auto", "scroll"].includes(style.overflowY)) {
551
+ return current;
552
+ }
553
+ current = current.parentElement;
554
+ }
555
+ return window;
556
+ }
557
+ destroy() {
558
+ this.subscribers.clear();
559
+ ScrollLoopManager.getInstance().unregister(this.tick);
560
+ if (this.resizeObserver) {
561
+ this.resizeObserver.disconnect();
562
+ }
563
+ if (typeof window !== "undefined") {
564
+ window.removeEventListener("resize", this.onWindowResize);
565
+ }
566
+ }
567
+ };
568
+
569
+ // src/react/ScrollTimelineProvider.tsx
570
+ var import_jsx_runtime = require("react/jsx-runtime");
571
+ function ScrollTimelineProvider({
572
+ children,
573
+ scrollLength = "300vh",
574
+ className = "",
575
+ style = {}
576
+ }) {
577
+ const containerRef = (0, import_react3.useRef)(null);
578
+ const [timeline, setTimeline] = (0, import_react3.useState)(null);
579
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? import_react3.default.useLayoutEffect : import_react3.default.useEffect;
580
+ useIsomorphicLayoutEffect(() => {
581
+ if (typeof window === "undefined") return;
582
+ if (!containerRef.current) return;
583
+ const instance = new ScrollTimeline(containerRef.current);
584
+ setTimeline(instance);
585
+ return () => {
586
+ instance.destroy();
587
+ setTimeline(null);
588
+ };
589
+ }, []);
590
+ const containerStyle = {
591
+ height: scrollLength,
592
+ position: "relative",
593
+ width: "100%",
594
+ ...style
595
+ };
596
+ const stickyWrapperStyle = {
597
+ position: "sticky",
598
+ top: 0,
599
+ height: "100vh",
600
+ width: "100%",
601
+ overflow: "hidden"
602
+ };
603
+ const contextValue = import_react3.default.useMemo(() => ({ timeline }), [timeline]);
604
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ScrollTimelineContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
605
+ "div",
606
+ {
607
+ ref: containerRef,
608
+ className,
609
+ style: containerStyle,
610
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: stickyWrapperStyle, children })
611
+ }
612
+ ) });
613
+ }
614
+
615
+ // src/react/ScrollSequence.tsx
616
+ var import_jsx_runtime2 = require("react/jsx-runtime");
617
+ var InnerSequence = ({
618
+ source,
619
+ debug,
620
+ memoryStrategy,
621
+ lazyBuffer,
622
+ accessibilityLabel = "Scroll sequence",
623
+ fallback,
624
+ onError
625
+ }) => {
626
+ const debugRef = (0, import_react4.useRef)(null);
627
+ const { canvasRef, isLoaded } = useScrollSequence({
628
+ source,
629
+ debugRef,
630
+ memoryStrategy,
631
+ lazyBuffer,
632
+ onError
633
+ });
634
+ const canvasStyle = {
635
+ display: "block",
636
+ width: "100%",
637
+ height: "100%",
638
+ objectFit: "cover",
639
+ opacity: isLoaded ? 1 : 0,
640
+ transition: "opacity 0.2s ease-in"
641
+ };
642
+ const debugStyle = {
643
+ position: "absolute",
644
+ top: "10px",
645
+ left: "10px",
646
+ background: "rgba(0, 0, 0, 0.7)",
647
+ color: "#00ff00",
648
+ padding: "8px",
649
+ borderRadius: "4px",
650
+ fontFamily: "monospace",
651
+ fontSize: "12px",
652
+ pointerEvents: "none",
653
+ whiteSpace: "pre-wrap",
654
+ zIndex: 9999
655
+ };
656
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
657
+ !isLoaded && fallback && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { position: "absolute", inset: 0, zIndex: 1 }, children: fallback }),
658
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
659
+ "canvas",
660
+ {
661
+ ref: canvasRef,
662
+ style: canvasStyle,
663
+ role: "img",
664
+ "aria-label": accessibilityLabel
665
+ }
666
+ ),
667
+ debug && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: debugRef, style: debugStyle, children: "Waiting for scroll..." })
668
+ ] });
669
+ };
670
+ var ScrollSequence = import_react4.default.forwardRef(
671
+ (props, ref) => {
672
+ const {
673
+ source,
674
+ scrollLength = "300vh",
675
+ className = "",
676
+ debug = false,
677
+ memoryStrategy = "eager",
678
+ lazyBuffer = 10,
679
+ fallback,
680
+ accessibilityLabel,
681
+ onError
682
+ } = props;
683
+ const prefersReducedMotion = import_react4.default.useMemo(() => {
684
+ if (typeof window !== "undefined") {
685
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
686
+ }
687
+ return false;
688
+ }, []);
689
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref, className, style: { width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(ScrollTimelineProvider, { scrollLength, children: [
690
+ prefersReducedMotion && fallback ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { position: "sticky", top: 0, height: "100vh", width: "100%" }, children: fallback }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
691
+ InnerSequence,
692
+ {
693
+ source,
694
+ debug,
695
+ memoryStrategy,
696
+ lazyBuffer,
697
+ fallback,
698
+ accessibilityLabel,
699
+ onError
700
+ }
701
+ ),
702
+ props.children
703
+ ] }) });
704
+ }
705
+ );
706
+
707
+ // src/react/ScrollText.tsx
708
+ var import_react5 = require("react");
709
+ var import_jsx_runtime3 = require("react/jsx-runtime");
710
+ function ScrollText({
711
+ children,
712
+ start = 0,
713
+ end = 0.2,
714
+ exitStart,
715
+ exitEnd,
716
+ initialOpacity = 0,
717
+ targetOpacity = 1,
718
+ finalOpacity = 0,
719
+ translateY = 50,
720
+ style,
721
+ className
722
+ }) {
723
+ const ref = (0, import_react5.useRef)(null);
724
+ const { subscribe } = useScrollTimeline();
725
+ (0, import_react5.useEffect)(() => {
726
+ if (typeof window === "undefined") return;
727
+ const unsubscribe = subscribe((progress) => {
728
+ if (!ref.current) return;
729
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
730
+ const effectiveTranslateY = prefersReducedMotion ? 0 : translateY;
731
+ let opacity = initialOpacity;
732
+ let currentY = effectiveTranslateY;
733
+ if (progress < start) {
734
+ opacity = initialOpacity;
735
+ currentY = effectiveTranslateY;
736
+ } else if (progress >= start && progress <= end) {
737
+ const local = (progress - start) / (end - start);
738
+ opacity = initialOpacity + (targetOpacity - initialOpacity) * local;
739
+ currentY = effectiveTranslateY * (1 - local);
740
+ } else if (!exitStart || progress < exitStart) {
741
+ opacity = targetOpacity;
742
+ currentY = 0;
743
+ } else if (exitStart && exitEnd && progress >= exitStart && progress <= exitEnd) {
744
+ const local = (progress - exitStart) / (exitEnd - exitStart);
745
+ opacity = targetOpacity + (finalOpacity - targetOpacity) * local;
746
+ currentY = -effectiveTranslateY * local;
747
+ } else {
748
+ opacity = finalOpacity;
749
+ currentY = -effectiveTranslateY;
750
+ }
751
+ ref.current.style.opacity = opacity.toFixed(3);
752
+ ref.current.style.transform = `translateY(${currentY}px)`;
753
+ });
754
+ return unsubscribe;
755
+ }, [subscribe, start, end, exitStart, exitEnd, initialOpacity, targetOpacity, finalOpacity, translateY]);
756
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
757
+ "div",
758
+ {
759
+ ref,
760
+ className,
761
+ style: {
762
+ opacity: initialOpacity,
763
+ transform: `translateY(${translateY}px)`,
764
+ transition: "none",
765
+ // Critical: no CSS transition fighting JS
766
+ willChange: "opacity, transform",
767
+ ...style
768
+ },
769
+ children
770
+ }
771
+ );
772
+ }
773
+
774
+ // src/react/ScrollWordReveal.tsx
775
+ var import_react6 = require("react");
776
+ var import_jsx_runtime4 = require("react/jsx-runtime");
777
+ function ScrollWordReveal({
778
+ text,
779
+ start = 0,
780
+ end = 1,
781
+ className,
782
+ style
783
+ }) {
784
+ const containerRef = (0, import_react6.useRef)(null);
785
+ const { subscribe } = useScrollTimeline();
786
+ const words = text.split(/\s+/);
787
+ (0, import_react6.useEffect)(() => {
788
+ const unsubscribe = subscribe((globalProgress) => {
789
+ if (!containerRef.current) return;
790
+ const spans = containerRef.current.children;
791
+ let localProgress = 0;
792
+ if (globalProgress <= start) localProgress = 0;
793
+ else if (globalProgress >= end) localProgress = 1;
794
+ else localProgress = (globalProgress - start) / (end - start);
795
+ const totalWords = spans.length;
796
+ const progressPerWord = 1 / totalWords;
797
+ for (let i = 0; i < totalWords; i++) {
798
+ const span = spans[i];
799
+ const wordStart = i * progressPerWord;
800
+ const wordEnd = (i + 1) * progressPerWord;
801
+ let wordOpacity = 0;
802
+ if (localProgress >= wordEnd) {
803
+ wordOpacity = 1;
804
+ } else if (localProgress <= wordStart) {
805
+ wordOpacity = 0.1;
806
+ } else {
807
+ wordOpacity = 0.1 + 0.9 * ((localProgress - wordStart) / (wordEnd - wordStart));
808
+ }
809
+ span.style.opacity = wordOpacity.toFixed(2);
810
+ const translate = (1 - wordOpacity) * 10;
811
+ span.style.transform = `translateY(${translate}px)`;
812
+ }
813
+ });
814
+ return unsubscribe;
815
+ }, [subscribe, start, end]);
816
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: containerRef, className, style: { ...style, display: "flex", flexWrap: "wrap", gap: "0.25em" }, children: words.map((word, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
817
+ "span",
818
+ {
819
+ style: {
820
+ opacity: 0.1,
821
+ transform: "translateY(10px)",
822
+ transition: "none",
823
+ willChange: "opacity, transform"
824
+ },
825
+ children: word
826
+ },
827
+ i
828
+ )) });
829
+ }
830
+ // Annotate the CommonJS export names for ESM import in node:
831
+ 0 && (module.exports = {
832
+ ImageController,
833
+ ScrollSequence,
834
+ ScrollText,
835
+ ScrollTimeline,
836
+ ScrollTimelineProvider,
837
+ ScrollWordReveal,
838
+ clamp,
839
+ resolveSequence,
840
+ useScrollSequence,
841
+ useScrollTimeline
842
+ });
843
+ //# sourceMappingURL=index.js.map