I am using react and three.js to display some 3D models in the browser. Everything works fine as long as the three.js component is the only element with visible content on the page. The three.js canvas will resize bigger and smaller just fine. You can wrap it in other containers and it still works fine. The issue starts when another element or component is added as a sibling.
I want to have a viewport with a fixed width sidebar. I'm doing this with a flexbox row container, wrapped around a sidebar component (a simple div with min-width set), and the responsive three.js component.
Resizing the window bigger works fine, the canvas fills the browser window appropriately. However, when resizing the window smaller, the canvas does not properly re-calculate it's size in relation to the available space left over by the static width sidebar, resulting in a canvas that does not fully fit in the browser and introducing scrollbars.
The canvas does shrink some, but not enough to keep it fully in the browser window. The same issue occurs vertically if a component sits above or below the three.js component. If you recompile the app while the browser window is smaller, the refreshed view will have the canvas properly resized.
Following the advice of many of the answers here on StackOverflow, the three.js react code looks something like this:
export class Viewport extends React.Component {
componentDidMount() {
const width = this.mount.clientWidth;
const height = this.mount.clientHeight;
window.addEventListener("resize", this.handleWindowResize);
// setup scene
this.scene = new THREE.Scene();
//setup camera
this.camera = new THREE.PerspectiveCamera( 75, width / height, 0.1, 1000 );
this.camera.position.set( 0, 5, 10 );
// setup rendering
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setClearColor('#666666');
this.renderer.setSize(width, height, false);
this.mount.appendChild(this.renderer.domElement);
// setup geo
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: '#433F81' });
this.cube = new THREE.Mesh(geometry, material);
this.scene.add(this.cube);
...
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleWindowResize);
this.mount.removeChild(this.renderer.domElement);
}
handleWindowResize = () => {
const width = this.mount.clientWidth;
const height = this.mount.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height, false);
};
render() {
return (
<div className="viewport" style={{display: 'flex', width: "100%", height: "100%"}} ref={(mount) => { this.mount = mount }} />
);
}
}
The styling is simple css flexbox, with the elements set to full width and height (100%), with the sidebar having a set min-width.
.flex-row-container {
display: flex;
flex-flow: row;
height: 100%;
width: 100%;
}
.sidebar {
display: block;
min-width: 250px;
background-color: var(--color-mid-gray);
margin: 3px 4px 3px 4px;
box-sizing: border-box;
border: 1px solid var(--color-mid-gray);
outline: 3px solid var(--color-mid-gray);
}
What am I missing?
So after commenting, I think I got it working how you want.
Basically,
I removed setting display:flex
on the viewport, as this is a child of an element (the flex-row-container div) with display:flex
, it does not need it, but does need flex:1 1 auto
to set how it will display within the flexbox (which I did in the css rather than the JSX).
The problem then became that the viewport div would expand, but not contract (because it contains the canvas), so adding overflow:hidden
fixed that.
After making those changes, this.mount.clientWidth
would get the desired result that could then be used to set the three canvas size.
See This Fiddle.
I added rendering and animation to the example so the result could be easily seen.
Hope this helps.