About canvas resize error

Normally, the three.js project takes a canvas filled browser’s viewport, but if the canvas is a child element of one of the projects that uses a grid layout, the following problem arises.

The usual practice of resize is to change the canvas viewport by listening to the resize event of the window, but under the above conditions, the child element size will not change with the change of the viewport of the window. I experimented with flex layout and grid layout, and only grid layout happened, I guess because of some adaptive sequencing.

As you can see in the image below, the innerwidth of the window has changed, but the size of the parent element that carries the canvas has not changed


This is my code

As you can see in app.css, I annotated the flex layout version, which is fine, and I tested the ResizeObserver class to listen to the child element before finally using flex to resolve the issue, but it still didn’t trigger

Finally, to summarize my question,
1.What are the differences between grid and flex layouts for three.js
2.When it comes to adaptation, is there any order within three.js and grid, flex, resizeObserver
3.After using flex to solve the adaptive problem, I ran into a problem where the canvas was always 1px smaller than its container
In the figure below, although flex solves the adaptive problem, there is a 1px problem

As far as I understand, first you have to achieve the desired arrangement of elements on the page (without tripping over three). Next, use the child’s getBoundingClientRect to define the area for three and use the setViewport and setScissor commands for the renderer.
Here is my example

    for( const [mode, view] of Object.entries( app.views ) ) {
        view.show = ( view.canvas.style.display !== 'none' );
        if( view.show && view.enable ) {
            if( view.camera ) {
                const element = view.canvas;
                //element.style.zIndex = 20;
                const rect = element.getBoundingClientRect();
                if ( rect.bottom < 0 || rect.top > app.renderer.domElement.clientHeight ||
                    rect.right < 0 || rect.left > app.renderer.domElement.clientWidth ) {
                    return;
                }
                const margin = 1;
                const width = ( rect.right - rect.left ) - 2*margin;
                const height = ( rect.bottom - rect.top ) - 2*margin;
                const left = rect.left + margin;
                const bottom = app.renderer.domElement.clientHeight - rect.bottom + margin;
                view.bound = { width:width, height:height, left:left, bottom:bottom };

                app.renderer.setViewport( left, bottom, width, height );
                app.renderer.setScissor( left, bottom, width, height );
                view.camera.setViewOffset( height, height, ( height-width/2 )/2, height/4, width/2, height/2 );
                view.camera.updateProjectionMatrix();

                if( view.controls ) view.controls.update();
                app.renderer.render( view.scene, view.camera );
            }
        }
    }

Thanks for your code, I used getBoundingClientRect() to fix the 1px problem because clientWidth is missing the decimal point, but for the grid layout, changing the size of the window does not change the value of getBoundingClientRect(). The extra white space is reflected in the rendering, but not in the numerical value

You can treat emptiness as another element that fills it completely. Add an empty element as “flex grow” and see its borders. In fact, this topic is very interesting and it is unlikely that you will be able to avoid it without understanding the layout. But I would like to note that these are not three problems, but the need to organize canvas.

PS Moreover, further cross-browser problems may arise that can blow your mind forever

You are right, this is really not a problem with three.js, I put the grid layout problem here to see if anyone can explain this style logic

If possible, post the source codes. This way it will be easier for me to understand what the problem is.

I open the library you can see, originally wanted to use codesandbox, my network is not good seems to have failed,

https://codesandbox.io/p/github.com/ailuoamang/threejsPractice

:face_with_raised_eyebrow: What browsers did you check on? I don’t see such an error in chrome, firefox.

The version I submitted uses the flex layout, and the grid layout is annotated by me. Here’s what you can try.

You can simply check the canvas size every animation frame with canvas.getBoundingClientRect() and if changed, call renderer.setSize(width, height, false) - third parameter prevents applying inline css to the canvas element. And you can fully controls the canvas size with your css.

1 Like

Also It will not be superfluous to check devicePixelRatio as well, and if changed (for example, user changed the page zoom) call renderer.setPixelRatio(devicePixelRatio).

I used this example
https://threejs.org/examples/?q=multi#webgl_multiple_elements

You need to make a separate canvas for three and reshape it when changing the sceneContainer.

