diff --git a/docs/API/canvas.mdx b/docs/API/canvas.mdx index f986b0bd01..36749a978d 100644 --- a/docs/API/canvas.mdx +++ b/docs/API/canvas.mdx @@ -67,6 +67,70 @@ It will also create the following scene internals: In recent versions of threejs, `THREE.ColorManagement.enabled` will be set to `true` to enable automatic conversion of colors according to the renderer's configured color space. R3F will handle texture color space conversion. For more on this topic, see [https://threejs.org/docs/#manual/en/introduction/Color-management](https://threejs.org/docs/#manual/en/introduction/Color-management). +### Size + +By default, the Canvas will be responsive (it uses [`useMeasure`](https://www.npmjs.com/package/react-use-measure) internally). + +Eg: inside a 1280x800 container, on a dpr=2 device: + +```html +
+ + +
+
+ +
+
+ +
+``` + +It: +0. gets the computed size of the container-div +1. sets the `` width/height attributes accordingly to 0. and the dpr +2. sets the `` CSS width/height accordingly to 0. + +> [!NOTE] +> You can also override outer-div's `style` directly: +> ```html +> +>
+>
+> +>
+>
+> ``` + +
+Fixed size + +It is also possible to fix ``'s width/height attributes, independently from the container-div's size: + +```html + +
+
+ +
+
+``` + +> [!TIP] +> Using tailwindcss, you can override nested structure styles: +> ```tsx +> width={800} height={600} +>. className={cn( +> "!w-full !h-dvh" // outer-div +>. "[&>*]:!w-full [&>*]:!h-full" // container-div +> "[&>*>*]:!max-w-full [&>*>*]:!max-h-full [&>*>*]:object-contain" // +> )} +> /> +> ``` + +
+ ## Errors and fallbacks On some systems WebGL may not be supported, you can provide a fallback component that will be rendered instead of the canvas: diff --git a/packages/fiber/src/web/Canvas.tsx b/packages/fiber/src/web/Canvas.tsx index 1993c9d250..7411a1d3c0 100644 --- a/packages/fiber/src/web/Canvas.tsx +++ b/packages/fiber/src/web/Canvas.tsx @@ -22,6 +22,10 @@ export interface CanvasProps ref?: React.Ref /** Canvas fallback content, similar to img's alt prop */ fallback?: React.ReactNode + /** */ + width?: number + /** */ + height?: number /** * Options to pass to useMeasure. * @see https://github.com/pmndrs/react-use-measure#api @@ -37,6 +41,8 @@ function CanvasImpl({ ref, children, fallback, + width, + height, resize, style, gl, @@ -83,7 +89,11 @@ function CanvasImpl({ useIsomorphicLayoutEffect(() => { const canvas = canvasRef.current - if (containerRect.width > 0 && containerRect.height > 0 && canvas) { + + const manualSize = width && height ? { width, height, left: 0, top: 0 } : undefined + const size = manualSize ?? containerRect + + if (size.width > 0 && size.height > 0 && canvas) { if (!root.current) root.current = createRoot(canvas) async function run() { @@ -101,7 +111,7 @@ function CanvasImpl({ performance, raycaster, camera, - size: containerRect, + size, // Pass mutable reference to onPointerMissed so it's free to update onPointerMissed: (...args) => handlePointerMissed.current?.(...args), onCreated: (state) => { diff --git a/packages/fiber/tests/canvas.test.tsx b/packages/fiber/tests/canvas.test.tsx index ec15d6cfc5..3bc578b706 100644 --- a/packages/fiber/tests/canvas.test.tsx +++ b/packages/fiber/tests/canvas.test.tsx @@ -76,4 +76,63 @@ describe('web Canvas', () => { expect(useLayoutEffect).not.toHaveBeenCalled() }) + + it('should use manual width and height when provided', async () => { + const renderer = await act(async () => + render( + + + , + ), + ) + + const canvas = renderer.container.querySelector('canvas') + expect(canvas?.getAttribute('width')).toBe('640') + expect(canvas?.getAttribute('height')).toBe('480') + }) + + it('should fallback to useMeasure when only width is provided', async () => { + const renderer = await act(async () => + render( + + + , + ), + ) + + const canvas = renderer.container.querySelector('canvas') + // Should use mocked useMeasure dimensions (1280x800) + expect(canvas?.getAttribute('width')).toBe('1280') + expect(canvas?.getAttribute('height')).toBe('800') + }) + + it('should fallback to useMeasure when only height is provided', async () => { + const renderer = await act(async () => + render( + + + , + ), + ) + + const canvas = renderer.container.querySelector('canvas') + // Should use mocked useMeasure dimensions (1280x800) + expect(canvas?.getAttribute('width')).toBe('1280') + expect(canvas?.getAttribute('height')).toBe('800') + }) + + it('should fallback to useMeasure when neither width nor height is provided', async () => { + const renderer = await act(async () => + render( + + + , + ), + ) + + const canvas = renderer.container.querySelector('canvas') + // Should use mocked useMeasure dimensions (1280x800) + expect(canvas?.getAttribute('width')).toBe('1280') + expect(canvas?.getAttribute('height')).toBe('800') + }) })