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