Skip to content

Canvas[width][height] props -- aka "manual size" #3552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/API/canvas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
<div style="width:1280px; height:800px;">

<!-- <Canvas> -->
<div style="width:100%; height:100%;">
<div style="width:100%; height:100%;"> <!-- container-div -->
<canvas width="2560" height="1600" style="display:block; width:1280px; height:800px;"></canvas>
</div>
</div>

</div>
```

It:
0. gets the computed size of the container-div
1. sets the `<canvas>` width/height attributes accordingly to 0. and the dpr
2. sets the `<canvas>` CSS width/height accordingly to 0.

> [!NOTE]
> You can also override outer-div's `style` directly:
> ```html
> <!-- <Canvas style={{width:1280, height:800}}> -->
> <div style="width:1280px; height:800px;"> <!-- outer-div -->
> <div style="width:100%; height:100%;">
> <canvas width="2560" height="1600" style="display:block; width:1280px; height:800px;"></canvas>
> </div>
> </div>
> ```

<details>
<summary>Fixed size</summary>

It is also possible to fix `<canvas>`'s width/height attributes, independently from the container-div's size:

```html
<!-- <Canvas width={800} height={600} dpr={1}> -->
<div style="position:relative; width:100%; height:100%; overflow:hidden; pointer-events:auto;">
<div style="width:100%; height:100%;">
<canvas width="800" height="600" style="display:block; width:800px; height:600px;"></canvas>
</div>
</div>
```

> [!TIP]
> Using tailwindcss, you can override nested structure styles:
> ```tsx
> <Canvas
> 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" // <canvas>
> )}
> />
> ```

</details>

## 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:
Expand Down
14 changes: 12 additions & 2 deletions packages/fiber/src/web/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export interface CanvasProps
ref?: React.Ref<HTMLCanvasElement>
/** 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
Expand All @@ -37,6 +41,8 @@ function CanvasImpl({
ref,
children,
fallback,
width,
height,
resize,
style,
gl,
Expand Down Expand Up @@ -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<HTMLCanvasElement>(canvas)

async function run() {
Expand All @@ -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) => {
Expand Down
59 changes: 59 additions & 0 deletions packages/fiber/tests/canvas.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Canvas width={640} height={480}>
<group />
</Canvas>,
),
)

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(
<Canvas width={640}>
<group />
</Canvas>,
),
)

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(
<Canvas height={480}>
<group />
</Canvas>,
),
)

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(
<Canvas>
<group />
</Canvas>,
),
)

const canvas = renderer.container.querySelector('canvas')
// Should use mocked useMeasure dimensions (1280x800)
expect(canvas?.getAttribute('width')).toBe('1280')
expect(canvas?.getAttribute('height')).toBe('800')
})
})