@buildcores/render-client 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.esm.js +11 -181
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +75 -245
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +28 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
-
var
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var framerMotion = require('framer-motion');
|
|
5
6
|
|
|
6
7
|
// Helper to extract clientX from mouse or touch events
|
|
7
8
|
const getClientX$1 = (e) => {
|
|
@@ -20,13 +21,13 @@ const calculateCircularFrame = (startFrame, deltaX, sensitivity, totalFrames) =>
|
|
|
20
21
|
};
|
|
21
22
|
const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
|
|
22
23
|
const { mouseSensitivity = 0.1, touchSensitivity = 0.1, onFrameChange, } = options;
|
|
23
|
-
const [isDragging, setIsDragging] =
|
|
24
|
-
const [dragStartX, setDragStartX] =
|
|
25
|
-
const [dragStartFrame, setDragStartFrame] =
|
|
26
|
-
const hasDragged =
|
|
27
|
-
const currentFrame =
|
|
24
|
+
const [isDragging, setIsDragging] = react.useState(false);
|
|
25
|
+
const [dragStartX, setDragStartX] = react.useState(0);
|
|
26
|
+
const [dragStartFrame, setDragStartFrame] = react.useState(0);
|
|
27
|
+
const hasDragged = react.useRef(false);
|
|
28
|
+
const currentFrame = react.useRef(0);
|
|
28
29
|
// Helper to start dragging (common logic for mouse and touch)
|
|
29
|
-
const startDrag =
|
|
30
|
+
const startDrag = react.useCallback((clientX, event) => {
|
|
30
31
|
if (!canvasRef.current)
|
|
31
32
|
return;
|
|
32
33
|
setIsDragging(true);
|
|
@@ -36,7 +37,7 @@ const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
|
|
|
36
37
|
event.preventDefault();
|
|
37
38
|
}, [canvasRef]);
|
|
38
39
|
// Helper to handle drag movement (common logic for mouse and touch)
|
|
39
|
-
const handleDragMove =
|
|
40
|
+
const handleDragMove = react.useCallback((clientX, sensitivity) => {
|
|
40
41
|
if (!isDragging || !canvasRef.current)
|
|
41
42
|
return;
|
|
42
43
|
const deltaX = clientX - dragStartX;
|
|
@@ -50,29 +51,29 @@ const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
|
|
|
50
51
|
return newFrame;
|
|
51
52
|
}, [isDragging, dragStartX, dragStartFrame, totalFrames, onFrameChange]);
|
|
52
53
|
// Helper to end dragging (common logic for mouse and touch)
|
|
53
|
-
const endDrag =
|
|
54
|
+
const endDrag = react.useCallback(() => {
|
|
54
55
|
setIsDragging(false);
|
|
55
56
|
}, []);
|
|
56
|
-
const handleMouseDown =
|
|
57
|
+
const handleMouseDown = react.useCallback((e) => {
|
|
57
58
|
startDrag(e.clientX, e.nativeEvent);
|
|
58
59
|
}, [startDrag]);
|
|
59
|
-
const handleTouchStart =
|
|
60
|
+
const handleTouchStart = react.useCallback((e) => {
|
|
60
61
|
startDrag(e.touches[0].clientX, e.nativeEvent);
|
|
61
62
|
}, [startDrag]);
|
|
62
|
-
const handleDocumentMouseMove =
|
|
63
|
+
const handleDocumentMouseMove = react.useCallback((e) => {
|
|
63
64
|
return handleDragMove(getClientX$1(e), mouseSensitivity);
|
|
64
65
|
}, [handleDragMove, mouseSensitivity]);
|
|
65
|
-
const handleDocumentTouchMove =
|
|
66
|
+
const handleDocumentTouchMove = react.useCallback((e) => {
|
|
66
67
|
return handleDragMove(getClientX$1(e), touchSensitivity);
|
|
67
68
|
}, [handleDragMove, touchSensitivity]);
|
|
68
|
-
const handleDocumentMouseUp =
|
|
69
|
+
const handleDocumentMouseUp = react.useCallback(() => {
|
|
69
70
|
endDrag();
|
|
70
71
|
}, [endDrag]);
|
|
71
|
-
const handleDocumentTouchEnd =
|
|
72
|
+
const handleDocumentTouchEnd = react.useCallback(() => {
|
|
72
73
|
endDrag();
|
|
73
74
|
}, [endDrag]);
|
|
74
75
|
// Add document-level event listeners when dragging starts
|
|
75
|
-
|
|
76
|
+
react.useEffect(() => {
|
|
76
77
|
if (isDragging) {
|
|
77
78
|
document.addEventListener("mousemove", handleDocumentMouseMove);
|
|
78
79
|
document.addEventListener("mouseup", handleDocumentMouseUp);
|
|
@@ -104,191 +105,11 @@ const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
|
|
|
104
105
|
};
|
|
105
106
|
};
|
|
106
107
|
|
|
107
|
-
/**
|
|
108
|
-
* @public
|
|
109
|
-
*/
|
|
110
|
-
const MotionConfigContext = React.createContext({
|
|
111
|
-
transformPagePoint: (p) => p,
|
|
112
|
-
isStatic: false,
|
|
113
|
-
reducedMotion: "never",
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
/*#__NO_SIDE_EFFECTS__*/
|
|
117
|
-
const noop = (any) => any;
|
|
118
|
-
|
|
119
|
-
if (process.env.NODE_ENV !== "production") ;
|
|
120
|
-
|
|
121
|
-
function createRenderStep(runNextFrame) {
|
|
122
|
-
/**
|
|
123
|
-
* We create and reuse two queues, one to queue jobs for the current frame
|
|
124
|
-
* and one for the next. We reuse to avoid triggering GC after x frames.
|
|
125
|
-
*/
|
|
126
|
-
let thisFrame = new Set();
|
|
127
|
-
let nextFrame = new Set();
|
|
128
|
-
/**
|
|
129
|
-
* Track whether we're currently processing jobs in this step. This way
|
|
130
|
-
* we can decide whether to schedule new jobs for this frame or next.
|
|
131
|
-
*/
|
|
132
|
-
let isProcessing = false;
|
|
133
|
-
let flushNextFrame = false;
|
|
134
|
-
/**
|
|
135
|
-
* A set of processes which were marked keepAlive when scheduled.
|
|
136
|
-
*/
|
|
137
|
-
const toKeepAlive = new WeakSet();
|
|
138
|
-
let latestFrameData = {
|
|
139
|
-
delta: 0.0,
|
|
140
|
-
timestamp: 0.0,
|
|
141
|
-
isProcessing: false,
|
|
142
|
-
};
|
|
143
|
-
function triggerCallback(callback) {
|
|
144
|
-
if (toKeepAlive.has(callback)) {
|
|
145
|
-
step.schedule(callback);
|
|
146
|
-
runNextFrame();
|
|
147
|
-
}
|
|
148
|
-
callback(latestFrameData);
|
|
149
|
-
}
|
|
150
|
-
const step = {
|
|
151
|
-
/**
|
|
152
|
-
* Schedule a process to run on the next frame.
|
|
153
|
-
*/
|
|
154
|
-
schedule: (callback, keepAlive = false, immediate = false) => {
|
|
155
|
-
const addToCurrentFrame = immediate && isProcessing;
|
|
156
|
-
const queue = addToCurrentFrame ? thisFrame : nextFrame;
|
|
157
|
-
if (keepAlive)
|
|
158
|
-
toKeepAlive.add(callback);
|
|
159
|
-
if (!queue.has(callback))
|
|
160
|
-
queue.add(callback);
|
|
161
|
-
return callback;
|
|
162
|
-
},
|
|
163
|
-
/**
|
|
164
|
-
* Cancel the provided callback from running on the next frame.
|
|
165
|
-
*/
|
|
166
|
-
cancel: (callback) => {
|
|
167
|
-
nextFrame.delete(callback);
|
|
168
|
-
toKeepAlive.delete(callback);
|
|
169
|
-
},
|
|
170
|
-
/**
|
|
171
|
-
* Execute all schedule callbacks.
|
|
172
|
-
*/
|
|
173
|
-
process: (frameData) => {
|
|
174
|
-
latestFrameData = frameData;
|
|
175
|
-
/**
|
|
176
|
-
* If we're already processing we've probably been triggered by a flushSync
|
|
177
|
-
* inside an existing process. Instead of executing, mark flushNextFrame
|
|
178
|
-
* as true and ensure we flush the following frame at the end of this one.
|
|
179
|
-
*/
|
|
180
|
-
if (isProcessing) {
|
|
181
|
-
flushNextFrame = true;
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
isProcessing = true;
|
|
185
|
-
[thisFrame, nextFrame] = [nextFrame, thisFrame];
|
|
186
|
-
// Execute this frame
|
|
187
|
-
thisFrame.forEach(triggerCallback);
|
|
188
|
-
// Clear the frame so no callbacks remain. This is to avoid
|
|
189
|
-
// memory leaks should this render step not run for a while.
|
|
190
|
-
thisFrame.clear();
|
|
191
|
-
isProcessing = false;
|
|
192
|
-
if (flushNextFrame) {
|
|
193
|
-
flushNextFrame = false;
|
|
194
|
-
step.process(frameData);
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
return step;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const stepsOrder = [
|
|
202
|
-
"read", // Read
|
|
203
|
-
"resolveKeyframes", // Write/Read/Write/Read
|
|
204
|
-
"update", // Compute
|
|
205
|
-
"preRender", // Compute
|
|
206
|
-
"render", // Write
|
|
207
|
-
"postRender", // Compute
|
|
208
|
-
];
|
|
209
|
-
const maxElapsed = 40;
|
|
210
|
-
function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
|
|
211
|
-
let runNextFrame = false;
|
|
212
|
-
let useDefaultElapsed = true;
|
|
213
|
-
const state = {
|
|
214
|
-
delta: 0.0,
|
|
215
|
-
timestamp: 0.0,
|
|
216
|
-
isProcessing: false,
|
|
217
|
-
};
|
|
218
|
-
const flagRunNextFrame = () => (runNextFrame = true);
|
|
219
|
-
const steps = stepsOrder.reduce((acc, key) => {
|
|
220
|
-
acc[key] = createRenderStep(flagRunNextFrame);
|
|
221
|
-
return acc;
|
|
222
|
-
}, {});
|
|
223
|
-
const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
|
|
224
|
-
const processBatch = () => {
|
|
225
|
-
const timestamp = performance.now();
|
|
226
|
-
runNextFrame = false;
|
|
227
|
-
state.delta = useDefaultElapsed
|
|
228
|
-
? 1000 / 60
|
|
229
|
-
: Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
|
|
230
|
-
state.timestamp = timestamp;
|
|
231
|
-
state.isProcessing = true;
|
|
232
|
-
// Unrolled render loop for better per-frame performance
|
|
233
|
-
read.process(state);
|
|
234
|
-
resolveKeyframes.process(state);
|
|
235
|
-
update.process(state);
|
|
236
|
-
preRender.process(state);
|
|
237
|
-
render.process(state);
|
|
238
|
-
postRender.process(state);
|
|
239
|
-
state.isProcessing = false;
|
|
240
|
-
if (runNextFrame && allowKeepAlive) {
|
|
241
|
-
useDefaultElapsed = false;
|
|
242
|
-
scheduleNextBatch(processBatch);
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
const wake = () => {
|
|
246
|
-
runNextFrame = true;
|
|
247
|
-
useDefaultElapsed = true;
|
|
248
|
-
if (!state.isProcessing) {
|
|
249
|
-
scheduleNextBatch(processBatch);
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
const schedule = stepsOrder.reduce((acc, key) => {
|
|
253
|
-
const step = steps[key];
|
|
254
|
-
acc[key] = (process, keepAlive = false, immediate = false) => {
|
|
255
|
-
if (!runNextFrame)
|
|
256
|
-
wake();
|
|
257
|
-
return step.schedule(process, keepAlive, immediate);
|
|
258
|
-
};
|
|
259
|
-
return acc;
|
|
260
|
-
}, {});
|
|
261
|
-
const cancel = (process) => {
|
|
262
|
-
for (let i = 0; i < stepsOrder.length; i++) {
|
|
263
|
-
steps[stepsOrder[i]].cancel(process);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
return { schedule, cancel, state, steps };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const { schedule: frame, cancel: cancelFrame} = createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
|
|
270
|
-
|
|
271
|
-
function useAnimationFrame(callback) {
|
|
272
|
-
const initialTimestamp = React.useRef(0);
|
|
273
|
-
const { isStatic } = React.useContext(MotionConfigContext);
|
|
274
|
-
React.useEffect(() => {
|
|
275
|
-
if (isStatic)
|
|
276
|
-
return;
|
|
277
|
-
const provideTimeSinceStart = ({ timestamp, delta }) => {
|
|
278
|
-
if (!initialTimestamp.current)
|
|
279
|
-
initialTimestamp.current = timestamp;
|
|
280
|
-
callback(timestamp - initialTimestamp.current, delta);
|
|
281
|
-
};
|
|
282
|
-
frame.update(provideTimeSinceStart, true);
|
|
283
|
-
return () => cancelFrame(provideTimeSinceStart);
|
|
284
|
-
}, [callback]);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
108
|
function useBouncePatternProgress(enabled = true) {
|
|
288
|
-
const [value, setValue] =
|
|
289
|
-
const [isBouncing, setIsBouncing] =
|
|
290
|
-
const start =
|
|
291
|
-
useAnimationFrame((t) => {
|
|
109
|
+
const [value, setValue] = react.useState(0);
|
|
110
|
+
const [isBouncing, setIsBouncing] = react.useState(false);
|
|
111
|
+
const start = react.useRef(null);
|
|
112
|
+
framerMotion.useAnimationFrame((t) => {
|
|
292
113
|
if (!enabled) {
|
|
293
114
|
// Reset animation when disabled
|
|
294
115
|
if (start.current !== null) {
|
|
@@ -355,6 +176,9 @@ const renderBuildExperimental = async (request, config) => {
|
|
|
355
176
|
const requestWithFormat = {
|
|
356
177
|
...request,
|
|
357
178
|
format: request.format || "video", // Default to video format
|
|
179
|
+
// Include width and height if provided
|
|
180
|
+
...(request.width !== undefined ? { width: request.width } : {}),
|
|
181
|
+
...(request.height !== undefined ? { height: request.height } : {}),
|
|
358
182
|
};
|
|
359
183
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
|
|
360
184
|
method: "POST",
|
|
@@ -379,6 +203,9 @@ const createRenderBuildJob = async (request, config) => {
|
|
|
379
203
|
parts: request.parts,
|
|
380
204
|
// If provided, forward format; default handled server-side but we keep explicit default
|
|
381
205
|
...(request.format ? { format: request.format } : {}),
|
|
206
|
+
// Include width and height if provided
|
|
207
|
+
...(request.width !== undefined ? { width: request.width } : {}),
|
|
208
|
+
...(request.height !== undefined ? { height: request.height } : {}),
|
|
382
209
|
};
|
|
383
210
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
|
|
384
211
|
method: "POST",
|
|
@@ -439,6 +266,9 @@ const renderSpriteExperimental = async (request, config) => {
|
|
|
439
266
|
const requestWithFormat = {
|
|
440
267
|
...request,
|
|
441
268
|
format: "sprite",
|
|
269
|
+
// Include width and height if provided
|
|
270
|
+
...(request.width !== undefined ? { width: request.width } : {}),
|
|
271
|
+
...(request.height !== undefined ? { height: request.height } : {}),
|
|
442
272
|
};
|
|
443
273
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
|
|
444
274
|
method: "POST",
|
|
@@ -549,11 +379,11 @@ const arePartsEqual = (parts1, parts2) => {
|
|
|
549
379
|
return true;
|
|
550
380
|
};
|
|
551
381
|
const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
552
|
-
const [videoSrc, setVideoSrc] =
|
|
553
|
-
const [isRenderingBuild, setIsRenderingBuild] =
|
|
554
|
-
const [renderError, setRenderError] =
|
|
555
|
-
const previousPartsRef =
|
|
556
|
-
const fetchRenderBuild =
|
|
382
|
+
const [videoSrc, setVideoSrc] = react.useState(null);
|
|
383
|
+
const [isRenderingBuild, setIsRenderingBuild] = react.useState(false);
|
|
384
|
+
const [renderError, setRenderError] = react.useState(null);
|
|
385
|
+
const previousPartsRef = react.useRef(null);
|
|
386
|
+
const fetchRenderBuild = react.useCallback(async (currentParts) => {
|
|
557
387
|
try {
|
|
558
388
|
setIsRenderingBuild(true);
|
|
559
389
|
setRenderError(null);
|
|
@@ -588,7 +418,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
588
418
|
}
|
|
589
419
|
}, [apiConfig, onLoadStart, options?.mode]);
|
|
590
420
|
// Effect to call API when parts content changes (using custom equality check)
|
|
591
|
-
|
|
421
|
+
react.useEffect(() => {
|
|
592
422
|
const shouldFetch = previousPartsRef.current === null ||
|
|
593
423
|
!arePartsEqual(previousPartsRef.current, parts);
|
|
594
424
|
if (shouldFetch) {
|
|
@@ -597,7 +427,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
597
427
|
}
|
|
598
428
|
}, [parts, fetchRenderBuild]);
|
|
599
429
|
// Cleanup effect for component unmount
|
|
600
|
-
|
|
430
|
+
react.useEffect(() => {
|
|
601
431
|
return () => {
|
|
602
432
|
if (videoSrc && videoSrc.startsWith("blob:")) {
|
|
603
433
|
URL.revokeObjectURL(videoSrc);
|
|
@@ -612,12 +442,12 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
612
442
|
};
|
|
613
443
|
|
|
614
444
|
const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
615
|
-
const [spriteSrc, setSpriteSrc] =
|
|
616
|
-
const [isRenderingSprite, setIsRenderingSprite] =
|
|
617
|
-
const [renderError, setRenderError] =
|
|
618
|
-
const [spriteMetadata, setSpriteMetadata] =
|
|
619
|
-
const previousPartsRef =
|
|
620
|
-
const fetchRenderSprite =
|
|
445
|
+
const [spriteSrc, setSpriteSrc] = react.useState(null);
|
|
446
|
+
const [isRenderingSprite, setIsRenderingSprite] = react.useState(false);
|
|
447
|
+
const [renderError, setRenderError] = react.useState(null);
|
|
448
|
+
const [spriteMetadata, setSpriteMetadata] = react.useState(null);
|
|
449
|
+
const previousPartsRef = react.useRef(null);
|
|
450
|
+
const fetchRenderSprite = react.useCallback(async (currentParts) => {
|
|
621
451
|
try {
|
|
622
452
|
setIsRenderingSprite(true);
|
|
623
453
|
setRenderError(null);
|
|
@@ -661,7 +491,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
661
491
|
}
|
|
662
492
|
}, [apiConfig, onLoadStart, options?.mode]);
|
|
663
493
|
// Effect to call API when parts content changes (using custom equality check)
|
|
664
|
-
|
|
494
|
+
react.useEffect(() => {
|
|
665
495
|
const shouldFetch = previousPartsRef.current === null ||
|
|
666
496
|
!arePartsEqual(previousPartsRef.current, parts);
|
|
667
497
|
if (shouldFetch) {
|
|
@@ -670,7 +500,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
670
500
|
}
|
|
671
501
|
}, [parts, fetchRenderSprite]);
|
|
672
502
|
// Cleanup effect for component unmount
|
|
673
|
-
|
|
503
|
+
react.useEffect(() => {
|
|
674
504
|
return () => {
|
|
675
505
|
if (spriteSrc && spriteSrc.startsWith("blob:")) {
|
|
676
506
|
URL.revokeObjectURL(spriteSrc);
|
|
@@ -741,10 +571,10 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
|
|
|
741
571
|
};
|
|
742
572
|
|
|
743
573
|
const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
|
|
744
|
-
const canvasRef =
|
|
745
|
-
const [img, setImg] =
|
|
746
|
-
const [isLoading, setIsLoading] =
|
|
747
|
-
const [bouncingAllowed, setBouncingAllowed] =
|
|
574
|
+
const canvasRef = react.useRef(null);
|
|
575
|
+
const [img, setImg] = react.useState(null);
|
|
576
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
577
|
+
const [bouncingAllowed, setBouncingAllowed] = react.useState(false);
|
|
748
578
|
const displayW = width ?? size ?? 300;
|
|
749
579
|
const displayH = height ?? size ?? 300;
|
|
750
580
|
// Use custom hook for sprite rendering
|
|
@@ -753,12 +583,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
753
583
|
const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
|
|
754
584
|
const cols = spriteMetadata ? spriteMetadata.cols : 12;
|
|
755
585
|
const rows = spriteMetadata ? spriteMetadata.rows : 6;
|
|
756
|
-
const frameRef =
|
|
586
|
+
const frameRef = react.useRef(0);
|
|
757
587
|
// Image/frame sizes
|
|
758
588
|
const frameW = img ? img.width / cols : 0;
|
|
759
589
|
const frameH = img ? img.height / rows : 0;
|
|
760
590
|
// ---- Load sprite image ----
|
|
761
|
-
|
|
591
|
+
react.useEffect(() => {
|
|
762
592
|
if (!spriteSrc) {
|
|
763
593
|
setImg(null);
|
|
764
594
|
setIsLoading(true);
|
|
@@ -783,7 +613,7 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
783
613
|
};
|
|
784
614
|
}, [spriteSrc]);
|
|
785
615
|
// ---- Drawing function ----
|
|
786
|
-
const draw =
|
|
616
|
+
const draw = react.useCallback((frameIndex) => {
|
|
787
617
|
const cnv = canvasRef.current;
|
|
788
618
|
if (!cnv || !img || !frameW || !frameH)
|
|
789
619
|
return;
|
|
@@ -822,12 +652,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
822
652
|
draw(newFrame);
|
|
823
653
|
},
|
|
824
654
|
});
|
|
825
|
-
|
|
655
|
+
react.useCallback(() => {
|
|
826
656
|
setIsLoading(true);
|
|
827
657
|
setBouncingAllowed(false);
|
|
828
658
|
}, []);
|
|
829
659
|
// Auto-rotate when bouncing is allowed and not dragged
|
|
830
|
-
|
|
660
|
+
react.useEffect(() => {
|
|
831
661
|
if (hasDragged.current || !img)
|
|
832
662
|
return;
|
|
833
663
|
// Calculate frame based on progress value (similar to video time calculation)
|
|
@@ -836,7 +666,7 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
836
666
|
draw(frame);
|
|
837
667
|
}, [progressValue, hasDragged, img, total, draw]);
|
|
838
668
|
// Initial draw once image is ready
|
|
839
|
-
|
|
669
|
+
react.useEffect(() => {
|
|
840
670
|
if (img && !isLoading) {
|
|
841
671
|
draw(frameRef.current);
|
|
842
672
|
}
|
|
@@ -879,12 +709,12 @@ const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
|
|
|
879
709
|
};
|
|
880
710
|
const useVideoScrubbing = (videoRef, options = {}) => {
|
|
881
711
|
const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
|
|
882
|
-
const [isDragging, setIsDragging] =
|
|
883
|
-
const [dragStartX, setDragStartX] =
|
|
884
|
-
const [dragStartTime, setDragStartTime] =
|
|
885
|
-
const hasDragged =
|
|
712
|
+
const [isDragging, setIsDragging] = react.useState(false);
|
|
713
|
+
const [dragStartX, setDragStartX] = react.useState(0);
|
|
714
|
+
const [dragStartTime, setDragStartTime] = react.useState(0);
|
|
715
|
+
const hasDragged = react.useRef(false);
|
|
886
716
|
// Helper to start dragging (common logic for mouse and touch)
|
|
887
|
-
const startDrag =
|
|
717
|
+
const startDrag = react.useCallback((clientX, event) => {
|
|
888
718
|
if (!videoRef.current)
|
|
889
719
|
return;
|
|
890
720
|
setIsDragging(true);
|
|
@@ -894,7 +724,7 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
894
724
|
event.preventDefault();
|
|
895
725
|
}, [videoRef]);
|
|
896
726
|
// Helper to handle drag movement (common logic for mouse and touch)
|
|
897
|
-
const handleDragMove =
|
|
727
|
+
const handleDragMove = react.useCallback((clientX, sensitivity) => {
|
|
898
728
|
if (!isDragging || !videoRef.current)
|
|
899
729
|
return;
|
|
900
730
|
const deltaX = clientX - dragStartX;
|
|
@@ -905,29 +735,29 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
905
735
|
}
|
|
906
736
|
}, [isDragging, dragStartX, dragStartTime, videoRef]);
|
|
907
737
|
// Helper to end dragging (common logic for mouse and touch)
|
|
908
|
-
const endDrag =
|
|
738
|
+
const endDrag = react.useCallback(() => {
|
|
909
739
|
setIsDragging(false);
|
|
910
740
|
}, []);
|
|
911
|
-
const handleMouseDown =
|
|
741
|
+
const handleMouseDown = react.useCallback((e) => {
|
|
912
742
|
startDrag(e.clientX, e.nativeEvent);
|
|
913
743
|
}, [startDrag]);
|
|
914
|
-
const handleTouchStart =
|
|
744
|
+
const handleTouchStart = react.useCallback((e) => {
|
|
915
745
|
startDrag(e.touches[0].clientX, e.nativeEvent);
|
|
916
746
|
}, [startDrag]);
|
|
917
|
-
const handleDocumentMouseMove =
|
|
747
|
+
const handleDocumentMouseMove = react.useCallback((e) => {
|
|
918
748
|
handleDragMove(getClientX(e), mouseSensitivity);
|
|
919
749
|
}, [handleDragMove, mouseSensitivity]);
|
|
920
|
-
const handleDocumentTouchMove =
|
|
750
|
+
const handleDocumentTouchMove = react.useCallback((e) => {
|
|
921
751
|
handleDragMove(getClientX(e), touchSensitivity);
|
|
922
752
|
}, [handleDragMove, touchSensitivity]);
|
|
923
|
-
const handleDocumentMouseUp =
|
|
753
|
+
const handleDocumentMouseUp = react.useCallback(() => {
|
|
924
754
|
endDrag();
|
|
925
755
|
}, [endDrag]);
|
|
926
|
-
const handleDocumentTouchEnd =
|
|
756
|
+
const handleDocumentTouchEnd = react.useCallback(() => {
|
|
927
757
|
endDrag();
|
|
928
758
|
}, [endDrag]);
|
|
929
759
|
// Add document-level event listeners when dragging starts
|
|
930
|
-
|
|
760
|
+
react.useEffect(() => {
|
|
931
761
|
if (isDragging) {
|
|
932
762
|
document.addEventListener("mousemove", handleDocumentMouseMove);
|
|
933
763
|
document.addEventListener("mouseup", handleDocumentMouseUp);
|
|
@@ -956,9 +786,9 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
956
786
|
};
|
|
957
787
|
|
|
958
788
|
const BuildRenderVideo = ({ parts, width, height, size, apiConfig, useBuildRenderOptions, mouseSensitivity = 0.01, touchSensitivity = 0.01, }) => {
|
|
959
|
-
const videoRef =
|
|
960
|
-
const [isLoading, setIsLoading] =
|
|
961
|
-
const [bouncingAllowed, setBouncingAllowed] =
|
|
789
|
+
const videoRef = react.useRef(null);
|
|
790
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
791
|
+
const [bouncingAllowed, setBouncingAllowed] = react.useState(false);
|
|
962
792
|
const displayW = width ?? size ?? 300;
|
|
963
793
|
const displayH = height ?? size ?? 300;
|
|
964
794
|
// Use custom hook for build rendering
|
|
@@ -968,18 +798,18 @@ const BuildRenderVideo = ({ parts, width, height, size, apiConfig, useBuildRende
|
|
|
968
798
|
mouseSensitivity,
|
|
969
799
|
touchSensitivity,
|
|
970
800
|
});
|
|
971
|
-
const handleLoadStartInternal =
|
|
801
|
+
const handleLoadStartInternal = react.useCallback(() => {
|
|
972
802
|
setIsLoading(true);
|
|
973
803
|
setBouncingAllowed(false);
|
|
974
804
|
}, []);
|
|
975
|
-
const handleCanPlayInternal =
|
|
805
|
+
const handleCanPlayInternal = react.useCallback(() => {
|
|
976
806
|
setIsLoading(false);
|
|
977
807
|
// Start bouncing animation after delay
|
|
978
808
|
setTimeout(() => {
|
|
979
809
|
setBouncingAllowed(true);
|
|
980
810
|
}, 2000);
|
|
981
811
|
}, []);
|
|
982
|
-
|
|
812
|
+
react.useEffect(() => {
|
|
983
813
|
if (hasDragged.current || !videoRef.current)
|
|
984
814
|
return;
|
|
985
815
|
const duration = videoRef.current.duration;
|