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