@buildcores/render-client 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ import { type TouchEvent as ReactTouchEvent, type WheelEvent as ReactWheelEvent } from "react";
2
+ interface UseZoomOptions {
3
+ displayWidth?: number;
4
+ displayHeight?: number;
5
+ minScale?: number;
6
+ maxScale?: number;
7
+ }
8
+ interface UseZoomReturn {
9
+ scale: number;
10
+ isPinching: boolean;
11
+ handleWheel: (event: ReactWheelEvent<Element>) => void;
12
+ handleTouchStart: (event: ReactTouchEvent<HTMLCanvasElement>) => boolean;
13
+ reset: () => void;
14
+ }
15
+ export declare const useZoomPan: ({ displayWidth, displayHeight, minScale, maxScale, }?: UseZoomOptions) => UseZoomReturn;
16
+ export {};
package/dist/index.d.ts CHANGED
@@ -368,6 +368,30 @@ interface RenderBuildRequest {
368
368
  * ```
369
369
  */
370
370
  height?: number;
371
+ /**
372
+ * Render quality profile that controls visual effects and rendering speed.
373
+ *
374
+ * - **cinematic**: All effects enabled (shadows, ambient occlusion, bloom) for highest quality
375
+ * - **flat**: No effects for clean, simple product shots
376
+ * - **fast**: Minimal rendering for fastest processing speed
377
+ *
378
+ * @example
379
+ * ```tsx
380
+ * const request: RenderBuildRequest = {
381
+ * parts: { CPU: ["7xjqsomhr"] },
382
+ * profile: 'cinematic' // High quality with all effects
383
+ * };
384
+ * ```
385
+ *
386
+ * @example Fast rendering
387
+ * ```tsx
388
+ * const request: RenderBuildRequest = {
389
+ * parts: { CPU: ["7xjqsomhr"] },
390
+ * profile: 'fast' // Quick render, minimal effects
391
+ * };
392
+ * ```
393
+ */
394
+ profile?: 'cinematic' | 'flat' | 'fast';
371
395
  }
372
396
  /**
373
397
  * Response structure containing all available parts for each category.
package/dist/index.esm.js CHANGED
@@ -177,6 +177,8 @@ const renderBuildExperimental = async (request, config) => {
177
177
  // Include width and height if provided
178
178
  ...(request.width !== undefined ? { width: request.width } : {}),
179
179
  ...(request.height !== undefined ? { height: request.height } : {}),
180
+ // Include profile if provided
181
+ ...(request.profile ? { profile: request.profile } : {}),
180
182
  };
181
183
  const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
182
184
  method: "POST",
@@ -204,6 +206,8 @@ const createRenderBuildJob = async (request, config) => {
204
206
  // Include width and height if provided
205
207
  ...(request.width !== undefined ? { width: request.width } : {}),
206
208
  ...(request.height !== undefined ? { height: request.height } : {}),
209
+ // Include profile if provided
210
+ ...(request.profile ? { profile: request.profile } : {}),
207
211
  };
208
212
  const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
209
213
  method: "POST",
@@ -267,6 +271,8 @@ const renderSpriteExperimental = async (request, config) => {
267
271
  // Include width and height if provided
268
272
  ...(request.width !== undefined ? { width: request.width } : {}),
269
273
  ...(request.height !== undefined ? { height: request.height } : {}),
274
+ // Include profile if provided
275
+ ...(request.profile ? { profile: request.profile } : {}),
270
276
  };
271
277
  const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
272
278
  method: "POST",
@@ -568,8 +574,101 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
568
574
  } })) }));
569
575
  };
570
576
 
577
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
578
+ const getTouchDistance = (touches) => {
579
+ const first = touches[0];
580
+ const second = touches[1];
581
+ if (!first || !second)
582
+ return 0;
583
+ return Math.hypot(second.clientX - first.clientX, second.clientY - first.clientY);
584
+ };
585
+ const useZoomPan = ({ displayWidth, displayHeight, minScale = 1, maxScale = 4, } = {}) => {
586
+ const [scale, setScale] = useState(1);
587
+ const [isPinching, setIsPinching] = useState(false);
588
+ const scaleRef = useRef(1);
589
+ const pinchDataRef = useRef({
590
+ initialDistance: 0,
591
+ initialScale: 1,
592
+ });
593
+ const setScaleSafe = useCallback((next) => {
594
+ const clamped = clamp(next, minScale, maxScale);
595
+ if (clamped === scaleRef.current)
596
+ return;
597
+ scaleRef.current = clamped;
598
+ setScale(clamped);
599
+ }, [minScale, maxScale]);
600
+ const handleWheel = useCallback((event) => {
601
+ event.preventDefault();
602
+ event.stopPropagation();
603
+ const deltaY = event.deltaMode === 1
604
+ ? event.deltaY * 16
605
+ : event.deltaMode === 2
606
+ ? event.deltaY * (displayHeight ?? 300)
607
+ : event.deltaY;
608
+ const zoomFactor = Math.exp(-deltaY * 0.0015);
609
+ const nextScale = scaleRef.current * zoomFactor;
610
+ setScaleSafe(nextScale);
611
+ }, [setScaleSafe, displayHeight]);
612
+ const handleTouchStart = useCallback((event) => {
613
+ if (event.touches.length < 2) {
614
+ return false;
615
+ }
616
+ const distance = getTouchDistance(event.touches);
617
+ if (!distance) {
618
+ return false;
619
+ }
620
+ pinchDataRef.current = {
621
+ initialDistance: distance,
622
+ initialScale: scaleRef.current,
623
+ };
624
+ setIsPinching(true);
625
+ event.preventDefault();
626
+ return true;
627
+ }, []);
628
+ useEffect(() => {
629
+ if (!isPinching)
630
+ return;
631
+ const handleMove = (event) => {
632
+ if (event.touches.length < 2)
633
+ return;
634
+ const distance = getTouchDistance(event.touches);
635
+ if (!distance || pinchDataRef.current.initialDistance === 0)
636
+ return;
637
+ const scaleFactor = distance / pinchDataRef.current.initialDistance;
638
+ const nextScale = pinchDataRef.current.initialScale * scaleFactor;
639
+ setScaleSafe(nextScale);
640
+ event.preventDefault();
641
+ };
642
+ const handleEnd = (event) => {
643
+ if (event.touches.length < 2) {
644
+ setIsPinching(false);
645
+ }
646
+ };
647
+ window.addEventListener("touchmove", handleMove, { passive: false });
648
+ window.addEventListener("touchend", handleEnd);
649
+ window.addEventListener("touchcancel", handleEnd);
650
+ return () => {
651
+ window.removeEventListener("touchmove", handleMove);
652
+ window.removeEventListener("touchend", handleEnd);
653
+ window.removeEventListener("touchcancel", handleEnd);
654
+ };
655
+ }, [isPinching, setScaleSafe]);
656
+ const reset = useCallback(() => {
657
+ scaleRef.current = 1;
658
+ setScale(1);
659
+ }, []);
660
+ return {
661
+ scale,
662
+ isPinching,
663
+ handleWheel,
664
+ handleTouchStart,
665
+ reset,
666
+ };
667
+ };
668
+
571
669
  const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
572
670
  const canvasRef = useRef(null);
671
+ const containerRef = useRef(null);
573
672
  const [img, setImg] = useState(null);
574
673
  const [isLoading, setIsLoading] = useState(true);
575
674
  const [bouncingAllowed, setBouncingAllowed] = useState(false);
@@ -582,6 +681,10 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
582
681
  const cols = spriteMetadata ? spriteMetadata.cols : 12;
583
682
  const rows = spriteMetadata ? spriteMetadata.rows : 6;
584
683
  const frameRef = useRef(0);
684
+ const { scale, handleWheel: handleZoomWheel, handleTouchStart: handleZoomTouchStart, reset: resetZoom, } = useZoomPan({
685
+ displayWidth: displayW,
686
+ displayHeight: displayH,
687
+ });
585
688
  // Image/frame sizes
586
689
  const frameW = img ? img.width / cols : 0;
587
690
  const frameH = img ? img.height / rows : 0;
@@ -640,8 +743,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
640
743
  ctx.clearRect(0, 0, targetW, targetH);
641
744
  ctx.imageSmoothingEnabled = true;
642
745
  ctx.imageSmoothingQuality = "high";
643
- ctx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
644
- }, [img, frameW, frameH, displayW, displayH, cols, total]);
746
+ const scaledW = targetW * scale;
747
+ const scaledH = targetH * scale;
748
+ const offsetX = -((scaledW - targetW) / 2);
749
+ const offsetY = -((scaledH - targetH) / 2);
750
+ ctx.drawImage(img, sx, sy, sw, sh, offsetX, offsetY, scaledW, scaledH);
751
+ }, [img, frameW, frameH, displayW, displayH, cols, total, scale]);
645
752
  const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useSpriteScrubbing(canvasRef, total, {
646
753
  mouseSensitivity,
647
754
  touchSensitivity,
@@ -663,18 +770,66 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
663
770
  frameRef.current = frame;
664
771
  draw(frame);
665
772
  }, [progressValue, hasDragged, img, total, draw]);
666
- // Initial draw once image is ready
773
+ // Reset zoom when sprite changes or container size updates
774
+ useEffect(() => {
775
+ resetZoom();
776
+ }, [spriteSrc, displayW, displayH, resetZoom]);
777
+ // Add native wheel event listener to prevent scrolling AND handle zoom
778
+ useEffect(() => {
779
+ const container = containerRef.current;
780
+ if (!container)
781
+ return;
782
+ const handleNativeWheel = (event) => {
783
+ event.preventDefault();
784
+ event.stopPropagation();
785
+ // Manually trigger zoom since we're preventing the React event
786
+ event.deltaMode === 1
787
+ ? event.deltaY * 16
788
+ : event.deltaMode === 2
789
+ ? event.deltaY * (displayH ?? 300)
790
+ : event.deltaY;
791
+ // We need to call the zoom handler directly
792
+ // Create a synthetic React event-like object
793
+ const syntheticEvent = {
794
+ preventDefault: () => { },
795
+ stopPropagation: () => { },
796
+ deltaY: event.deltaY,
797
+ deltaMode: event.deltaMode,
798
+ currentTarget: container,
799
+ };
800
+ handleZoomWheel(syntheticEvent);
801
+ hasDragged.current = true;
802
+ };
803
+ // Add listener to container to catch all wheel events
804
+ container.addEventListener('wheel', handleNativeWheel, { passive: false });
805
+ return () => {
806
+ container.removeEventListener('wheel', handleNativeWheel);
807
+ };
808
+ }, [handleZoomWheel, scale, displayH]);
809
+ // Initial draw once image is ready or zoom changes
667
810
  useEffect(() => {
668
811
  if (img && !isLoading) {
669
812
  draw(frameRef.current);
670
813
  }
671
814
  }, [img, isLoading, draw]);
672
- return (jsxs("div", { style: {
815
+ const handleCanvasTouchStart = useCallback((event) => {
816
+ if (handleZoomTouchStart(event)) {
817
+ hasDragged.current = true;
818
+ return;
819
+ }
820
+ handleTouchStart(event);
821
+ }, [handleZoomTouchStart, handleTouchStart, hasDragged]);
822
+ useCallback((event) => {
823
+ hasDragged.current = true;
824
+ handleZoomWheel(event);
825
+ }, [handleZoomWheel, hasDragged]);
826
+ return (jsxs("div", { ref: containerRef, style: {
673
827
  position: "relative",
674
828
  width: displayW,
675
829
  height: displayH,
676
830
  backgroundColor: "black",
677
- }, children: [img && (jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, style: {
831
+ overflow: "hidden",
832
+ }, children: [img && (jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onTouchStart: handleCanvasTouchStart, style: {
678
833
  width: displayW,
679
834
  height: displayH,
680
835
  cursor: isDragging ? "grabbing" : "grab",