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.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
|