@buildcores/render-client 1.1.0 → 1.3.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/hooks/useZoomPan.d.ts +16 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.esm.js +171 -186
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +235 -250
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +52 -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,11 @@ 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 } : {}),
|
|
182
|
+
// Include profile if provided
|
|
183
|
+
...(request.profile ? { profile: request.profile } : {}),
|
|
358
184
|
};
|
|
359
185
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
|
|
360
186
|
method: "POST",
|
|
@@ -379,6 +205,11 @@ const createRenderBuildJob = async (request, config) => {
|
|
|
379
205
|
parts: request.parts,
|
|
380
206
|
// If provided, forward format; default handled server-side but we keep explicit default
|
|
381
207
|
...(request.format ? { format: request.format } : {}),
|
|
208
|
+
// Include width and height if provided
|
|
209
|
+
...(request.width !== undefined ? { width: request.width } : {}),
|
|
210
|
+
...(request.height !== undefined ? { height: request.height } : {}),
|
|
211
|
+
// Include profile if provided
|
|
212
|
+
...(request.profile ? { profile: request.profile } : {}),
|
|
382
213
|
};
|
|
383
214
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
|
|
384
215
|
method: "POST",
|
|
@@ -439,6 +270,11 @@ const renderSpriteExperimental = async (request, config) => {
|
|
|
439
270
|
const requestWithFormat = {
|
|
440
271
|
...request,
|
|
441
272
|
format: "sprite",
|
|
273
|
+
// Include width and height if provided
|
|
274
|
+
...(request.width !== undefined ? { width: request.width } : {}),
|
|
275
|
+
...(request.height !== undefined ? { height: request.height } : {}),
|
|
276
|
+
// Include profile if provided
|
|
277
|
+
...(request.profile ? { profile: request.profile } : {}),
|
|
442
278
|
};
|
|
443
279
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
|
|
444
280
|
method: "POST",
|
|
@@ -549,11 +385,11 @@ const arePartsEqual = (parts1, parts2) => {
|
|
|
549
385
|
return true;
|
|
550
386
|
};
|
|
551
387
|
const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
552
|
-
const [videoSrc, setVideoSrc] =
|
|
553
|
-
const [isRenderingBuild, setIsRenderingBuild] =
|
|
554
|
-
const [renderError, setRenderError] =
|
|
555
|
-
const previousPartsRef =
|
|
556
|
-
const fetchRenderBuild =
|
|
388
|
+
const [videoSrc, setVideoSrc] = react.useState(null);
|
|
389
|
+
const [isRenderingBuild, setIsRenderingBuild] = react.useState(false);
|
|
390
|
+
const [renderError, setRenderError] = react.useState(null);
|
|
391
|
+
const previousPartsRef = react.useRef(null);
|
|
392
|
+
const fetchRenderBuild = react.useCallback(async (currentParts) => {
|
|
557
393
|
try {
|
|
558
394
|
setIsRenderingBuild(true);
|
|
559
395
|
setRenderError(null);
|
|
@@ -588,7 +424,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
588
424
|
}
|
|
589
425
|
}, [apiConfig, onLoadStart, options?.mode]);
|
|
590
426
|
// Effect to call API when parts content changes (using custom equality check)
|
|
591
|
-
|
|
427
|
+
react.useEffect(() => {
|
|
592
428
|
const shouldFetch = previousPartsRef.current === null ||
|
|
593
429
|
!arePartsEqual(previousPartsRef.current, parts);
|
|
594
430
|
if (shouldFetch) {
|
|
@@ -597,7 +433,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
597
433
|
}
|
|
598
434
|
}, [parts, fetchRenderBuild]);
|
|
599
435
|
// Cleanup effect for component unmount
|
|
600
|
-
|
|
436
|
+
react.useEffect(() => {
|
|
601
437
|
return () => {
|
|
602
438
|
if (videoSrc && videoSrc.startsWith("blob:")) {
|
|
603
439
|
URL.revokeObjectURL(videoSrc);
|
|
@@ -612,12 +448,12 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
612
448
|
};
|
|
613
449
|
|
|
614
450
|
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 =
|
|
451
|
+
const [spriteSrc, setSpriteSrc] = react.useState(null);
|
|
452
|
+
const [isRenderingSprite, setIsRenderingSprite] = react.useState(false);
|
|
453
|
+
const [renderError, setRenderError] = react.useState(null);
|
|
454
|
+
const [spriteMetadata, setSpriteMetadata] = react.useState(null);
|
|
455
|
+
const previousPartsRef = react.useRef(null);
|
|
456
|
+
const fetchRenderSprite = react.useCallback(async (currentParts) => {
|
|
621
457
|
try {
|
|
622
458
|
setIsRenderingSprite(true);
|
|
623
459
|
setRenderError(null);
|
|
@@ -661,7 +497,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
661
497
|
}
|
|
662
498
|
}, [apiConfig, onLoadStart, options?.mode]);
|
|
663
499
|
// Effect to call API when parts content changes (using custom equality check)
|
|
664
|
-
|
|
500
|
+
react.useEffect(() => {
|
|
665
501
|
const shouldFetch = previousPartsRef.current === null ||
|
|
666
502
|
!arePartsEqual(previousPartsRef.current, parts);
|
|
667
503
|
if (shouldFetch) {
|
|
@@ -670,7 +506,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
|
670
506
|
}
|
|
671
507
|
}, [parts, fetchRenderSprite]);
|
|
672
508
|
// Cleanup effect for component unmount
|
|
673
|
-
|
|
509
|
+
react.useEffect(() => {
|
|
674
510
|
return () => {
|
|
675
511
|
if (spriteSrc && spriteSrc.startsWith("blob:")) {
|
|
676
512
|
URL.revokeObjectURL(spriteSrc);
|
|
@@ -740,11 +576,104 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
|
|
|
740
576
|
} })) }));
|
|
741
577
|
};
|
|
742
578
|
|
|
579
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
580
|
+
const getTouchDistance = (touches) => {
|
|
581
|
+
const first = touches[0];
|
|
582
|
+
const second = touches[1];
|
|
583
|
+
if (!first || !second)
|
|
584
|
+
return 0;
|
|
585
|
+
return Math.hypot(second.clientX - first.clientX, second.clientY - first.clientY);
|
|
586
|
+
};
|
|
587
|
+
const useZoomPan = ({ displayWidth, displayHeight, minScale = 1, maxScale = 4, } = {}) => {
|
|
588
|
+
const [scale, setScale] = react.useState(1);
|
|
589
|
+
const [isPinching, setIsPinching] = react.useState(false);
|
|
590
|
+
const scaleRef = react.useRef(1);
|
|
591
|
+
const pinchDataRef = react.useRef({
|
|
592
|
+
initialDistance: 0,
|
|
593
|
+
initialScale: 1,
|
|
594
|
+
});
|
|
595
|
+
const setScaleSafe = react.useCallback((next) => {
|
|
596
|
+
const clamped = clamp(next, minScale, maxScale);
|
|
597
|
+
if (clamped === scaleRef.current)
|
|
598
|
+
return;
|
|
599
|
+
scaleRef.current = clamped;
|
|
600
|
+
setScale(clamped);
|
|
601
|
+
}, [minScale, maxScale]);
|
|
602
|
+
const handleWheel = react.useCallback((event) => {
|
|
603
|
+
event.preventDefault();
|
|
604
|
+
event.stopPropagation();
|
|
605
|
+
const deltaY = event.deltaMode === 1
|
|
606
|
+
? event.deltaY * 16
|
|
607
|
+
: event.deltaMode === 2
|
|
608
|
+
? event.deltaY * (displayHeight ?? 300)
|
|
609
|
+
: event.deltaY;
|
|
610
|
+
const zoomFactor = Math.exp(-deltaY * 0.0015);
|
|
611
|
+
const nextScale = scaleRef.current * zoomFactor;
|
|
612
|
+
setScaleSafe(nextScale);
|
|
613
|
+
}, [setScaleSafe, displayHeight]);
|
|
614
|
+
const handleTouchStart = react.useCallback((event) => {
|
|
615
|
+
if (event.touches.length < 2) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
const distance = getTouchDistance(event.touches);
|
|
619
|
+
if (!distance) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
pinchDataRef.current = {
|
|
623
|
+
initialDistance: distance,
|
|
624
|
+
initialScale: scaleRef.current,
|
|
625
|
+
};
|
|
626
|
+
setIsPinching(true);
|
|
627
|
+
event.preventDefault();
|
|
628
|
+
return true;
|
|
629
|
+
}, []);
|
|
630
|
+
react.useEffect(() => {
|
|
631
|
+
if (!isPinching)
|
|
632
|
+
return;
|
|
633
|
+
const handleMove = (event) => {
|
|
634
|
+
if (event.touches.length < 2)
|
|
635
|
+
return;
|
|
636
|
+
const distance = getTouchDistance(event.touches);
|
|
637
|
+
if (!distance || pinchDataRef.current.initialDistance === 0)
|
|
638
|
+
return;
|
|
639
|
+
const scaleFactor = distance / pinchDataRef.current.initialDistance;
|
|
640
|
+
const nextScale = pinchDataRef.current.initialScale * scaleFactor;
|
|
641
|
+
setScaleSafe(nextScale);
|
|
642
|
+
event.preventDefault();
|
|
643
|
+
};
|
|
644
|
+
const handleEnd = (event) => {
|
|
645
|
+
if (event.touches.length < 2) {
|
|
646
|
+
setIsPinching(false);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
window.addEventListener("touchmove", handleMove, { passive: false });
|
|
650
|
+
window.addEventListener("touchend", handleEnd);
|
|
651
|
+
window.addEventListener("touchcancel", handleEnd);
|
|
652
|
+
return () => {
|
|
653
|
+
window.removeEventListener("touchmove", handleMove);
|
|
654
|
+
window.removeEventListener("touchend", handleEnd);
|
|
655
|
+
window.removeEventListener("touchcancel", handleEnd);
|
|
656
|
+
};
|
|
657
|
+
}, [isPinching, setScaleSafe]);
|
|
658
|
+
const reset = react.useCallback(() => {
|
|
659
|
+
scaleRef.current = 1;
|
|
660
|
+
setScale(1);
|
|
661
|
+
}, []);
|
|
662
|
+
return {
|
|
663
|
+
scale,
|
|
664
|
+
isPinching,
|
|
665
|
+
handleWheel,
|
|
666
|
+
handleTouchStart,
|
|
667
|
+
reset,
|
|
668
|
+
};
|
|
669
|
+
};
|
|
670
|
+
|
|
743
671
|
const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
|
|
744
|
-
const canvasRef =
|
|
745
|
-
const
|
|
746
|
-
const [
|
|
747
|
-
const [
|
|
672
|
+
const canvasRef = react.useRef(null);
|
|
673
|
+
const containerRef = react.useRef(null);
|
|
674
|
+
const [img, setImg] = react.useState(null);
|
|
675
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
676
|
+
const [bouncingAllowed, setBouncingAllowed] = react.useState(false);
|
|
748
677
|
const displayW = width ?? size ?? 300;
|
|
749
678
|
const displayH = height ?? size ?? 300;
|
|
750
679
|
// Use custom hook for sprite rendering
|
|
@@ -753,12 +682,16 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
753
682
|
const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
|
|
754
683
|
const cols = spriteMetadata ? spriteMetadata.cols : 12;
|
|
755
684
|
const rows = spriteMetadata ? spriteMetadata.rows : 6;
|
|
756
|
-
const frameRef =
|
|
685
|
+
const frameRef = react.useRef(0);
|
|
686
|
+
const { scale, handleWheel: handleZoomWheel, handleTouchStart: handleZoomTouchStart, reset: resetZoom, } = useZoomPan({
|
|
687
|
+
displayWidth: displayW,
|
|
688
|
+
displayHeight: displayH,
|
|
689
|
+
});
|
|
757
690
|
// Image/frame sizes
|
|
758
691
|
const frameW = img ? img.width / cols : 0;
|
|
759
692
|
const frameH = img ? img.height / rows : 0;
|
|
760
693
|
// ---- Load sprite image ----
|
|
761
|
-
|
|
694
|
+
react.useEffect(() => {
|
|
762
695
|
if (!spriteSrc) {
|
|
763
696
|
setImg(null);
|
|
764
697
|
setIsLoading(true);
|
|
@@ -783,7 +716,7 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
783
716
|
};
|
|
784
717
|
}, [spriteSrc]);
|
|
785
718
|
// ---- Drawing function ----
|
|
786
|
-
const draw =
|
|
719
|
+
const draw = react.useCallback((frameIndex) => {
|
|
787
720
|
const cnv = canvasRef.current;
|
|
788
721
|
if (!cnv || !img || !frameW || !frameH)
|
|
789
722
|
return;
|
|
@@ -812,8 +745,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
812
745
|
ctx.clearRect(0, 0, targetW, targetH);
|
|
813
746
|
ctx.imageSmoothingEnabled = true;
|
|
814
747
|
ctx.imageSmoothingQuality = "high";
|
|
815
|
-
|
|
816
|
-
|
|
748
|
+
const scaledW = targetW * scale;
|
|
749
|
+
const scaledH = targetH * scale;
|
|
750
|
+
const offsetX = -((scaledW - targetW) / 2);
|
|
751
|
+
const offsetY = -((scaledH - targetH) / 2);
|
|
752
|
+
ctx.drawImage(img, sx, sy, sw, sh, offsetX, offsetY, scaledW, scaledH);
|
|
753
|
+
}, [img, frameW, frameH, displayW, displayH, cols, total, scale]);
|
|
817
754
|
const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useSpriteScrubbing(canvasRef, total, {
|
|
818
755
|
mouseSensitivity,
|
|
819
756
|
touchSensitivity,
|
|
@@ -822,12 +759,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
822
759
|
draw(newFrame);
|
|
823
760
|
},
|
|
824
761
|
});
|
|
825
|
-
|
|
762
|
+
react.useCallback(() => {
|
|
826
763
|
setIsLoading(true);
|
|
827
764
|
setBouncingAllowed(false);
|
|
828
765
|
}, []);
|
|
829
766
|
// Auto-rotate when bouncing is allowed and not dragged
|
|
830
|
-
|
|
767
|
+
react.useEffect(() => {
|
|
831
768
|
if (hasDragged.current || !img)
|
|
832
769
|
return;
|
|
833
770
|
// Calculate frame based on progress value (similar to video time calculation)
|
|
@@ -835,18 +772,66 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
|
|
|
835
772
|
frameRef.current = frame;
|
|
836
773
|
draw(frame);
|
|
837
774
|
}, [progressValue, hasDragged, img, total, draw]);
|
|
838
|
-
//
|
|
839
|
-
|
|
775
|
+
// Reset zoom when sprite changes or container size updates
|
|
776
|
+
react.useEffect(() => {
|
|
777
|
+
resetZoom();
|
|
778
|
+
}, [spriteSrc, displayW, displayH, resetZoom]);
|
|
779
|
+
// Add native wheel event listener to prevent scrolling AND handle zoom
|
|
780
|
+
react.useEffect(() => {
|
|
781
|
+
const container = containerRef.current;
|
|
782
|
+
if (!container)
|
|
783
|
+
return;
|
|
784
|
+
const handleNativeWheel = (event) => {
|
|
785
|
+
event.preventDefault();
|
|
786
|
+
event.stopPropagation();
|
|
787
|
+
// Manually trigger zoom since we're preventing the React event
|
|
788
|
+
event.deltaMode === 1
|
|
789
|
+
? event.deltaY * 16
|
|
790
|
+
: event.deltaMode === 2
|
|
791
|
+
? event.deltaY * (displayH ?? 300)
|
|
792
|
+
: event.deltaY;
|
|
793
|
+
// We need to call the zoom handler directly
|
|
794
|
+
// Create a synthetic React event-like object
|
|
795
|
+
const syntheticEvent = {
|
|
796
|
+
preventDefault: () => { },
|
|
797
|
+
stopPropagation: () => { },
|
|
798
|
+
deltaY: event.deltaY,
|
|
799
|
+
deltaMode: event.deltaMode,
|
|
800
|
+
currentTarget: container,
|
|
801
|
+
};
|
|
802
|
+
handleZoomWheel(syntheticEvent);
|
|
803
|
+
hasDragged.current = true;
|
|
804
|
+
};
|
|
805
|
+
// Add listener to container to catch all wheel events
|
|
806
|
+
container.addEventListener('wheel', handleNativeWheel, { passive: false });
|
|
807
|
+
return () => {
|
|
808
|
+
container.removeEventListener('wheel', handleNativeWheel);
|
|
809
|
+
};
|
|
810
|
+
}, [handleZoomWheel, scale, displayH]);
|
|
811
|
+
// Initial draw once image is ready or zoom changes
|
|
812
|
+
react.useEffect(() => {
|
|
840
813
|
if (img && !isLoading) {
|
|
841
814
|
draw(frameRef.current);
|
|
842
815
|
}
|
|
843
816
|
}, [img, isLoading, draw]);
|
|
844
|
-
|
|
817
|
+
const handleCanvasTouchStart = react.useCallback((event) => {
|
|
818
|
+
if (handleZoomTouchStart(event)) {
|
|
819
|
+
hasDragged.current = true;
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
handleTouchStart(event);
|
|
823
|
+
}, [handleZoomTouchStart, handleTouchStart, hasDragged]);
|
|
824
|
+
react.useCallback((event) => {
|
|
825
|
+
hasDragged.current = true;
|
|
826
|
+
handleZoomWheel(event);
|
|
827
|
+
}, [handleZoomWheel, hasDragged]);
|
|
828
|
+
return (jsxRuntime.jsxs("div", { ref: containerRef, style: {
|
|
845
829
|
position: "relative",
|
|
846
830
|
width: displayW,
|
|
847
831
|
height: displayH,
|
|
848
832
|
backgroundColor: "black",
|
|
849
|
-
|
|
833
|
+
overflow: "hidden",
|
|
834
|
+
}, children: [img && (jsxRuntime.jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onTouchStart: handleCanvasTouchStart, style: {
|
|
850
835
|
width: displayW,
|
|
851
836
|
height: displayH,
|
|
852
837
|
cursor: isDragging ? "grabbing" : "grab",
|
|
@@ -879,12 +864,12 @@ const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
|
|
|
879
864
|
};
|
|
880
865
|
const useVideoScrubbing = (videoRef, options = {}) => {
|
|
881
866
|
const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
|
|
882
|
-
const [isDragging, setIsDragging] =
|
|
883
|
-
const [dragStartX, setDragStartX] =
|
|
884
|
-
const [dragStartTime, setDragStartTime] =
|
|
885
|
-
const hasDragged =
|
|
867
|
+
const [isDragging, setIsDragging] = react.useState(false);
|
|
868
|
+
const [dragStartX, setDragStartX] = react.useState(0);
|
|
869
|
+
const [dragStartTime, setDragStartTime] = react.useState(0);
|
|
870
|
+
const hasDragged = react.useRef(false);
|
|
886
871
|
// Helper to start dragging (common logic for mouse and touch)
|
|
887
|
-
const startDrag =
|
|
872
|
+
const startDrag = react.useCallback((clientX, event) => {
|
|
888
873
|
if (!videoRef.current)
|
|
889
874
|
return;
|
|
890
875
|
setIsDragging(true);
|
|
@@ -894,7 +879,7 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
894
879
|
event.preventDefault();
|
|
895
880
|
}, [videoRef]);
|
|
896
881
|
// Helper to handle drag movement (common logic for mouse and touch)
|
|
897
|
-
const handleDragMove =
|
|
882
|
+
const handleDragMove = react.useCallback((clientX, sensitivity) => {
|
|
898
883
|
if (!isDragging || !videoRef.current)
|
|
899
884
|
return;
|
|
900
885
|
const deltaX = clientX - dragStartX;
|
|
@@ -905,29 +890,29 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
905
890
|
}
|
|
906
891
|
}, [isDragging, dragStartX, dragStartTime, videoRef]);
|
|
907
892
|
// Helper to end dragging (common logic for mouse and touch)
|
|
908
|
-
const endDrag =
|
|
893
|
+
const endDrag = react.useCallback(() => {
|
|
909
894
|
setIsDragging(false);
|
|
910
895
|
}, []);
|
|
911
|
-
const handleMouseDown =
|
|
896
|
+
const handleMouseDown = react.useCallback((e) => {
|
|
912
897
|
startDrag(e.clientX, e.nativeEvent);
|
|
913
898
|
}, [startDrag]);
|
|
914
|
-
const handleTouchStart =
|
|
899
|
+
const handleTouchStart = react.useCallback((e) => {
|
|
915
900
|
startDrag(e.touches[0].clientX, e.nativeEvent);
|
|
916
901
|
}, [startDrag]);
|
|
917
|
-
const handleDocumentMouseMove =
|
|
902
|
+
const handleDocumentMouseMove = react.useCallback((e) => {
|
|
918
903
|
handleDragMove(getClientX(e), mouseSensitivity);
|
|
919
904
|
}, [handleDragMove, mouseSensitivity]);
|
|
920
|
-
const handleDocumentTouchMove =
|
|
905
|
+
const handleDocumentTouchMove = react.useCallback((e) => {
|
|
921
906
|
handleDragMove(getClientX(e), touchSensitivity);
|
|
922
907
|
}, [handleDragMove, touchSensitivity]);
|
|
923
|
-
const handleDocumentMouseUp =
|
|
908
|
+
const handleDocumentMouseUp = react.useCallback(() => {
|
|
924
909
|
endDrag();
|
|
925
910
|
}, [endDrag]);
|
|
926
|
-
const handleDocumentTouchEnd =
|
|
911
|
+
const handleDocumentTouchEnd = react.useCallback(() => {
|
|
927
912
|
endDrag();
|
|
928
913
|
}, [endDrag]);
|
|
929
914
|
// Add document-level event listeners when dragging starts
|
|
930
|
-
|
|
915
|
+
react.useEffect(() => {
|
|
931
916
|
if (isDragging) {
|
|
932
917
|
document.addEventListener("mousemove", handleDocumentMouseMove);
|
|
933
918
|
document.addEventListener("mouseup", handleDocumentMouseUp);
|
|
@@ -956,9 +941,9 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
956
941
|
};
|
|
957
942
|
|
|
958
943
|
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] =
|
|
944
|
+
const videoRef = react.useRef(null);
|
|
945
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
946
|
+
const [bouncingAllowed, setBouncingAllowed] = react.useState(false);
|
|
962
947
|
const displayW = width ?? size ?? 300;
|
|
963
948
|
const displayH = height ?? size ?? 300;
|
|
964
949
|
// Use custom hook for build rendering
|
|
@@ -968,18 +953,18 @@ const BuildRenderVideo = ({ parts, width, height, size, apiConfig, useBuildRende
|
|
|
968
953
|
mouseSensitivity,
|
|
969
954
|
touchSensitivity,
|
|
970
955
|
});
|
|
971
|
-
const handleLoadStartInternal =
|
|
956
|
+
const handleLoadStartInternal = react.useCallback(() => {
|
|
972
957
|
setIsLoading(true);
|
|
973
958
|
setBouncingAllowed(false);
|
|
974
959
|
}, []);
|
|
975
|
-
const handleCanPlayInternal =
|
|
960
|
+
const handleCanPlayInternal = react.useCallback(() => {
|
|
976
961
|
setIsLoading(false);
|
|
977
962
|
// Start bouncing animation after delay
|
|
978
963
|
setTimeout(() => {
|
|
979
964
|
setBouncingAllowed(true);
|
|
980
965
|
}, 2000);
|
|
981
966
|
}, []);
|
|
982
|
-
|
|
967
|
+
react.useEffect(() => {
|
|
983
968
|
if (hasDragged.current || !videoRef.current)
|
|
984
969
|
return;
|
|
985
970
|
const duration = videoRef.current.duration;
|