@buildcores/render-client 1.0.12 → 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/api.d.ts +3 -3
- package/dist/index.d.ts +70 -6
- package/dist/index.esm.js +23 -184
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +87 -248
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +67 -3
- 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",
|
|
@@ -460,15 +290,24 @@ const renderSpriteExperimental = async (request, config) => {
|
|
|
460
290
|
},
|
|
461
291
|
};
|
|
462
292
|
};
|
|
463
|
-
const getAvailableParts = async (config) => {
|
|
464
|
-
const
|
|
293
|
+
const getAvailableParts = async (category, config, options) => {
|
|
294
|
+
const base = buildApiUrl(API_ENDPOINTS.AVAILABLE_PARTS, config);
|
|
295
|
+
const params = new URLSearchParams();
|
|
296
|
+
params.set("category", category);
|
|
297
|
+
if (typeof options?.limit === "number")
|
|
298
|
+
params.set("limit", String(options.limit));
|
|
299
|
+
if (typeof options?.skip === "number")
|
|
300
|
+
params.set("skip", String(options.skip));
|
|
301
|
+
const separator = base.includes("?") ? "&" : "?";
|
|
302
|
+
const url = `${base}${separator}${params.toString()}`;
|
|
303
|
+
const response = await fetch(url, {
|
|
465
304
|
method: "GET",
|
|
466
305
|
headers: buildHeaders(config),
|
|
467
306
|
});
|
|
468
307
|
if (!response.ok) {
|
|
469
308
|
throw new Error(`Get available parts failed: ${response.status} ${response.statusText}`);
|
|
470
309
|
}
|
|
471
|
-
return response.json();
|
|
310
|
+
return (await response.json());
|
|
472
311
|
};
|
|
473
312
|
|
|
474
313
|
/**
|
|
@@ -540,11 +379,11 @@ const arePartsEqual = (parts1, parts2) => {
|
|
|
540
379
|
return true;
|
|
541
380
|
};
|
|
542
381
|
const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
543
|
-
const [videoSrc, setVideoSrc] =
|
|
544
|
-
const [isRenderingBuild, setIsRenderingBuild] =
|
|
545
|
-
const [renderError, setRenderError] =
|
|
546
|
-
const previousPartsRef =
|
|
547
|
-
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) => {
|
|
548
387
|
try {
|
|
549
388
|
setIsRenderingBuild(true);
|
|
550
389
|
setRenderError(null);
|
|
@@ -579,7 +418,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
579
418
|
}
|
|
580
419
|
}, [apiConfig, onLoadStart, options?.mode]);
|
|
581
420
|
// Effect to call API when parts content changes (using custom equality check)
|
|
582
|
-
|
|
421
|
+
react.useEffect(() => {
|
|
583
422
|
const shouldFetch = previousPartsRef.current === null ||
|
|
584
423
|
!arePartsEqual(previousPartsRef.current, parts);
|
|
585
424
|
if (shouldFetch) {
|
|
@@ -588,7 +427,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
588
427
|
}
|
|
589
428
|
}, [parts, fetchRenderBuild]);
|
|
590
429
|
// Cleanup effect for component unmount
|
|
591
|
-
|
|
430
|
+
react.useEffect(() => {
|
|
592
431
|
return () => {
|
|
593
432
|
if (videoSrc && videoSrc.startsWith("blob:")) {
|
|
594
433
|
URL.revokeObjectURL(videoSrc);
|
|
@@ -603,12 +442,12 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
603
442
|
};
|
|
604
443
|
|
|
605
444
|
const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
606
|
-
const [spriteSrc, setSpriteSrc] =
|
|
607
|
-
const [isRenderingSprite, setIsRenderingSprite] =
|
|
608
|
-
const [renderError, setRenderError] =
|
|
609
|
-
const [spriteMetadata, setSpriteMetadata] =
|
|
610
|
-
const previousPartsRef =
|
|
611
|
-
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) => {
|
|
612
451
|
try {
|
|
613
452
|
setIsRenderingSprite(true);
|
|
614
453
|
setRenderError(null);
|
|
@@ -652,7 +491,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
652
491
|
}
|
|
653
492
|
}, [apiConfig, onLoadStart, options?.mode]);
|
|
654
493
|
// Effect to call API when parts content changes (using custom equality check)
|
|
655
|
-
|
|
494
|
+
react.useEffect(() => {
|
|
656
495
|
const shouldFetch = previousPartsRef.current === null ||
|
|
657
496
|
!arePartsEqual(previousPartsRef.current, parts);
|
|
658
497
|
if (shouldFetch) {
|
|
@@ -661,7 +500,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
661
500
|
}
|
|
662
501
|
}, [parts, fetchRenderSprite]);
|
|
663
502
|
// Cleanup effect for component unmount
|
|
664
|
-
|
|
503
|
+
react.useEffect(() => {
|
|
665
504
|
return () => {
|
|
666
505
|
if (spriteSrc && spriteSrc.startsWith("blob:")) {
|
|
667
506
|
URL.revokeObjectURL(spriteSrc);
|
|
@@ -732,10 +571,10 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
|
|
|
732
571
|
};
|
|
733
572
|
|
|
734
573
|
const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
|
|
735
|
-
const canvasRef =
|
|
736
|
-
const [img, setImg] =
|
|
737
|
-
const [isLoading, setIsLoading] =
|
|
738
|
-
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);
|
|
739
578
|
const displayW = width ?? size ?? 300;
|
|
740
579
|
const displayH = height ?? size ?? 300;
|
|
741
580
|
// Use custom hook for sprite rendering
|
|
@@ -744,12 +583,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
744
583
|
const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
|
|
745
584
|
const cols = spriteMetadata ? spriteMetadata.cols : 12;
|
|
746
585
|
const rows = spriteMetadata ? spriteMetadata.rows : 6;
|
|
747
|
-
const frameRef =
|
|
586
|
+
const frameRef = react.useRef(0);
|
|
748
587
|
// Image/frame sizes
|
|
749
588
|
const frameW = img ? img.width / cols : 0;
|
|
750
589
|
const frameH = img ? img.height / rows : 0;
|
|
751
590
|
// ---- Load sprite image ----
|
|
752
|
-
|
|
591
|
+
react.useEffect(() => {
|
|
753
592
|
if (!spriteSrc) {
|
|
754
593
|
setImg(null);
|
|
755
594
|
setIsLoading(true);
|
|
@@ -774,7 +613,7 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
774
613
|
};
|
|
775
614
|
}, [spriteSrc]);
|
|
776
615
|
// ---- Drawing function ----
|
|
777
|
-
const draw =
|
|
616
|
+
const draw = react.useCallback((frameIndex) => {
|
|
778
617
|
const cnv = canvasRef.current;
|
|
779
618
|
if (!cnv || !img || !frameW || !frameH)
|
|
780
619
|
return;
|
|
@@ -813,12 +652,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
813
652
|
draw(newFrame);
|
|
814
653
|
},
|
|
815
654
|
});
|
|
816
|
-
|
|
655
|
+
react.useCallback(() => {
|
|
817
656
|
setIsLoading(true);
|
|
818
657
|
setBouncingAllowed(false);
|
|
819
658
|
}, []);
|
|
820
659
|
// Auto-rotate when bouncing is allowed and not dragged
|
|
821
|
-
|
|
660
|
+
react.useEffect(() => {
|
|
822
661
|
if (hasDragged.current || !img)
|
|
823
662
|
return;
|
|
824
663
|
// Calculate frame based on progress value (similar to video time calculation)
|
|
@@ -827,7 +666,7 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
827
666
|
draw(frame);
|
|
828
667
|
}, [progressValue, hasDragged, img, total, draw]);
|
|
829
668
|
// Initial draw once image is ready
|
|
830
|
-
|
|
669
|
+
react.useEffect(() => {
|
|
831
670
|
if (img && !isLoading) {
|
|
832
671
|
draw(frameRef.current);
|
|
833
672
|
}
|
|
@@ -870,12 +709,12 @@ const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
|
|
|
870
709
|
};
|
|
871
710
|
const useVideoScrubbing = (videoRef, options = {}) => {
|
|
872
711
|
const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
|
|
873
|
-
const [isDragging, setIsDragging] =
|
|
874
|
-
const [dragStartX, setDragStartX] =
|
|
875
|
-
const [dragStartTime, setDragStartTime] =
|
|
876
|
-
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);
|
|
877
716
|
// Helper to start dragging (common logic for mouse and touch)
|
|
878
|
-
const startDrag =
|
|
717
|
+
const startDrag = react.useCallback((clientX, event) => {
|
|
879
718
|
if (!videoRef.current)
|
|
880
719
|
return;
|
|
881
720
|
setIsDragging(true);
|
|
@@ -885,7 +724,7 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
885
724
|
event.preventDefault();
|
|
886
725
|
}, [videoRef]);
|
|
887
726
|
// Helper to handle drag movement (common logic for mouse and touch)
|
|
888
|
-
const handleDragMove =
|
|
727
|
+
const handleDragMove = react.useCallback((clientX, sensitivity) => {
|
|
889
728
|
if (!isDragging || !videoRef.current)
|
|
890
729
|
return;
|
|
891
730
|
const deltaX = clientX - dragStartX;
|
|
@@ -896,29 +735,29 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
896
735
|
}
|
|
897
736
|
}, [isDragging, dragStartX, dragStartTime, videoRef]);
|
|
898
737
|
// Helper to end dragging (common logic for mouse and touch)
|
|
899
|
-
const endDrag =
|
|
738
|
+
const endDrag = react.useCallback(() => {
|
|
900
739
|
setIsDragging(false);
|
|
901
740
|
}, []);
|
|
902
|
-
const handleMouseDown =
|
|
741
|
+
const handleMouseDown = react.useCallback((e) => {
|
|
903
742
|
startDrag(e.clientX, e.nativeEvent);
|
|
904
743
|
}, [startDrag]);
|
|
905
|
-
const handleTouchStart =
|
|
744
|
+
const handleTouchStart = react.useCallback((e) => {
|
|
906
745
|
startDrag(e.touches[0].clientX, e.nativeEvent);
|
|
907
746
|
}, [startDrag]);
|
|
908
|
-
const handleDocumentMouseMove =
|
|
747
|
+
const handleDocumentMouseMove = react.useCallback((e) => {
|
|
909
748
|
handleDragMove(getClientX(e), mouseSensitivity);
|
|
910
749
|
}, [handleDragMove, mouseSensitivity]);
|
|
911
|
-
const handleDocumentTouchMove =
|
|
750
|
+
const handleDocumentTouchMove = react.useCallback((e) => {
|
|
912
751
|
handleDragMove(getClientX(e), touchSensitivity);
|
|
913
752
|
}, [handleDragMove, touchSensitivity]);
|
|
914
|
-
const handleDocumentMouseUp =
|
|
753
|
+
const handleDocumentMouseUp = react.useCallback(() => {
|
|
915
754
|
endDrag();
|
|
916
755
|
}, [endDrag]);
|
|
917
|
-
const handleDocumentTouchEnd =
|
|
756
|
+
const handleDocumentTouchEnd = react.useCallback(() => {
|
|
918
757
|
endDrag();
|
|
919
758
|
}, [endDrag]);
|
|
920
759
|
// Add document-level event listeners when dragging starts
|
|
921
|
-
|
|
760
|
+
react.useEffect(() => {
|
|
922
761
|
if (isDragging) {
|
|
923
762
|
document.addEventListener("mousemove", handleDocumentMouseMove);
|
|
924
763
|
document.addEventListener("mouseup", handleDocumentMouseUp);
|
|
@@ -947,9 +786,9 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
947
786
|
};
|
|
948
787
|
|
|
949
788
|
const BuildRenderVideo = ({ parts, width, height, size, apiConfig, useBuildRenderOptions, mouseSensitivity = 0.01, touchSensitivity = 0.01, }) => {
|
|
950
|
-
const videoRef =
|
|
951
|
-
const [isLoading, setIsLoading] =
|
|
952
|
-
const [bouncingAllowed, setBouncingAllowed] =
|
|
789
|
+
const videoRef = react.useRef(null);
|
|
790
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
791
|
+
const [bouncingAllowed, setBouncingAllowed] = react.useState(false);
|
|
953
792
|
const displayW = width ?? size ?? 300;
|
|
954
793
|
const displayH = height ?? size ?? 300;
|
|
955
794
|
// Use custom hook for build rendering
|
|
@@ -959,18 +798,18 @@ const BuildRenderVideo = ({ parts, width, height, size, apiConfig, useBuildRende
|
|
|
959
798
|
mouseSensitivity,
|
|
960
799
|
touchSensitivity,
|
|
961
800
|
});
|
|
962
|
-
const handleLoadStartInternal =
|
|
801
|
+
const handleLoadStartInternal = react.useCallback(() => {
|
|
963
802
|
setIsLoading(true);
|
|
964
803
|
setBouncingAllowed(false);
|
|
965
804
|
}, []);
|
|
966
|
-
const handleCanPlayInternal =
|
|
805
|
+
const handleCanPlayInternal = react.useCallback(() => {
|
|
967
806
|
setIsLoading(false);
|
|
968
807
|
// Start bouncing animation after delay
|
|
969
808
|
setTimeout(() => {
|
|
970
809
|
setBouncingAllowed(true);
|
|
971
810
|
}, 2000);
|
|
972
811
|
}, []);
|
|
973
|
-
|
|
812
|
+
react.useEffect(() => {
|
|
974
813
|
if (hasDragged.current || !videoRef.current)
|
|
975
814
|
return;
|
|
976
815
|
const duration = videoRef.current.duration;
|