@buildcores/render-client 1.0.3 → 1.0.4
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 +4 -4
- package/dist/BuildRenderVideo.d.ts +2 -0
- package/dist/SpriteRender.d.ts +2 -0
- package/dist/api.d.ts +18 -1
- package/dist/hooks/useSpriteRender.d.ts +12 -0
- package/dist/hooks/useSpriteScrubbing.d.ts +16 -0
- package/dist/index.d.ts +80 -89
- package/dist/index.esm.js +353 -105
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +356 -104
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +82 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,47 +4,51 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
4
4
|
var React = require('react');
|
|
5
5
|
|
|
6
6
|
// Helper to extract clientX from mouse or touch events
|
|
7
|
-
const getClientX = (e) => {
|
|
7
|
+
const getClientX$1 = (e) => {
|
|
8
8
|
return "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
9
9
|
};
|
|
10
|
-
// Helper to calculate new
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
let
|
|
10
|
+
// Helper to calculate new frame with circular wrapping
|
|
11
|
+
const calculateCircularFrame = (startFrame, deltaX, sensitivity, totalFrames) => {
|
|
12
|
+
const frameDelta = deltaX * sensitivity;
|
|
13
|
+
let newFrame = startFrame + frameDelta;
|
|
14
14
|
// Make it circular - wrap around when going past boundaries
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
15
|
+
newFrame = newFrame % totalFrames;
|
|
16
|
+
if (newFrame < 0) {
|
|
17
|
+
newFrame += totalFrames;
|
|
18
18
|
}
|
|
19
|
-
return
|
|
19
|
+
return newFrame;
|
|
20
20
|
};
|
|
21
|
-
const
|
|
22
|
-
const { mouseSensitivity = 0.
|
|
21
|
+
const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
|
|
22
|
+
const { mouseSensitivity = 0.1, touchSensitivity = 0.1, onFrameChange, } = options;
|
|
23
23
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
24
24
|
const [dragStartX, setDragStartX] = React.useState(0);
|
|
25
|
-
const [
|
|
25
|
+
const [dragStartFrame, setDragStartFrame] = React.useState(0);
|
|
26
26
|
const hasDragged = React.useRef(false);
|
|
27
|
+
const currentFrame = React.useRef(0);
|
|
27
28
|
// Helper to start dragging (common logic for mouse and touch)
|
|
28
29
|
const startDrag = React.useCallback((clientX, event) => {
|
|
29
|
-
if (!
|
|
30
|
+
if (!canvasRef.current)
|
|
30
31
|
return;
|
|
31
32
|
setIsDragging(true);
|
|
32
33
|
setDragStartX(clientX);
|
|
33
|
-
|
|
34
|
+
setDragStartFrame(currentFrame.current);
|
|
34
35
|
hasDragged.current = true;
|
|
35
36
|
event.preventDefault();
|
|
36
|
-
}, [
|
|
37
|
+
}, [canvasRef]);
|
|
37
38
|
// Helper to handle drag movement (common logic for mouse and touch)
|
|
38
39
|
const handleDragMove = React.useCallback((clientX, sensitivity) => {
|
|
39
|
-
if (!isDragging || !
|
|
40
|
+
if (!isDragging || !canvasRef.current)
|
|
40
41
|
return;
|
|
41
42
|
const deltaX = clientX - dragStartX;
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const newFrame = calculateCircularFrame(dragStartFrame, deltaX, // Positive for natural "spin right" feel
|
|
44
|
+
sensitivity, totalFrames);
|
|
45
|
+
currentFrame.current = newFrame;
|
|
46
|
+
// Call the frame change callback if provided
|
|
47
|
+
if (onFrameChange) {
|
|
48
|
+
onFrameChange(newFrame);
|
|
46
49
|
}
|
|
47
|
-
|
|
50
|
+
return newFrame;
|
|
51
|
+
}, [isDragging, dragStartX, dragStartFrame, totalFrames, onFrameChange]);
|
|
48
52
|
// Helper to end dragging (common logic for mouse and touch)
|
|
49
53
|
const endDrag = React.useCallback(() => {
|
|
50
54
|
setIsDragging(false);
|
|
@@ -56,10 +60,10 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
56
60
|
startDrag(e.touches[0].clientX, e.nativeEvent);
|
|
57
61
|
}, [startDrag]);
|
|
58
62
|
const handleDocumentMouseMove = React.useCallback((e) => {
|
|
59
|
-
handleDragMove(getClientX(e), mouseSensitivity);
|
|
63
|
+
return handleDragMove(getClientX$1(e), mouseSensitivity);
|
|
60
64
|
}, [handleDragMove, mouseSensitivity]);
|
|
61
65
|
const handleDocumentTouchMove = React.useCallback((e) => {
|
|
62
|
-
handleDragMove(getClientX(e), touchSensitivity);
|
|
66
|
+
return handleDragMove(getClientX$1(e), touchSensitivity);
|
|
63
67
|
}, [handleDragMove, touchSensitivity]);
|
|
64
68
|
const handleDocumentMouseUp = React.useCallback(() => {
|
|
65
69
|
endDrag();
|
|
@@ -93,6 +97,10 @@ const useVideoScrubbing = (videoRef, options = {}) => {
|
|
|
93
97
|
handleMouseDown,
|
|
94
98
|
handleTouchStart,
|
|
95
99
|
hasDragged,
|
|
100
|
+
currentFrame: currentFrame.current,
|
|
101
|
+
setCurrentFrame: (frame) => {
|
|
102
|
+
currentFrame.current = frame;
|
|
103
|
+
},
|
|
96
104
|
};
|
|
97
105
|
};
|
|
98
106
|
|
|
@@ -313,48 +321,6 @@ function useBouncePatternProgress(enabled = true) {
|
|
|
313
321
|
return { value, isBouncing };
|
|
314
322
|
}
|
|
315
323
|
|
|
316
|
-
// API Types
|
|
317
|
-
/**
|
|
318
|
-
* Enum defining all available PC part categories that can be rendered.
|
|
319
|
-
*
|
|
320
|
-
* Each category represents a different type of computer component that can be
|
|
321
|
-
* included in the 3D build visualization.
|
|
322
|
-
*
|
|
323
|
-
* @example
|
|
324
|
-
* ```tsx
|
|
325
|
-
* // All available categories
|
|
326
|
-
* const categories = [
|
|
327
|
-
* PartCategory.CPU, // "CPU"
|
|
328
|
-
* PartCategory.GPU, // "GPU"
|
|
329
|
-
* PartCategory.RAM, // "RAM"
|
|
330
|
-
* PartCategory.Motherboard,// "Motherboard"
|
|
331
|
-
* PartCategory.PSU, // "PSU"
|
|
332
|
-
* PartCategory.Storage, // "Storage"
|
|
333
|
-
* PartCategory.PCCase, // "PCCase"
|
|
334
|
-
* PartCategory.CPUCooler, // "CPUCooler"
|
|
335
|
-
* ];
|
|
336
|
-
* ```
|
|
337
|
-
*/
|
|
338
|
-
exports.PartCategory = void 0;
|
|
339
|
-
(function (PartCategory) {
|
|
340
|
-
/** Central Processing Unit - The main processor */
|
|
341
|
-
PartCategory["CPU"] = "CPU";
|
|
342
|
-
/** Graphics Processing Unit - Video card for rendering */
|
|
343
|
-
PartCategory["GPU"] = "GPU";
|
|
344
|
-
/** Random Access Memory - System memory modules */
|
|
345
|
-
PartCategory["RAM"] = "RAM";
|
|
346
|
-
/** Main circuit board that connects all components */
|
|
347
|
-
PartCategory["Motherboard"] = "Motherboard";
|
|
348
|
-
/** Power Supply Unit - Provides power to all components */
|
|
349
|
-
PartCategory["PSU"] = "PSU";
|
|
350
|
-
/** Storage devices like SSDs, HDDs, NVMe drives */
|
|
351
|
-
PartCategory["Storage"] = "Storage";
|
|
352
|
-
/** PC Case - The enclosure that houses all components */
|
|
353
|
-
PartCategory["PCCase"] = "PCCase";
|
|
354
|
-
/** CPU Cooler - Air or liquid cooling for the processor */
|
|
355
|
-
PartCategory["CPUCooler"] = "CPUCooler";
|
|
356
|
-
})(exports.PartCategory || (exports.PartCategory = {}));
|
|
357
|
-
|
|
358
324
|
// API Configuration
|
|
359
325
|
const API_BASE_URL = "https://squid-app-7aeyk.ondigitalocean.app";
|
|
360
326
|
// API Endpoints
|
|
@@ -368,12 +334,16 @@ const buildApiUrl = (endpoint) => {
|
|
|
368
334
|
};
|
|
369
335
|
// API Implementation
|
|
370
336
|
const renderBuildExperimental = async (request) => {
|
|
337
|
+
const requestWithFormat = {
|
|
338
|
+
...request,
|
|
339
|
+
format: request.format || "video", // Default to video format
|
|
340
|
+
};
|
|
371
341
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL), {
|
|
372
342
|
method: "POST",
|
|
373
343
|
headers: {
|
|
374
344
|
"Content-Type": "application/json",
|
|
375
345
|
},
|
|
376
|
-
body: JSON.stringify(
|
|
346
|
+
body: JSON.stringify(requestWithFormat),
|
|
377
347
|
});
|
|
378
348
|
if (!response.ok) {
|
|
379
349
|
throw new Error(`Render build failed: ${response.status} ${response.statusText}`);
|
|
@@ -387,6 +357,33 @@ const renderBuildExperimental = async (request) => {
|
|
|
387
357
|
},
|
|
388
358
|
};
|
|
389
359
|
};
|
|
360
|
+
const renderSpriteExperimental = async (request) => {
|
|
361
|
+
const requestWithFormat = {
|
|
362
|
+
...request,
|
|
363
|
+
format: "sprite",
|
|
364
|
+
};
|
|
365
|
+
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL), {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: {
|
|
368
|
+
"Content-Type": "application/json",
|
|
369
|
+
},
|
|
370
|
+
body: JSON.stringify(requestWithFormat),
|
|
371
|
+
});
|
|
372
|
+
if (!response.ok) {
|
|
373
|
+
throw new Error(`Render sprite failed: ${response.status} ${response.statusText}`);
|
|
374
|
+
}
|
|
375
|
+
const sprite = await response.blob();
|
|
376
|
+
return {
|
|
377
|
+
sprite,
|
|
378
|
+
metadata: {
|
|
379
|
+
cols: 12, // Default sprite grid - could be returned from API
|
|
380
|
+
rows: 6,
|
|
381
|
+
totalFrames: 72,
|
|
382
|
+
size: sprite.size,
|
|
383
|
+
format: "image/webp",
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
};
|
|
390
387
|
const getAvailableParts = async () => {
|
|
391
388
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.AVAILABLE_PARTS), {
|
|
392
389
|
method: "GET",
|
|
@@ -400,6 +397,48 @@ const getAvailableParts = async () => {
|
|
|
400
397
|
return response.json();
|
|
401
398
|
};
|
|
402
399
|
|
|
400
|
+
// API Types
|
|
401
|
+
/**
|
|
402
|
+
* Enum defining all available PC part categories that can be rendered.
|
|
403
|
+
*
|
|
404
|
+
* Each category represents a different type of computer component that can be
|
|
405
|
+
* included in the 3D build visualization.
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```tsx
|
|
409
|
+
* // All available categories
|
|
410
|
+
* const categories = [
|
|
411
|
+
* PartCategory.CPU, // "CPU"
|
|
412
|
+
* PartCategory.GPU, // "GPU"
|
|
413
|
+
* PartCategory.RAM, // "RAM"
|
|
414
|
+
* PartCategory.Motherboard,// "Motherboard"
|
|
415
|
+
* PartCategory.PSU, // "PSU"
|
|
416
|
+
* PartCategory.Storage, // "Storage"
|
|
417
|
+
* PartCategory.PCCase, // "PCCase"
|
|
418
|
+
* PartCategory.CPUCooler, // "CPUCooler"
|
|
419
|
+
* ];
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
422
|
+
exports.PartCategory = void 0;
|
|
423
|
+
(function (PartCategory) {
|
|
424
|
+
/** Central Processing Unit - The main processor */
|
|
425
|
+
PartCategory["CPU"] = "CPU";
|
|
426
|
+
/** Graphics Processing Unit - Video card for rendering */
|
|
427
|
+
PartCategory["GPU"] = "GPU";
|
|
428
|
+
/** Random Access Memory - System memory modules */
|
|
429
|
+
PartCategory["RAM"] = "RAM";
|
|
430
|
+
/** Main circuit board that connects all components */
|
|
431
|
+
PartCategory["Motherboard"] = "Motherboard";
|
|
432
|
+
/** Power Supply Unit - Provides power to all components */
|
|
433
|
+
PartCategory["PSU"] = "PSU";
|
|
434
|
+
/** Storage devices like SSDs, HDDs, NVMe drives */
|
|
435
|
+
PartCategory["Storage"] = "Storage";
|
|
436
|
+
/** PC Case - The enclosure that houses all components */
|
|
437
|
+
PartCategory["PCCase"] = "PCCase";
|
|
438
|
+
/** CPU Cooler - Air or liquid cooling for the processor */
|
|
439
|
+
PartCategory["CPUCooler"] = "CPUCooler";
|
|
440
|
+
})(exports.PartCategory || (exports.PartCategory = {}));
|
|
441
|
+
|
|
403
442
|
/**
|
|
404
443
|
* Compares two RenderBuildRequest objects for equality by checking if the same IDs
|
|
405
444
|
* are present in each category array, regardless of order.
|
|
@@ -478,6 +517,65 @@ const useBuildRender = (parts, onLoadStart) => {
|
|
|
478
517
|
};
|
|
479
518
|
};
|
|
480
519
|
|
|
520
|
+
const useSpriteRender = (parts, onLoadStart) => {
|
|
521
|
+
const [spriteSrc, setSpriteSrc] = React.useState(null);
|
|
522
|
+
const [isRenderingSprite, setIsRenderingSprite] = React.useState(false);
|
|
523
|
+
const [renderError, setRenderError] = React.useState(null);
|
|
524
|
+
const [spriteMetadata, setSpriteMetadata] = React.useState(null);
|
|
525
|
+
const previousPartsRef = React.useRef(null);
|
|
526
|
+
const fetchRenderSprite = React.useCallback(async (currentParts) => {
|
|
527
|
+
try {
|
|
528
|
+
setIsRenderingSprite(true);
|
|
529
|
+
setRenderError(null);
|
|
530
|
+
onLoadStart?.();
|
|
531
|
+
const response = await renderSpriteExperimental(currentParts);
|
|
532
|
+
const objectUrl = URL.createObjectURL(response.sprite);
|
|
533
|
+
// Clean up previous sprite URL before setting new one
|
|
534
|
+
setSpriteSrc((prevSrc) => {
|
|
535
|
+
if (prevSrc) {
|
|
536
|
+
URL.revokeObjectURL(prevSrc);
|
|
537
|
+
}
|
|
538
|
+
return objectUrl;
|
|
539
|
+
});
|
|
540
|
+
// Set sprite metadata
|
|
541
|
+
setSpriteMetadata({
|
|
542
|
+
cols: response.metadata?.cols || 12,
|
|
543
|
+
rows: response.metadata?.rows || 6,
|
|
544
|
+
totalFrames: response.metadata?.totalFrames || 72,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
setRenderError(error instanceof Error ? error.message : "Failed to render sprite");
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
setIsRenderingSprite(false);
|
|
552
|
+
}
|
|
553
|
+
}, [onLoadStart]);
|
|
554
|
+
// Effect to call API when parts content changes (using custom equality check)
|
|
555
|
+
React.useEffect(() => {
|
|
556
|
+
const shouldFetch = previousPartsRef.current === null ||
|
|
557
|
+
!arePartsEqual(previousPartsRef.current, parts);
|
|
558
|
+
if (shouldFetch) {
|
|
559
|
+
previousPartsRef.current = parts;
|
|
560
|
+
fetchRenderSprite(parts);
|
|
561
|
+
}
|
|
562
|
+
}, [parts, fetchRenderSprite]);
|
|
563
|
+
// Cleanup effect for component unmount
|
|
564
|
+
React.useEffect(() => {
|
|
565
|
+
return () => {
|
|
566
|
+
if (spriteSrc) {
|
|
567
|
+
URL.revokeObjectURL(spriteSrc);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}, [spriteSrc]);
|
|
571
|
+
return {
|
|
572
|
+
spriteSrc,
|
|
573
|
+
isRenderingSprite,
|
|
574
|
+
renderError,
|
|
575
|
+
spriteMetadata,
|
|
576
|
+
};
|
|
577
|
+
};
|
|
578
|
+
|
|
481
579
|
const LoadingErrorOverlay = ({ isVisible, renderError, size, }) => {
|
|
482
580
|
if (!isVisible)
|
|
483
581
|
return null;
|
|
@@ -513,7 +611,7 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
|
|
|
513
611
|
position: "absolute",
|
|
514
612
|
top: "50%",
|
|
515
613
|
left: "50%",
|
|
516
|
-
transform: `translate(-50%, -50%) translateX(${progressValue *
|
|
614
|
+
transform: `translate(-50%, -50%) translateX(${progressValue * 50}px)`,
|
|
517
615
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
518
616
|
color: "white",
|
|
519
617
|
padding: "12px",
|
|
@@ -532,64 +630,214 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
|
|
|
532
630
|
} })) }));
|
|
533
631
|
};
|
|
534
632
|
|
|
535
|
-
const BuildRender = ({ parts, size, mouseSensitivity = 0.
|
|
536
|
-
const
|
|
633
|
+
const BuildRender = ({ parts, size, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
|
|
634
|
+
const canvasRef = React.useRef(null);
|
|
635
|
+
const [img, setImg] = React.useState(null);
|
|
537
636
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
538
637
|
const [bouncingAllowed, setBouncingAllowed] = React.useState(false);
|
|
539
|
-
// Use custom hook for
|
|
540
|
-
const {
|
|
638
|
+
// Use custom hook for sprite rendering
|
|
639
|
+
const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(parts);
|
|
541
640
|
const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed);
|
|
542
|
-
const
|
|
641
|
+
const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
|
|
642
|
+
const cols = spriteMetadata ? spriteMetadata.cols : 12;
|
|
643
|
+
const rows = spriteMetadata ? spriteMetadata.rows : 6;
|
|
644
|
+
const frameRef = React.useRef(0);
|
|
645
|
+
// Image/frame sizes
|
|
646
|
+
const frameW = img ? img.width / cols : 0;
|
|
647
|
+
const frameH = img ? img.height / rows : 0;
|
|
648
|
+
// ---- Load sprite image ----
|
|
649
|
+
React.useEffect(() => {
|
|
650
|
+
if (!spriteSrc) {
|
|
651
|
+
setImg(null);
|
|
652
|
+
setIsLoading(true);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
setIsLoading(true);
|
|
656
|
+
const i = new Image();
|
|
657
|
+
i.decoding = "async";
|
|
658
|
+
i.loading = "eager";
|
|
659
|
+
i.src = spriteSrc;
|
|
660
|
+
i.onload = () => {
|
|
661
|
+
setImg(i);
|
|
662
|
+
setIsLoading(false);
|
|
663
|
+
// Start bouncing animation after delay
|
|
664
|
+
setTimeout(() => {
|
|
665
|
+
setBouncingAllowed(true);
|
|
666
|
+
}, 2000);
|
|
667
|
+
};
|
|
668
|
+
i.onerror = () => {
|
|
669
|
+
setImg(null);
|
|
670
|
+
setIsLoading(false);
|
|
671
|
+
};
|
|
672
|
+
}, [spriteSrc]);
|
|
673
|
+
// ---- Drawing function ----
|
|
674
|
+
const draw = React.useCallback((frameIndex) => {
|
|
675
|
+
const cnv = canvasRef.current;
|
|
676
|
+
if (!cnv || !img || !frameW || !frameH)
|
|
677
|
+
return;
|
|
678
|
+
const ctx = cnv.getContext("2d");
|
|
679
|
+
if (!ctx)
|
|
680
|
+
return;
|
|
681
|
+
// Backing store sized for HiDPI; CSS size stays `size`
|
|
682
|
+
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
683
|
+
const targetW = Math.round(size * dpr);
|
|
684
|
+
const targetH = Math.round(size * dpr);
|
|
685
|
+
if (cnv.width !== targetW || cnv.height !== targetH) {
|
|
686
|
+
cnv.width = targetW;
|
|
687
|
+
cnv.height = targetH;
|
|
688
|
+
}
|
|
689
|
+
// Snap to integer frame (never between tiles)
|
|
690
|
+
let n = Math.round(frameIndex) % total;
|
|
691
|
+
if (n < 0)
|
|
692
|
+
n += total;
|
|
693
|
+
const r = Math.floor(n / cols);
|
|
694
|
+
const c = n % cols;
|
|
695
|
+
// Use integer source rects to avoid sampling bleed across tiles
|
|
696
|
+
const sx = Math.round(c * frameW);
|
|
697
|
+
const sy = Math.round(r * frameH);
|
|
698
|
+
const sw = Math.round(frameW);
|
|
699
|
+
const sh = Math.round(frameH);
|
|
700
|
+
ctx.clearRect(0, 0, targetW, targetH);
|
|
701
|
+
ctx.imageSmoothingEnabled = true;
|
|
702
|
+
ctx.imageSmoothingQuality = "high";
|
|
703
|
+
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
|
|
704
|
+
}, [img, frameW, frameH, size, cols, total]);
|
|
705
|
+
const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useSpriteScrubbing(canvasRef, total, {
|
|
543
706
|
mouseSensitivity,
|
|
544
707
|
touchSensitivity,
|
|
708
|
+
onFrameChange: (newFrame) => {
|
|
709
|
+
frameRef.current = newFrame;
|
|
710
|
+
draw(newFrame);
|
|
711
|
+
},
|
|
545
712
|
});
|
|
546
|
-
|
|
713
|
+
React.useCallback(() => {
|
|
547
714
|
setIsLoading(true);
|
|
548
715
|
setBouncingAllowed(false);
|
|
549
716
|
}, []);
|
|
550
|
-
|
|
551
|
-
setIsLoading(false);
|
|
552
|
-
// Start bouncing animation after delay
|
|
553
|
-
setTimeout(() => {
|
|
554
|
-
setBouncingAllowed(true);
|
|
555
|
-
}, 2000);
|
|
556
|
-
}, []);
|
|
717
|
+
// Auto-rotate when bouncing is allowed and not dragged
|
|
557
718
|
React.useEffect(() => {
|
|
558
|
-
if (hasDragged.current || !
|
|
719
|
+
if (hasDragged.current || !img)
|
|
559
720
|
return;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
721
|
+
// Calculate frame based on progress value (similar to video time calculation)
|
|
722
|
+
const frame = ((progressValue / 5) * total) % total;
|
|
723
|
+
frameRef.current = frame;
|
|
724
|
+
draw(frame);
|
|
725
|
+
}, [progressValue, hasDragged, img, total, draw]);
|
|
726
|
+
// Initial draw once image is ready
|
|
727
|
+
React.useEffect(() => {
|
|
728
|
+
if (img && !isLoading) {
|
|
729
|
+
draw(frameRef.current);
|
|
566
730
|
}
|
|
567
|
-
}, [
|
|
568
|
-
return (jsxRuntime.jsxs("div", { style: { position: "relative", width: size, height: size }, children: [
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
}, style: {
|
|
731
|
+
}, [img, isLoading, draw]);
|
|
732
|
+
return (jsxRuntime.jsxs("div", { style: { position: "relative", width: size, height: size }, children: [img && (jsxRuntime.jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, style: {
|
|
733
|
+
width: size,
|
|
734
|
+
height: size,
|
|
573
735
|
cursor: isDragging ? "grabbing" : "grab",
|
|
574
736
|
touchAction: "none", // Prevents default touch behaviors like scrolling
|
|
575
737
|
display: "block",
|
|
576
|
-
// Completely hide video controls on all browsers including mobile
|
|
577
|
-
WebkitMediaControls: "none",
|
|
578
|
-
MozMediaControls: "none",
|
|
579
|
-
OMediaControls: "none",
|
|
580
|
-
msMediaControls: "none",
|
|
581
|
-
mediaControls: "none",
|
|
582
|
-
// Additional iOS-specific properties
|
|
583
|
-
WebkitTouchCallout: "none",
|
|
584
|
-
WebkitUserSelect: "none",
|
|
585
738
|
userSelect: "none",
|
|
586
|
-
|
|
587
|
-
|
|
739
|
+
WebkitUserSelect: "none",
|
|
740
|
+
WebkitTouchCallout: "none",
|
|
741
|
+
}, role: "img", "aria-label": "360\u00B0 viewer", onContextMenu: (e) => e.preventDefault() })), jsxRuntime.jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingSprite || !!renderError, renderError: renderError || undefined, size: size }), jsxRuntime.jsx(InstructionTooltip, { isVisible: !isLoading &&
|
|
742
|
+
!isRenderingSprite &&
|
|
588
743
|
!renderError &&
|
|
589
744
|
isBouncing &&
|
|
590
745
|
!hasDragged.current, progressValue: progressValue })] }));
|
|
591
746
|
};
|
|
592
747
|
|
|
748
|
+
// Helper to extract clientX from mouse or touch events
|
|
749
|
+
const getClientX = (e) => {
|
|
750
|
+
return "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
751
|
+
};
|
|
752
|
+
// Helper to calculate new video time with circular wrapping
|
|
753
|
+
const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
|
|
754
|
+
const timeDelta = deltaX * sensitivity;
|
|
755
|
+
let newTime = startTime + timeDelta;
|
|
756
|
+
// Make it circular - wrap around when going past boundaries
|
|
757
|
+
newTime = newTime % duration;
|
|
758
|
+
if (newTime < 0) {
|
|
759
|
+
newTime += duration;
|
|
760
|
+
}
|
|
761
|
+
return newTime;
|
|
762
|
+
};
|
|
763
|
+
const useVideoScrubbing = (videoRef, options = {}) => {
|
|
764
|
+
const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
|
|
765
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
766
|
+
const [dragStartX, setDragStartX] = React.useState(0);
|
|
767
|
+
const [dragStartTime, setDragStartTime] = React.useState(0);
|
|
768
|
+
const hasDragged = React.useRef(false);
|
|
769
|
+
// Helper to start dragging (common logic for mouse and touch)
|
|
770
|
+
const startDrag = React.useCallback((clientX, event) => {
|
|
771
|
+
if (!videoRef.current)
|
|
772
|
+
return;
|
|
773
|
+
setIsDragging(true);
|
|
774
|
+
setDragStartX(clientX);
|
|
775
|
+
setDragStartTime(videoRef.current.currentTime);
|
|
776
|
+
hasDragged.current = true;
|
|
777
|
+
event.preventDefault();
|
|
778
|
+
}, [videoRef]);
|
|
779
|
+
// Helper to handle drag movement (common logic for mouse and touch)
|
|
780
|
+
const handleDragMove = React.useCallback((clientX, sensitivity) => {
|
|
781
|
+
if (!isDragging || !videoRef.current)
|
|
782
|
+
return;
|
|
783
|
+
const deltaX = clientX - dragStartX;
|
|
784
|
+
const duration = videoRef.current.duration || 0;
|
|
785
|
+
if (duration > 0) {
|
|
786
|
+
const newTime = calculateCircularTime(dragStartTime, deltaX, sensitivity, duration);
|
|
787
|
+
videoRef.current.currentTime = newTime;
|
|
788
|
+
}
|
|
789
|
+
}, [isDragging, dragStartX, dragStartTime, videoRef]);
|
|
790
|
+
// Helper to end dragging (common logic for mouse and touch)
|
|
791
|
+
const endDrag = React.useCallback(() => {
|
|
792
|
+
setIsDragging(false);
|
|
793
|
+
}, []);
|
|
794
|
+
const handleMouseDown = React.useCallback((e) => {
|
|
795
|
+
startDrag(e.clientX, e.nativeEvent);
|
|
796
|
+
}, [startDrag]);
|
|
797
|
+
const handleTouchStart = React.useCallback((e) => {
|
|
798
|
+
startDrag(e.touches[0].clientX, e.nativeEvent);
|
|
799
|
+
}, [startDrag]);
|
|
800
|
+
const handleDocumentMouseMove = React.useCallback((e) => {
|
|
801
|
+
handleDragMove(getClientX(e), mouseSensitivity);
|
|
802
|
+
}, [handleDragMove, mouseSensitivity]);
|
|
803
|
+
const handleDocumentTouchMove = React.useCallback((e) => {
|
|
804
|
+
handleDragMove(getClientX(e), touchSensitivity);
|
|
805
|
+
}, [handleDragMove, touchSensitivity]);
|
|
806
|
+
const handleDocumentMouseUp = React.useCallback(() => {
|
|
807
|
+
endDrag();
|
|
808
|
+
}, [endDrag]);
|
|
809
|
+
const handleDocumentTouchEnd = React.useCallback(() => {
|
|
810
|
+
endDrag();
|
|
811
|
+
}, [endDrag]);
|
|
812
|
+
// Add document-level event listeners when dragging starts
|
|
813
|
+
React.useEffect(() => {
|
|
814
|
+
if (isDragging) {
|
|
815
|
+
document.addEventListener("mousemove", handleDocumentMouseMove);
|
|
816
|
+
document.addEventListener("mouseup", handleDocumentMouseUp);
|
|
817
|
+
document.addEventListener("touchmove", handleDocumentTouchMove);
|
|
818
|
+
document.addEventListener("touchend", handleDocumentTouchEnd);
|
|
819
|
+
return () => {
|
|
820
|
+
document.removeEventListener("mousemove", handleDocumentMouseMove);
|
|
821
|
+
document.removeEventListener("mouseup", handleDocumentMouseUp);
|
|
822
|
+
document.removeEventListener("touchmove", handleDocumentTouchMove);
|
|
823
|
+
document.removeEventListener("touchend", handleDocumentTouchEnd);
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}, [
|
|
827
|
+
isDragging,
|
|
828
|
+
handleDocumentMouseMove,
|
|
829
|
+
handleDocumentMouseUp,
|
|
830
|
+
handleDocumentTouchMove,
|
|
831
|
+
handleDocumentTouchEnd,
|
|
832
|
+
]);
|
|
833
|
+
return {
|
|
834
|
+
isDragging,
|
|
835
|
+
handleMouseDown,
|
|
836
|
+
handleTouchStart,
|
|
837
|
+
hasDragged,
|
|
838
|
+
};
|
|
839
|
+
};
|
|
840
|
+
|
|
593
841
|
exports.API_BASE_URL = API_BASE_URL;
|
|
594
842
|
exports.API_ENDPOINTS = API_ENDPOINTS;
|
|
595
843
|
exports.BuildRender = BuildRender;
|
|
@@ -598,10 +846,14 @@ exports.InstructionTooltip = InstructionTooltip;
|
|
|
598
846
|
exports.LoadingErrorOverlay = LoadingErrorOverlay;
|
|
599
847
|
exports.arePartsEqual = arePartsEqual;
|
|
600
848
|
exports.buildApiUrl = buildApiUrl;
|
|
849
|
+
exports.calculateCircularFrame = calculateCircularFrame;
|
|
601
850
|
exports.calculateCircularTime = calculateCircularTime;
|
|
602
851
|
exports.getAvailableParts = getAvailableParts;
|
|
603
852
|
exports.renderBuildExperimental = renderBuildExperimental;
|
|
853
|
+
exports.renderSpriteExperimental = renderSpriteExperimental;
|
|
604
854
|
exports.useBouncePatternProgress = useBouncePatternProgress;
|
|
605
855
|
exports.useBuildRender = useBuildRender;
|
|
856
|
+
exports.useSpriteRender = useSpriteRender;
|
|
857
|
+
exports.useSpriteScrubbing = useSpriteScrubbing;
|
|
606
858
|
exports.useVideoScrubbing = useVideoScrubbing;
|
|
607
859
|
//# sourceMappingURL=index.js.map
|