https://codesandbox.io/p/devbox/divine-cookies-dmmf5w

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
import React, { useEffect, useRef } from "react";
import * as Three from "three";
import { update } from "three/examples/jsm/libs/tween.module.js";

export default function Box() {
  const sceneRef = useRef(null);
  useEffect(() => {
    if (sceneRef.current === null) {
      console.log("执行一次");
      // dom节点
      const canvas = document.getElementById("canvas");
      const container = document.getElementById("sceneContainer");

      sceneRef.current = new Three.Scene();

      //相机
      const camera = new Three.PerspectiveCamera(75, 1, 0.1, 100);
      camera.position.z = 10;

      //立方体
      const geometry = new Three.BoxGeometry(1, 1, 1);
      const material = new Three.MeshBasicMaterial();
      const cube = new Three.Mesh(geometry, material);
      sceneRef.current.add(cube);

      //渲染器
      const renderer = new Three.WebGLRenderer({ canvas: canvas, antialias: true });
      renderer.setPixelRatio(window.devicePixelRatio);

      // container.append(renderer.domElement);

      //renderer.render(sceneRef.current, camera)

      function updateSize() {
        const width = canvas.clientWidth;
        const height = canvas.clientHeight;
        if (canvas.width !== width || canvas.height !== height) {
          renderer.setSize(width, height, false);
          console.log("canvas", width, height);
        }
      }

      const animate = () => {
        // console.log('帧渲染',container.clientWidth)
        cube.rotation.x += 0.01;
        cube.rotation.y += 0.01;
        //渲染器帧渲染场景和相机
        updateSize();

        canvas.style.transform = `translateY(${window.scrollY}px)`;
        // renderer.setScissorTest(false);
        // renderer.clear();
        // renderer.setScissorTest(true);

        const rect = container.getBoundingClientRect();
        if (
          rect.bottom < 0 ||
          rect.top > renderer.domElement.clientHeight ||
          rect.right < 0 ||
          rect.left > renderer.domElement.clientWidth
        ) {
          return;
        }

        const width = rect.right - rect.left;
        const height = rect.bottom - rect.top;
        const left = rect.left;
        const bottom = renderer.domElement.clientHeight - rect.bottom;
        renderer.setScissorTest( true );
        renderer.setViewport(left, bottom, width, height);
        renderer.setScissor(left, bottom, width, height);      
        camera.aspect = width / height;
        camera.updateProjectionMatrix();
        renderer.render(sceneRef.current, camera);
        console.log(left, bottom, width, height);
      };

      //动画循环
      renderer.setAnimationLoop(animate);

      window.addEventListener("resize", () => {
        return;
        //我这里与其他项目不同的是,渲染器并不是直接绑定在body元素上的,而是某个子元素
        //这里通过window的resize函数监听
        //发现会出现缩小,container的宽不随window变化,放大会变化,但是会闪回
        //个人判断是这些css自适应规则发生了冲突以及一些先后顺序
        //经过查询决定采用另一个可以监听任意元素变化的方法ResizeObserver,这个方法会导致three.js不渲染
        //three.js的官网解决方式是侧边栏使用了iframe,我没法在这里使用
        console.log("页面发生缩放", window.innerWidth, container.clientWidth);
        size.width = container.clientWidth;
        size.height = container.clientHeight;
        //更新相机宽高比
        camera.aspect = size.width / size.height;
        //更新相机的投影矩阵
        camera.updateProjectionMatrix();
        //更新渲染器尺寸
        renderer.setSize(size.width, size.height);
      });

      // const resizeObserver = new ResizeObserver(() => {
      //     const newWidth = container.clientWidth;
      //     const newHeight = container.clientHeight;
      //     console.log('容器大小变化', newWidth, newHeight);
      // });

      // resizeObserver.observe(container);

      // return () => {
      //     resizeObserver.disconnect();
      //     renderer.dispose();
      // };
    }
  }, []);
  return <div id='sceneContainer' style={{ height: "100%", width: "100%" }}></div>;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

#canvas {
  position: absolute;
  left: 0;
  width: 100%;
  height: 100%;
  /* z-index: 100; */
}

@ailuoamang Read this comment ^… since by default threejs changes the css/width/height of the canvas when you call resize. If you pass false as 3rd parameter to resize, then your canvas dom size is fully controlled by CSS.