@buildcores/render-client 1.0.3 → 1.0.5
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 +362 -108
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +365 -107
- 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;
|
|
@@ -487,18 +585,19 @@ const LoadingErrorOverlay = ({ isVisible, renderError, size, }) => {
|
|
|
487
585
|
left: 0,
|
|
488
586
|
right: 0,
|
|
489
587
|
bottom: 0,
|
|
490
|
-
backgroundColor: "rgba(0, 0, 0,
|
|
588
|
+
backgroundColor: "rgba(0, 0, 0, 1)",
|
|
491
589
|
display: "flex",
|
|
492
590
|
flexDirection: "column",
|
|
493
591
|
alignItems: "center",
|
|
494
592
|
justifyContent: "center",
|
|
495
593
|
color: "white",
|
|
496
594
|
zIndex: 10,
|
|
497
|
-
}, children: renderError ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { style: { marginBottom: "20px", fontSize: "18px" }, children: "Render Failed" }), jsxRuntime.jsx("div", { style: {
|
|
595
|
+
}, children: renderError ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { style: { marginBottom: "20px", fontSize: "18px", color: "white" }, children: "Render Failed" }), jsxRuntime.jsx("div", { style: {
|
|
498
596
|
fontSize: "14px",
|
|
499
597
|
textAlign: "center",
|
|
500
598
|
maxWidth: size * 0.8,
|
|
501
|
-
|
|
599
|
+
color: "white",
|
|
600
|
+
}, children: renderError })] })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx("div", { style: { marginBottom: "20px", fontSize: "18px", color: "white" }, children: "Loading Build..." }) })) }));
|
|
502
601
|
};
|
|
503
602
|
|
|
504
603
|
const DragIcon = ({ width = 24, height = 24, className, style, ...props }) => {
|
|
@@ -513,7 +612,7 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
|
|
|
513
612
|
position: "absolute",
|
|
514
613
|
top: "50%",
|
|
515
614
|
left: "50%",
|
|
516
|
-
transform: `translate(-50%, -50%) translateX(${progressValue *
|
|
615
|
+
transform: `translate(-50%, -50%) translateX(${progressValue * 50}px)`,
|
|
517
616
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
518
617
|
color: "white",
|
|
519
618
|
padding: "12px",
|
|
@@ -532,64 +631,219 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
|
|
|
532
631
|
} })) }));
|
|
533
632
|
};
|
|
534
633
|
|
|
535
|
-
const BuildRender = ({ parts, size, mouseSensitivity = 0.
|
|
536
|
-
const
|
|
634
|
+
const BuildRender = ({ parts, size, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
|
|
635
|
+
const canvasRef = React.useRef(null);
|
|
636
|
+
const [img, setImg] = React.useState(null);
|
|
537
637
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
538
638
|
const [bouncingAllowed, setBouncingAllowed] = React.useState(false);
|
|
539
|
-
// Use custom hook for
|
|
540
|
-
const {
|
|
639
|
+
// Use custom hook for sprite rendering
|
|
640
|
+
const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(parts);
|
|
541
641
|
const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed);
|
|
542
|
-
const
|
|
642
|
+
const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
|
|
643
|
+
const cols = spriteMetadata ? spriteMetadata.cols : 12;
|
|
644
|
+
const rows = spriteMetadata ? spriteMetadata.rows : 6;
|
|
645
|
+
const frameRef = React.useRef(0);
|
|
646
|
+
// Image/frame sizes
|
|
647
|
+
const frameW = img ? img.width / cols : 0;
|
|
648
|
+
const frameH = img ? img.height / rows : 0;
|
|
649
|
+
// ---- Load sprite image ----
|
|
650
|
+
React.useEffect(() => {
|
|
651
|
+
if (!spriteSrc) {
|
|
652
|
+
setImg(null);
|
|
653
|
+
setIsLoading(true);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
setIsLoading(true);
|
|
657
|
+
const i = new Image();
|
|
658
|
+
i.decoding = "async";
|
|
659
|
+
i.loading = "eager";
|
|
660
|
+
i.src = spriteSrc;
|
|
661
|
+
i.onload = () => {
|
|
662
|
+
setImg(i);
|
|
663
|
+
setIsLoading(false);
|
|
664
|
+
// Start bouncing animation after delay
|
|
665
|
+
setTimeout(() => {
|
|
666
|
+
setBouncingAllowed(true);
|
|
667
|
+
}, 2000);
|
|
668
|
+
};
|
|
669
|
+
i.onerror = () => {
|
|
670
|
+
setImg(null);
|
|
671
|
+
setIsLoading(false);
|
|
672
|
+
};
|
|
673
|
+
}, [spriteSrc]);
|
|
674
|
+
// ---- Drawing function ----
|
|
675
|
+
const draw = React.useCallback((frameIndex) => {
|
|
676
|
+
const cnv = canvasRef.current;
|
|
677
|
+
if (!cnv || !img || !frameW || !frameH)
|
|
678
|
+
return;
|
|
679
|
+
const ctx = cnv.getContext("2d");
|
|
680
|
+
if (!ctx)
|
|
681
|
+
return;
|
|
682
|
+
// Backing store sized for HiDPI; CSS size stays `size`
|
|
683
|
+
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
684
|
+
const targetW = Math.round(size * dpr);
|
|
685
|
+
const targetH = Math.round(size * dpr);
|
|
686
|
+
if (cnv.width !== targetW || cnv.height !== targetH) {
|
|
687
|
+
cnv.width = targetW;
|
|
688
|
+
cnv.height = targetH;
|
|
689
|
+
}
|
|
690
|
+
// Snap to integer frame (never between tiles)
|
|
691
|
+
let n = Math.round(frameIndex) % total;
|
|
692
|
+
if (n < 0)
|
|
693
|
+
n += total;
|
|
694
|
+
const r = Math.floor(n / cols);
|
|
695
|
+
const c = n % cols;
|
|
696
|
+
// Use integer source rects to avoid sampling bleed across tiles
|
|
697
|
+
const sx = Math.round(c * frameW);
|
|
698
|
+
const sy = Math.round(r * frameH);
|
|
699
|
+
const sw = Math.round(frameW);
|
|
700
|
+
const sh = Math.round(frameH);
|
|
701
|
+
ctx.clearRect(0, 0, targetW, targetH);
|
|
702
|
+
ctx.imageSmoothingEnabled = true;
|
|
703
|
+
ctx.imageSmoothingQuality = "high";
|
|
704
|
+
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
|
|
705
|
+
}, [img, frameW, frameH, size, cols, total]);
|
|
706
|
+
const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useSpriteScrubbing(canvasRef, total, {
|
|
543
707
|
mouseSensitivity,
|
|
544
708
|
touchSensitivity,
|
|
709
|
+
onFrameChange: (newFrame) => {
|
|
710
|
+
frameRef.current = newFrame;
|
|
711
|
+
draw(newFrame);
|
|
712
|
+
},
|
|
545
713
|
});
|
|
546
|
-
|
|
714
|
+
React.useCallback(() => {
|
|
547
715
|
setIsLoading(true);
|
|
548
716
|
setBouncingAllowed(false);
|
|
549
717
|
}, []);
|
|
550
|
-
|
|
551
|
-
setIsLoading(false);
|
|
552
|
-
// Start bouncing animation after delay
|
|
553
|
-
setTimeout(() => {
|
|
554
|
-
setBouncingAllowed(true);
|
|
555
|
-
}, 2000);
|
|
556
|
-
}, []);
|
|
718
|
+
// Auto-rotate when bouncing is allowed and not dragged
|
|
557
719
|
React.useEffect(() => {
|
|
558
|
-
if (hasDragged.current || !
|
|
720
|
+
if (hasDragged.current || !img)
|
|
559
721
|
return;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
722
|
+
// Calculate frame based on progress value (similar to video time calculation)
|
|
723
|
+
const frame = ((progressValue / 5) * total) % total;
|
|
724
|
+
frameRef.current = frame;
|
|
725
|
+
draw(frame);
|
|
726
|
+
}, [progressValue, hasDragged, img, total, draw]);
|
|
727
|
+
// Initial draw once image is ready
|
|
728
|
+
React.useEffect(() => {
|
|
729
|
+
if (img && !isLoading) {
|
|
730
|
+
draw(frameRef.current);
|
|
566
731
|
}
|
|
567
|
-
}, [
|
|
568
|
-
return (jsxRuntime.jsxs("div", { style: {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
732
|
+
}, [img, isLoading, draw]);
|
|
733
|
+
return (jsxRuntime.jsxs("div", { style: {
|
|
734
|
+
position: "relative",
|
|
735
|
+
width: size,
|
|
736
|
+
height: size,
|
|
737
|
+
backgroundColor: "black",
|
|
738
|
+
}, children: [img && (jsxRuntime.jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, style: {
|
|
739
|
+
width: size,
|
|
740
|
+
height: size,
|
|
573
741
|
cursor: isDragging ? "grabbing" : "grab",
|
|
574
742
|
touchAction: "none", // Prevents default touch behaviors like scrolling
|
|
575
743
|
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
744
|
userSelect: "none",
|
|
586
|
-
|
|
587
|
-
|
|
745
|
+
WebkitUserSelect: "none",
|
|
746
|
+
WebkitTouchCallout: "none",
|
|
747
|
+
}, 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 &&
|
|
748
|
+
!isRenderingSprite &&
|
|
588
749
|
!renderError &&
|
|
589
750
|
isBouncing &&
|
|
590
751
|
!hasDragged.current, progressValue: progressValue })] }));
|
|
591
752
|
};
|
|
592
753
|
|
|
754
|
+
// Helper to extract clientX from mouse or touch events
|
|
755
|
+
const getClientX = (e) => {
|
|
756
|
+
return "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
757
|
+
};
|
|
758
|
+
// Helper to calculate new video time with circular wrapping
|
|
759
|
+
const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
|
|
760
|
+
const timeDelta = deltaX * sensitivity;
|
|
761
|
+
let newTime = startTime + timeDelta;
|
|
762
|
+
// Make it circular - wrap around when going past boundaries
|
|
763
|
+
newTime = newTime % duration;
|
|
764
|
+
if (newTime < 0) {
|
|
765
|
+
newTime += duration;
|
|
766
|
+
}
|
|
767
|
+
return newTime;
|
|
768
|
+
};
|
|
769
|
+
const useVideoScrubbing = (videoRef, options = {}) => {
|
|
770
|
+
const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
|
|
771
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
772
|
+
const [dragStartX, setDragStartX] = React.useState(0);
|
|
773
|
+
const [dragStartTime, setDragStartTime] = React.useState(0);
|
|
774
|
+
const hasDragged = React.useRef(false);
|
|
775
|
+
// Helper to start dragging (common logic for mouse and touch)
|
|
776
|
+
const startDrag = React.useCallback((clientX, event) => {
|
|
777
|
+
if (!videoRef.current)
|
|
778
|
+
return;
|
|
779
|
+
setIsDragging(true);
|
|
780
|
+
setDragStartX(clientX);
|
|
781
|
+
setDragStartTime(videoRef.current.currentTime);
|
|
782
|
+
hasDragged.current = true;
|
|
783
|
+
event.preventDefault();
|
|
784
|
+
}, [videoRef]);
|
|
785
|
+
// Helper to handle drag movement (common logic for mouse and touch)
|
|
786
|
+
const handleDragMove = React.useCallback((clientX, sensitivity) => {
|
|
787
|
+
if (!isDragging || !videoRef.current)
|
|
788
|
+
return;
|
|
789
|
+
const deltaX = clientX - dragStartX;
|
|
790
|
+
const duration = videoRef.current.duration || 0;
|
|
791
|
+
if (duration > 0) {
|
|
792
|
+
const newTime = calculateCircularTime(dragStartTime, deltaX, sensitivity, duration);
|
|
793
|
+
videoRef.current.currentTime = newTime;
|
|
794
|
+
}
|
|
795
|
+
}, [isDragging, dragStartX, dragStartTime, videoRef]);
|
|
796
|
+
// Helper to end dragging (common logic for mouse and touch)
|
|
797
|
+
const endDrag = React.useCallback(() => {
|
|
798
|
+
setIsDragging(false);
|
|
799
|
+
}, []);
|
|
800
|
+
const handleMouseDown = React.useCallback((e) => {
|
|
801
|
+
startDrag(e.clientX, e.nativeEvent);
|
|
802
|
+
}, [startDrag]);
|
|
803
|
+
const handleTouchStart = React.useCallback((e) => {
|
|
804
|
+
startDrag(e.touches[0].clientX, e.nativeEvent);
|
|
805
|
+
}, [startDrag]);
|
|
806
|
+
const handleDocumentMouseMove = React.useCallback((e) => {
|
|
807
|
+
handleDragMove(getClientX(e), mouseSensitivity);
|
|
808
|
+
}, [handleDragMove, mouseSensitivity]);
|
|
809
|
+
const handleDocumentTouchMove = React.useCallback((e) => {
|
|
810
|
+
handleDragMove(getClientX(e), touchSensitivity);
|
|
811
|
+
}, [handleDragMove, touchSensitivity]);
|
|
812
|
+
const handleDocumentMouseUp = React.useCallback(() => {
|
|
813
|
+
endDrag();
|
|
814
|
+
}, [endDrag]);
|
|
815
|
+
const handleDocumentTouchEnd = React.useCallback(() => {
|
|
816
|
+
endDrag();
|
|
817
|
+
}, [endDrag]);
|
|
818
|
+
// Add document-level event listeners when dragging starts
|
|
819
|
+
React.useEffect(() => {
|
|
820
|
+
if (isDragging) {
|
|
821
|
+
document.addEventListener("mousemove", handleDocumentMouseMove);
|
|
822
|
+
document.addEventListener("mouseup", handleDocumentMouseUp);
|
|
823
|
+
document.addEventListener("touchmove", handleDocumentTouchMove);
|
|
824
|
+
document.addEventListener("touchend", handleDocumentTouchEnd);
|
|
825
|
+
return () => {
|
|
826
|
+
document.removeEventListener("mousemove", handleDocumentMouseMove);
|
|
827
|
+
document.removeEventListener("mouseup", handleDocumentMouseUp);
|
|
828
|
+
document.removeEventListener("touchmove", handleDocumentTouchMove);
|
|
829
|
+
document.removeEventListener("touchend", handleDocumentTouchEnd);
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
}, [
|
|
833
|
+
isDragging,
|
|
834
|
+
handleDocumentMouseMove,
|
|
835
|
+
handleDocumentMouseUp,
|
|
836
|
+
handleDocumentTouchMove,
|
|
837
|
+
handleDocumentTouchEnd,
|
|
838
|
+
]);
|
|
839
|
+
return {
|
|
840
|
+
isDragging,
|
|
841
|
+
handleMouseDown,
|
|
842
|
+
handleTouchStart,
|
|
843
|
+
hasDragged,
|
|
844
|
+
};
|
|
845
|
+
};
|
|
846
|
+
|
|
593
847
|
exports.API_BASE_URL = API_BASE_URL;
|
|
594
848
|
exports.API_ENDPOINTS = API_ENDPOINTS;
|
|
595
849
|
exports.BuildRender = BuildRender;
|
|
@@ -598,10 +852,14 @@ exports.InstructionTooltip = InstructionTooltip;
|
|
|
598
852
|
exports.LoadingErrorOverlay = LoadingErrorOverlay;
|
|
599
853
|
exports.arePartsEqual = arePartsEqual;
|
|
600
854
|
exports.buildApiUrl = buildApiUrl;
|
|
855
|
+
exports.calculateCircularFrame = calculateCircularFrame;
|
|
601
856
|
exports.calculateCircularTime = calculateCircularTime;
|
|
602
857
|
exports.getAvailableParts = getAvailableParts;
|
|
603
858
|
exports.renderBuildExperimental = renderBuildExperimental;
|
|
859
|
+
exports.renderSpriteExperimental = renderSpriteExperimental;
|
|
604
860
|
exports.useBouncePatternProgress = useBouncePatternProgress;
|
|
605
861
|
exports.useBuildRender = useBuildRender;
|
|
862
|
+
exports.useSpriteRender = useSpriteRender;
|
|
863
|
+
exports.useSpriteScrubbing = useSpriteScrubbing;
|
|
606
864
|
exports.useVideoScrubbing = useVideoScrubbing;
|
|
607
865
|
//# sourceMappingURL=index.js.map
|