@buildcores/render-client 1.4.0 → 1.5.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/dist/hooks/useContinuousSpin.d.ts +17 -0
- package/dist/hooks/useSpriteRender.d.ts +2 -0
- package/dist/index.d.ts +106 -1
- package/dist/index.esm.js +170 -47
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +170 -46
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +85 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -142,6 +142,57 @@ function useBouncePatternProgress(enabled = true) {
|
|
|
142
142
|
return { value, isBouncing };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Hook for continuous 360° spin animation with smooth interpolation support.
|
|
147
|
+
*
|
|
148
|
+
* @param enabled - Whether the animation is enabled
|
|
149
|
+
* @param duration - Duration in ms for one full rotation (default: 10000ms = 10 seconds)
|
|
150
|
+
* @param totalFrames - Total number of frames in the sprite (default: 72)
|
|
151
|
+
* @returns Object with current frame, next frame, and blend factor for smooth interpolation
|
|
152
|
+
*/
|
|
153
|
+
function useContinuousSpin(enabled = true, duration = 10000, totalFrames = 72) {
|
|
154
|
+
const [result, setResult] = react.useState({
|
|
155
|
+
frame: 0,
|
|
156
|
+
nextFrame: 1,
|
|
157
|
+
blend: 0
|
|
158
|
+
});
|
|
159
|
+
const startTime = react.useRef(null);
|
|
160
|
+
// Use refs to avoid stale closures in useAnimationFrame
|
|
161
|
+
const totalFramesRef = react.useRef(totalFrames);
|
|
162
|
+
const durationRef = react.useRef(duration);
|
|
163
|
+
// Keep refs in sync with props
|
|
164
|
+
react.useEffect(() => {
|
|
165
|
+
totalFramesRef.current = totalFrames;
|
|
166
|
+
}, [totalFrames]);
|
|
167
|
+
react.useEffect(() => {
|
|
168
|
+
durationRef.current = duration;
|
|
169
|
+
}, [duration]);
|
|
170
|
+
framerMotion.useAnimationFrame((time) => {
|
|
171
|
+
if (!enabled) {
|
|
172
|
+
if (startTime.current !== null) {
|
|
173
|
+
startTime.current = null;
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (startTime.current === null) {
|
|
178
|
+
startTime.current = time;
|
|
179
|
+
}
|
|
180
|
+
const elapsed = time - startTime.current;
|
|
181
|
+
const currentDuration = durationRef.current;
|
|
182
|
+
const currentTotalFrames = totalFramesRef.current;
|
|
183
|
+
// Calculate progress through the full rotation (0 to 1)
|
|
184
|
+
const progress = (elapsed % currentDuration) / currentDuration;
|
|
185
|
+
// Convert to fractional frame position
|
|
186
|
+
const exactFrame = progress * currentTotalFrames;
|
|
187
|
+
const currentFrame = Math.floor(exactFrame) % currentTotalFrames;
|
|
188
|
+
const nextFrame = (currentFrame + 1) % currentTotalFrames;
|
|
189
|
+
// Blend is the fractional part - how far between current and next frame
|
|
190
|
+
const blend = exactFrame - Math.floor(exactFrame);
|
|
191
|
+
setResult({ frame: currentFrame, nextFrame, blend });
|
|
192
|
+
});
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
145
196
|
// API Configuration
|
|
146
197
|
const API_BASE_URL = "https://www.renderapi.buildcores.com";
|
|
147
198
|
// API Endpoints
|
|
@@ -217,6 +268,8 @@ const createRenderBuildJob = async (request, config) => {
|
|
|
217
268
|
...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
|
|
218
269
|
...(request.cameraOffsetX !== undefined ? { cameraOffsetX: request.cameraOffsetX } : {}),
|
|
219
270
|
...(request.gridSettings ? { gridSettings: request.gridSettings } : {}),
|
|
271
|
+
// Include frame quality for sprite rendering
|
|
272
|
+
...(request.frameQuality ? { frameQuality: request.frameQuality } : {}),
|
|
220
273
|
};
|
|
221
274
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
|
|
222
275
|
method: "POST",
|
|
@@ -412,6 +465,7 @@ const createRenderByShareCodeJob = async (shareCode, config, options) => {
|
|
|
412
465
|
...(options?.showGrid !== undefined ? { showGrid: options.showGrid } : {}),
|
|
413
466
|
...(options?.cameraOffsetX !== undefined ? { cameraOffsetX: options.cameraOffsetX } : {}),
|
|
414
467
|
...(options?.gridSettings ? { gridSettings: options.gridSettings } : {}),
|
|
468
|
+
...(options?.frameQuality ? { frameQuality: options.frameQuality } : {}),
|
|
415
469
|
};
|
|
416
470
|
const response = await fetch(url, {
|
|
417
471
|
method: "POST",
|
|
@@ -639,28 +693,41 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
639
693
|
profile: currentInput.profile,
|
|
640
694
|
showGrid: currentInput.showGrid,
|
|
641
695
|
cameraOffsetX: currentInput.cameraOffsetX,
|
|
642
|
-
gridSettings: currentInput.gridSettings
|
|
696
|
+
gridSettings: currentInput.gridSettings,
|
|
697
|
+
frameQuality: currentInput.frameQuality
|
|
643
698
|
});
|
|
699
|
+
// Set metadata BEFORE sprite URL to avoid race condition
|
|
700
|
+
// (image load starts immediately when spriteSrc changes)
|
|
701
|
+
const rows = currentInput.frameQuality === 'high' ? 12 : 6;
|
|
702
|
+
setSpriteMetadata({ cols: 12, rows, totalFrames: 12 * rows });
|
|
644
703
|
setSpriteSrc((prevSrc) => {
|
|
645
704
|
if (prevSrc && prevSrc.startsWith("blob:")) {
|
|
646
705
|
URL.revokeObjectURL(prevSrc);
|
|
647
706
|
}
|
|
648
707
|
return spriteUrl;
|
|
649
708
|
});
|
|
650
|
-
setSpriteMetadata({ cols: 12, rows: 6, totalFrames: 72 });
|
|
651
709
|
return;
|
|
652
710
|
}
|
|
653
711
|
// Handle parts-based rendering (creates new build)
|
|
654
712
|
const currentParts = currentInput.parts;
|
|
655
713
|
const mode = options?.mode ?? "async";
|
|
714
|
+
const frameQuality = currentInput.frameQuality;
|
|
715
|
+
const rows = frameQuality === 'high' ? 12 : 6;
|
|
656
716
|
if (mode === "experimental") {
|
|
657
717
|
const response = await renderSpriteExperimental({
|
|
658
718
|
...currentParts,
|
|
659
719
|
showGrid: currentInput.showGrid,
|
|
660
720
|
cameraOffsetX: currentInput.cameraOffsetX,
|
|
661
721
|
gridSettings: currentInput.gridSettings,
|
|
722
|
+
frameQuality,
|
|
662
723
|
}, apiConfig);
|
|
663
724
|
const objectUrl = URL.createObjectURL(response.sprite);
|
|
725
|
+
// Set sprite metadata BEFORE sprite URL to avoid race condition
|
|
726
|
+
setSpriteMetadata({
|
|
727
|
+
cols: response.metadata?.cols || 12,
|
|
728
|
+
rows: response.metadata?.rows || rows,
|
|
729
|
+
totalFrames: response.metadata?.totalFrames || 12 * rows,
|
|
730
|
+
});
|
|
664
731
|
// Clean up previous sprite URL before setting new one
|
|
665
732
|
setSpriteSrc((prevSrc) => {
|
|
666
733
|
if (prevSrc && prevSrc.startsWith("blob:")) {
|
|
@@ -668,12 +735,6 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
668
735
|
}
|
|
669
736
|
return objectUrl;
|
|
670
737
|
});
|
|
671
|
-
// Set sprite metadata
|
|
672
|
-
setSpriteMetadata({
|
|
673
|
-
cols: response.metadata?.cols || 12,
|
|
674
|
-
rows: response.metadata?.rows || 6,
|
|
675
|
-
totalFrames: response.metadata?.totalFrames || 72,
|
|
676
|
-
});
|
|
677
738
|
}
|
|
678
739
|
else {
|
|
679
740
|
// Async job-based flow: request sprite format and use returned URL
|
|
@@ -683,15 +744,16 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
683
744
|
showGrid: currentInput.showGrid,
|
|
684
745
|
cameraOffsetX: currentInput.cameraOffsetX,
|
|
685
746
|
gridSettings: currentInput.gridSettings,
|
|
747
|
+
frameQuality,
|
|
686
748
|
}, apiConfig);
|
|
749
|
+
// Set metadata BEFORE sprite URL to avoid race condition
|
|
750
|
+
setSpriteMetadata({ cols: 12, rows, totalFrames: 12 * rows });
|
|
687
751
|
setSpriteSrc((prevSrc) => {
|
|
688
752
|
if (prevSrc && prevSrc.startsWith("blob:")) {
|
|
689
753
|
URL.revokeObjectURL(prevSrc);
|
|
690
754
|
}
|
|
691
755
|
return spriteUrl;
|
|
692
756
|
});
|
|
693
|
-
// No metadata from async endpoint; keep defaults
|
|
694
|
-
setSpriteMetadata({ cols: 12, rows: 6, totalFrames: 72 });
|
|
695
757
|
}
|
|
696
758
|
}
|
|
697
759
|
catch (error) {
|
|
@@ -714,6 +776,7 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
714
776
|
a.profile === b.profile &&
|
|
715
777
|
a.showGrid === b.showGrid &&
|
|
716
778
|
a.cameraOffsetX === b.cameraOffsetX &&
|
|
779
|
+
a.frameQuality === b.frameQuality &&
|
|
717
780
|
gridSettingsEqual;
|
|
718
781
|
}
|
|
719
782
|
if (a.type === 'parts' && b.type === 'parts') {
|
|
@@ -722,6 +785,7 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
|
|
|
722
785
|
return arePartsEqual(a.parts, b.parts) &&
|
|
723
786
|
a.showGrid === b.showGrid &&
|
|
724
787
|
a.cameraOffsetX === b.cameraOffsetX &&
|
|
788
|
+
a.frameQuality === b.frameQuality &&
|
|
725
789
|
gridSettingsEqual;
|
|
726
790
|
}
|
|
727
791
|
return false;
|
|
@@ -897,7 +961,7 @@ const useZoomPan = ({ displayWidth, displayHeight, minScale = 1, maxScale = 4, }
|
|
|
897
961
|
};
|
|
898
962
|
};
|
|
899
963
|
|
|
900
|
-
const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, cameraOffsetX, gridSettings, }) => {
|
|
964
|
+
const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, cameraOffsetX, gridSettings, animationMode = 'bounce', spinDuration = 10000, interactive = true, frameQuality, }) => {
|
|
901
965
|
const canvasRef = react.useRef(null);
|
|
902
966
|
const containerRef = react.useRef(null);
|
|
903
967
|
const [img, setImg] = react.useState(null);
|
|
@@ -915,6 +979,7 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
915
979
|
showGrid,
|
|
916
980
|
cameraOffsetX,
|
|
917
981
|
gridSettings,
|
|
982
|
+
frameQuality,
|
|
918
983
|
};
|
|
919
984
|
}
|
|
920
985
|
return {
|
|
@@ -923,12 +988,15 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
923
988
|
showGrid,
|
|
924
989
|
cameraOffsetX,
|
|
925
990
|
gridSettings,
|
|
991
|
+
frameQuality,
|
|
926
992
|
};
|
|
927
|
-
}, [shareCode, parts, showGrid, cameraOffsetX, gridSettings]);
|
|
993
|
+
}, [shareCode, parts, showGrid, cameraOffsetX, gridSettings, frameQuality]);
|
|
928
994
|
// Use custom hook for sprite rendering
|
|
929
995
|
const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(renderInput, apiConfig, undefined, useSpriteRenderOptions);
|
|
930
|
-
const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed);
|
|
931
996
|
const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
|
|
997
|
+
// Animation hooks - only one will be active based on animationMode
|
|
998
|
+
const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed && animationMode === 'bounce');
|
|
999
|
+
const spinResult = useContinuousSpin(bouncingAllowed && animationMode === 'spin360', spinDuration, total);
|
|
932
1000
|
const cols = spriteMetadata ? spriteMetadata.cols : 12;
|
|
933
1001
|
const rows = spriteMetadata ? spriteMetadata.rows : 6;
|
|
934
1002
|
const frameRef = react.useRef(0);
|
|
@@ -936,9 +1004,13 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
936
1004
|
displayWidth: displayW,
|
|
937
1005
|
displayHeight: displayH,
|
|
938
1006
|
});
|
|
939
|
-
// Image/frame sizes
|
|
1007
|
+
// Image/frame sizes - only calculate if image dimensions match expected metadata
|
|
1008
|
+
// This prevents using stale image with new metadata during transitions
|
|
940
1009
|
const frameW = img ? img.width / cols : 0;
|
|
941
1010
|
const frameH = img ? img.height / rows : 0;
|
|
1011
|
+
// Track expected rows to detect stale images
|
|
1012
|
+
const expectedRowsRef = react.useRef(rows);
|
|
1013
|
+
expectedRowsRef.current = rows;
|
|
942
1014
|
// ---- Load sprite image ----
|
|
943
1015
|
react.useEffect(() => {
|
|
944
1016
|
if (!spriteSrc) {
|
|
@@ -946,12 +1018,18 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
946
1018
|
setIsLoading(true);
|
|
947
1019
|
return;
|
|
948
1020
|
}
|
|
1021
|
+
// Clear current image immediately when source changes to prevent
|
|
1022
|
+
// using old image with new metadata
|
|
1023
|
+
setImg(null);
|
|
949
1024
|
setIsLoading(true);
|
|
1025
|
+
setBouncingAllowed(false);
|
|
950
1026
|
const i = new Image();
|
|
951
1027
|
i.decoding = "async";
|
|
952
1028
|
i.loading = "eager";
|
|
953
1029
|
i.src = spriteSrc;
|
|
954
1030
|
i.onload = () => {
|
|
1031
|
+
// Only set the image if rows haven't changed since we started loading
|
|
1032
|
+
// This prevents race conditions where metadata updates mid-load
|
|
955
1033
|
setImg(i);
|
|
956
1034
|
setIsLoading(false);
|
|
957
1035
|
// Start bouncing animation after delay
|
|
@@ -963,9 +1041,16 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
963
1041
|
setImg(null);
|
|
964
1042
|
setIsLoading(false);
|
|
965
1043
|
};
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1044
|
+
// Cleanup: if this effect re-runs (due to spriteSrc or rows change),
|
|
1045
|
+
// the new effect will clear the image
|
|
1046
|
+
return () => {
|
|
1047
|
+
// Abort loading if component updates before load completes
|
|
1048
|
+
i.onload = null;
|
|
1049
|
+
i.onerror = null;
|
|
1050
|
+
};
|
|
1051
|
+
}, [spriteSrc, rows]);
|
|
1052
|
+
// ---- Drawing function with optional cross-fade interpolation ----
|
|
1053
|
+
const draw = react.useCallback((frameIndex, nextFrameIndex, blend) => {
|
|
969
1054
|
const cnv = canvasRef.current;
|
|
970
1055
|
if (!cnv || !img || !frameW || !frameH)
|
|
971
1056
|
return;
|
|
@@ -980,26 +1065,54 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
980
1065
|
cnv.width = targetW;
|
|
981
1066
|
cnv.height = targetH;
|
|
982
1067
|
}
|
|
983
|
-
//
|
|
984
|
-
let n = Math.round(frameIndex) % total;
|
|
985
|
-
if (n < 0)
|
|
986
|
-
n += total;
|
|
987
|
-
const r = Math.floor(n / cols);
|
|
988
|
-
const c = n % cols;
|
|
989
|
-
// Use integer source rects to avoid sampling bleed across tiles
|
|
990
|
-
const sx = Math.round(c * frameW);
|
|
991
|
-
const sy = Math.round(r * frameH);
|
|
1068
|
+
// Calculate destination dimensions (letterboxing)
|
|
992
1069
|
const sw = Math.round(frameW);
|
|
993
1070
|
const sh = Math.round(frameH);
|
|
1071
|
+
const frameAspect = sw / sh;
|
|
1072
|
+
const canvasAspect = targetW / targetH;
|
|
1073
|
+
let drawW, drawH;
|
|
1074
|
+
if (frameAspect > canvasAspect) {
|
|
1075
|
+
drawW = targetW;
|
|
1076
|
+
drawH = targetW / frameAspect;
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
drawH = targetH;
|
|
1080
|
+
drawW = targetH * frameAspect;
|
|
1081
|
+
}
|
|
1082
|
+
// Apply zoom scale
|
|
1083
|
+
drawW *= scale;
|
|
1084
|
+
drawH *= scale;
|
|
1085
|
+
// Center in canvas
|
|
1086
|
+
const drawX = (targetW - drawW) / 2;
|
|
1087
|
+
const drawY = (targetH - drawH) / 2;
|
|
994
1088
|
ctx.clearRect(0, 0, targetW, targetH);
|
|
995
1089
|
ctx.imageSmoothingEnabled = true;
|
|
996
1090
|
ctx.imageSmoothingQuality = "high";
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1091
|
+
// Helper to draw a specific frame
|
|
1092
|
+
const drawFrame = (frameIdx, opacity) => {
|
|
1093
|
+
let n = Math.round(frameIdx) % total;
|
|
1094
|
+
if (n < 0)
|
|
1095
|
+
n += total;
|
|
1096
|
+
const r = Math.floor(n / cols);
|
|
1097
|
+
const c = n % cols;
|
|
1098
|
+
const sx = Math.round(c * frameW);
|
|
1099
|
+
const sy = Math.round(r * frameH);
|
|
1100
|
+
ctx.globalAlpha = opacity;
|
|
1101
|
+
ctx.drawImage(img, sx, sy, sw, sh, drawX, drawY, drawW, drawH);
|
|
1102
|
+
};
|
|
1103
|
+
// Cross-fade interpolation for smooth animation
|
|
1104
|
+
if (nextFrameIndex !== undefined && blend !== undefined && blend > 0.01) {
|
|
1105
|
+
// Draw current frame at full opacity first
|
|
1106
|
+
drawFrame(frameIndex, 1);
|
|
1107
|
+
// Then overlay next frame with blend opacity for smooth transition
|
|
1108
|
+
drawFrame(nextFrameIndex, blend);
|
|
1109
|
+
ctx.globalAlpha = 1; // Reset
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
// Single frame draw (no interpolation)
|
|
1113
|
+
drawFrame(frameIndex, 1);
|
|
1114
|
+
}
|
|
1115
|
+
}, [img, frameW, frameH, displayW, displayH, cols, rows, total, scale]);
|
|
1003
1116
|
const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useSpriteScrubbing(canvasRef, total, {
|
|
1004
1117
|
mouseSensitivity,
|
|
1005
1118
|
touchSensitivity,
|
|
@@ -1012,23 +1125,30 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
1012
1125
|
setIsLoading(true);
|
|
1013
1126
|
setBouncingAllowed(false);
|
|
1014
1127
|
}, []);
|
|
1015
|
-
// Auto-rotate when
|
|
1128
|
+
// Auto-rotate when animation is allowed and user hasn't manually dragged
|
|
1016
1129
|
react.useEffect(() => {
|
|
1017
|
-
if (hasDragged.current || !img)
|
|
1130
|
+
if ((interactive && hasDragged.current) || !img)
|
|
1018
1131
|
return;
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1132
|
+
if (animationMode === 'spin360') {
|
|
1133
|
+
// Continuous 360 spin with cross-fade interpolation for smoothness
|
|
1134
|
+
frameRef.current = spinResult.frame;
|
|
1135
|
+
draw(spinResult.frame, spinResult.nextFrame, spinResult.blend);
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
// Bounce animation (no interpolation needed - it pauses between movements)
|
|
1139
|
+
const frame = ((progressValue / 5) * total) % total;
|
|
1140
|
+
frameRef.current = frame;
|
|
1141
|
+
draw(frame);
|
|
1142
|
+
}
|
|
1143
|
+
}, [progressValue, spinResult, hasDragged, img, total, draw, animationMode, interactive]);
|
|
1024
1144
|
// Reset zoom when sprite changes or container size updates
|
|
1025
1145
|
react.useEffect(() => {
|
|
1026
1146
|
resetZoom();
|
|
1027
1147
|
}, [spriteSrc, displayW, displayH, resetZoom]);
|
|
1028
|
-
// Add native wheel event listener to prevent scrolling AND handle zoom
|
|
1148
|
+
// Add native wheel event listener to prevent scrolling AND handle zoom (only when interactive)
|
|
1029
1149
|
react.useEffect(() => {
|
|
1030
1150
|
const container = containerRef.current;
|
|
1031
|
-
if (!container)
|
|
1151
|
+
if (!container || !interactive)
|
|
1032
1152
|
return;
|
|
1033
1153
|
const handleNativeWheel = (event) => {
|
|
1034
1154
|
event.preventDefault();
|
|
@@ -1056,7 +1176,7 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
1056
1176
|
return () => {
|
|
1057
1177
|
container.removeEventListener('wheel', handleNativeWheel);
|
|
1058
1178
|
};
|
|
1059
|
-
}, [handleZoomWheel, scale, displayH]);
|
|
1179
|
+
}, [handleZoomWheel, scale, displayH, interactive]);
|
|
1060
1180
|
// Initial draw once image is ready or zoom changes
|
|
1061
1181
|
react.useEffect(() => {
|
|
1062
1182
|
if (img && !isLoading) {
|
|
@@ -1080,16 +1200,19 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
|
|
|
1080
1200
|
height: displayH,
|
|
1081
1201
|
backgroundColor: "black",
|
|
1082
1202
|
overflow: "hidden",
|
|
1083
|
-
}, children: [img && (jsxRuntime.jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onTouchStart: handleCanvasTouchStart, style: {
|
|
1203
|
+
}, children: [img && (jsxRuntime.jsx("canvas", { ref: canvasRef, onMouseDown: interactive ? handleMouseDown : undefined, onTouchStart: interactive ? handleCanvasTouchStart : undefined, style: {
|
|
1084
1204
|
width: displayW,
|
|
1085
1205
|
height: displayH,
|
|
1086
|
-
cursor: isDragging ? "grabbing" : "grab",
|
|
1087
|
-
touchAction: "none"
|
|
1206
|
+
cursor: interactive ? (isDragging ? "grabbing" : "grab") : "pointer",
|
|
1207
|
+
touchAction: interactive ? "none" : "auto",
|
|
1088
1208
|
display: "block",
|
|
1089
1209
|
userSelect: "none",
|
|
1090
1210
|
WebkitUserSelect: "none",
|
|
1091
1211
|
WebkitTouchCallout: "none",
|
|
1092
|
-
|
|
1212
|
+
pointerEvents: interactive ? "auto" : "none", // Allow click-through when not interactive
|
|
1213
|
+
}, role: "img", "aria-label": "360\u00B0 viewer", onContextMenu: interactive ? (e) => e.preventDefault() : undefined })), jsxRuntime.jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingSprite || !!renderError, renderError: renderError || undefined, size: Math.min(displayW, displayH) }), jsxRuntime.jsx(InstructionTooltip, { isVisible: interactive &&
|
|
1214
|
+
animationMode === 'bounce' &&
|
|
1215
|
+
!isLoading &&
|
|
1093
1216
|
!isRenderingSprite &&
|
|
1094
1217
|
!renderError &&
|
|
1095
1218
|
isBouncing &&
|
|
@@ -1270,6 +1393,7 @@ exports.renderByShareCode = renderByShareCode;
|
|
|
1270
1393
|
exports.renderSpriteExperimental = renderSpriteExperimental;
|
|
1271
1394
|
exports.useBouncePatternProgress = useBouncePatternProgress;
|
|
1272
1395
|
exports.useBuildRender = useBuildRender;
|
|
1396
|
+
exports.useContinuousSpin = useContinuousSpin;
|
|
1273
1397
|
exports.useSpriteRender = useSpriteRender;
|
|
1274
1398
|
exports.useSpriteScrubbing = useSpriteScrubbing;
|
|
1275
1399
|
exports.useVideoScrubbing = useVideoScrubbing;
|