【发布时间】:2015-07-05 05:48:01
【问题描述】:
如何根据容器计算画布大小?避免滚动。
如果我根据窗口设置大小,画布太大了。
【问题讨论】:
标签: javascript three.js
如何根据容器计算画布大小?避免滚动。
如果我根据窗口设置大小,画布太大了。
【问题讨论】:
标签: javascript three.js
嗯,这并不难。设置渲染的大小就可以了。
container = document.getElementById('container');
renderer.setSize($(container).width(), $(container).height());
container.appendChild(renderer.domElement);
【讨论】:
可以说是调整three.js 大小的最佳方式,用于对其进行编码,因此它只接受CSS 设置的画布大小。这样,无论您如何使用画布,您的代码都可以正常工作,无需针对不同情况进行更改。
首先在设置初始纵横比时没有理由设置它,因为我们要设置它以响应画布的大小不同,所以设置两次只是浪费代码
// There's no reason to set the aspect here because we're going
// to set it every frame anyway so we'll set it to 2 since 2
// is the the aspect for the canvas default size (300w/150h = 2)
const camera = new THREE.PerspectiveCamera(70, 2, 1, 1000);
然后我们需要一些代码来调整画布的大小以匹配其显示大小
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
// look up the size the canvas is being displayed
const width = canvas.clientWidth;
const height = canvas.clientHeight;
// adjust displayBuffer size to match
if (canvas.width !== width || canvas.height !== height) {
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// update any render target sizes here
}
}
在渲染之前在渲染循环中调用它
function animate(time) {
time *= 0.001; // seconds
resizeCanvasToDisplaySize();
mesh.rotation.x = time * 0.5;
mesh.rotation.y = time * 1;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
这里有 3 个示例,这三个示例之间的唯一区别是 CSS 以及是我们制作 canvas 还是 three.js 制作了 canvas
示例 1:全屏,我们制作画布
"use strict";
const renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});
// There's no reason to set the aspect here because we're going
// to set it every frame anyway so we'll set it to 2 since 2
// is the the aspect for the canvas default size (300w/150h = 2)
const camera = new THREE.PerspectiveCamera(70, 2, 1, 1000);
camera.position.z = 400;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(200, 200, 200);
const material = new THREE.MeshPhongMaterial({
color: 0x555555,
specular: 0xffffff,
shininess: 50,
shading: THREE.SmoothShading
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const light1 = new THREE.PointLight(0xff80C0, 2, 0);
light1.position.set(200, 100, 300);
scene.add(light1);
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (canvas.width !== width ||canvas.height !== height) {
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// set render target sizes here
}
}
function animate(time) {
time *= 0.001; // seconds
resizeCanvasToDisplaySize();
mesh.rotation.x = time * 0.5;
mesh.rotation.y = time * 1;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script>
示例2:全屏画布,three.js制作画布
"use strict";
const renderer = new THREE.WebGLRenderer();
document.body.appendChild(renderer.domElement);
// There's no reason to set the aspect here because we're going
// to set it every frame anyway so we'll set it to 2 since 2
// is the the aspect for the canvas default size (300w/150h = 2)
const camera = new THREE.PerspectiveCamera(70, 2, 1, 1000);
camera.position.z = 400;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(200, 200, 200);
const material = new THREE.MeshPhongMaterial({
color: 0x555555,
specular: 0xffffff,
shininess: 50,
shading: THREE.SmoothShading
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const light1 = new THREE.PointLight(0xff80C0, 2, 0);
light1.position.set(200, 100, 300);
scene.add(light1);
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (canvas.width !== width ||canvas.height !== height) {
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// set render target sizes here
}
}
function animate(time) {
time *= 0.001; // seconds
resizeCanvasToDisplaySize();
mesh.rotation.x = time * 0.5;
mesh.rotation.y = time * 1;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script>
示例 3:内联画布
"use strict";
const renderer = new THREE.WebGLRenderer({canvas: document.querySelector(".diagram canvas")});
// There's no reason to set the aspect here because we're going
// to set it every frame anyway so we'll set it to 2 since 2
// is the the aspect for the canvas default size (300w/150h = 2)
const camera = new THREE.PerspectiveCamera(70, 2, 1, 1000);
camera.position.z = 400;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(200, 200, 200);
const material = new THREE.MeshPhongMaterial({
color: 0x555555,
specular: 0xffffff,
shininess: 50,
shading: THREE.SmoothShading
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const light1 = new THREE.PointLight(0xff80C0, 2, 0);
light1.position.set(200, 100, 300);
scene.add(light1);
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (canvas.width !== width ||canvas.height !== height) {
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// set render target sizes here
}
}
function animate(time) {
time *= 0.001; // seconds
resizeCanvasToDisplaySize();
mesh.rotation.x = time * 0.5;
mesh.rotation.y = time * 1;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
body { font-size: x-large; }
.diagram { width: 150px; height: 150px; float: left; margin: 1em; }
canvas { width: 100%; height: 100%; }
<p>
Pretend this is a diagram in a physics lesson and it's inline. Notice we didn't have to change the code to handle this case.
<span class="diagram"><canvas></canvas></span>
The same code that handles fullscreen handles this case as well. The only difference is the CSS and how we look up the canvas. Otherwise it just works. We didn't have to change the code because we cooperated with the browser instead of fighting it.
</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script>
示例 4:50% 宽度的画布(如实时编辑器)
"use strict";
const renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});
// There's no reason to set the aspect here because we're going
// to set it every frame anyway so we'll set it to 2 since 2
// is the the aspect for the canvas default size (300w/150h = 2)
const camera = new THREE.PerspectiveCamera(70, 2, 1, 1000);
camera.position.z = 400;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(200, 200, 200);
const material = new THREE.MeshPhongMaterial({
color: 0x555555,
specular: 0xffffff,
shininess: 50,
shading: THREE.SmoothShading
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const light1 = new THREE.PointLight(0xff80C0, 2, 0);
light1.position.set(200, 100, 300);
scene.add(light1);
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (canvas.width !== width ||canvas.height !== height) {
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// set render target sizes here
}
}
function animate(time) {
time *= 0.001; // seconds
resizeCanvasToDisplaySize();
mesh.rotation.x = time * 0.5;
mesh.rotation.y = time * 1;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body { margin: 0; }
.outer {
}
.frame {
display: flex;
width: 100vw;
height: 100vh;
}
.frame>* {
flex: 1 1 50%;
}
#editor {
font-family: monospace;
padding: .5em;
background: #444;
color: white;
}
canvas {
width: 100%;
height: 100%;
}
<div class="frame">
<div id="result">
<canvas></canvas>
</div>
<div id="editor">
explaintion of example on left or the code for it would go here
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script>
注意window.innerWidth 和window.innerHeight 从未在上面的代码中引用,但它适用于所有情况。
更新:2021 年,没有“最佳”方式,只有多种方式需要权衡取舍。
有些人似乎关心性能,好像检查 2 个值是否不匹配 2 个其他值对任何需要性能的程序有任何可衡量的性能影响?
无论如何,我在上面发布的旧答案只是检查容器的大小。人们建议使用window.addEventListener('resize', ...),但没有注意到它不处理画布改变大小但窗口本身不改变的情况(上面的示例4)。
无论如何,对于那些认为每帧检查 2 个变量是性能开销的人,我们可以在 2021 年使用 ResizeObserver 而不是 window.addEventListener('resize', ...)。 ResizeObserver 将在画布改变大小时触发,即使窗口本身没有改变大小。
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
// look up the size the canvas is being displayed
const width = canvas.clientWidth;
const height = canvas.clientHeight;
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// update any render target sizes here
}
const resizeObserver = new ResizeObserver(resizeCanvasToDisplaySize);
resizeObserver.observe(canvas, {box: 'content-box'});
示例 1:全屏,我们制作画布
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/build/three.module.js';
const renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});
// There's no reason to set the aspect here because we're going
// to set it every frame anyway so we'll set it to 2 since 2
// is the the aspect for the canvas default size (300w/150h = 2)
const camera = new THREE.PerspectiveCamera(70, 2, 1, 1000);
camera.position.z = 400;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(200, 200, 200);
const material = new THREE.MeshPhongMaterial({
color: 0x555555,
specular: 0xffffff,
shininess: 50,
shading: THREE.SmoothShading
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const light1 = new THREE.PointLight(0xff80C0, 2, 0);
light1.position.set(200, 100, 300);
scene.add(light1);
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// set render target sizes here
}
function animate(time) {
time *= 0.001; // seconds
mesh.rotation.x = time * 0.5;
mesh.rotation.y = time * 1;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
const resizeObserver = new ResizeObserver(resizeCanvasToDisplaySize);
resizeObserver.observe(renderer.domElement, {box: 'content-box'});
</script>
示例 4:50% 宽度的画布(如实时编辑器)
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body { margin: 0; }
.outer {
}
.frame {
display: flex;
width: 100vw;
height: 100vh;
}
.frame>* {
flex: 1 1 50%;
}
#ui {
position: absolute;
right: 0;
bottom: 0;
padding: 0.5em;
}
#editor {
font-family: monospace;
padding: .5em;
background: #444;
color: white;
}
canvas {
width: 100%;
height: 100%;
}
<div class="frame">
<div id="result">
<canvas></canvas>
</div>
<div id="editor">
<p>no window resize</p>
<p>For example level editor might have the level
displayed on the left and a control panel on the right.</p>
<p>Click the buttons below. The only thing changing is CSS. The window size is not changing so using <code>window.addEventListener('resize', ...)</code> would fail</p>
<div id="ui">
<button type="button" data-size="25%">small</button>
<button type="button" data-size="50%">medium</button>
<button type="button" data-size="75%">large</button>
</div>
</div>
</div>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/build/three.module.js';
const renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});
// There's no reason to set the aspect here because we're going
// to set it every frame anyway so we'll set it to 2 since 2
// is the the aspect for the canvas default size (300w/150h = 2)
const camera = new THREE.PerspectiveCamera(70, 2, 1, 1000);
camera.position.z = 400;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(200, 200, 200);
const material = new THREE.MeshPhongMaterial({
color: 0x555555,
specular: 0xffffff,
shininess: 50,
shading: THREE.SmoothShading
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const light1 = new THREE.PointLight(0xff80C0, 2, 0);
light1.position.set(200, 100, 300);
scene.add(light1);
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// set render target sizes here
}
function animate(time) {
time *= 0.001; // seconds
mesh.rotation.x = time * 0.5;
mesh.rotation.y = time * 1;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
const editorElem = document.querySelector("#editor");
document.querySelectorAll('button').forEach(elem => {
elem.addEventListener('click', () => {
editorElem.style.flexBasis = elem.dataset.size;
});
});
const resizeObserver = new ResizeObserver(resizeCanvasToDisplaySize);
resizeObserver.observe(renderer.domElement, {box: 'content-box'});
</script>
为什么不使用ResizeObserver?
如果您仍然需要支持它,它在旧浏览器(如 IE)中不可用
不保证会尽快更新。
换句话说,使用 rAF 并检查每一帧,理想情况下,您的内容将始终保持正确的大小,因为浏览器要求您准备内容。相反,ResizeObserver 只是说它最终会给你一个事件。这可能会在您的内容实际需要调整大小后 5-10 帧出现。
更多的是处理 devicePixelRatio。对于这个答案来说,这个话题太大了,但你可以see more here
【讨论】:
renderer.setSize 并且最后不传递 false 时才应用样式。这可以说是与浏览器作斗争。 CSS 的全部意义在于让 CSS 选择大小。你的画布可以设置为 50%、50px、50em、50rem、50vw。它可以在 flexbox、表格、段落、网格框中。您应该使用 CSS 选择大小,查找它的大小。您不应该与浏览器对抗并通过 JavaScript 强制其大小
resizeCanvasToDisplaySize 是对性能的巨大浪费。最好和最安全的方法是在 window.addEventListener('resize', resizeCanvasToDisplaySize) 中运行它,这样它只会在布局更改而不是每一帧时渲染。
我认为调整画布大小的最佳方法不是上面公认的答案。 每个动画帧@gman 都会运行 resizeCanvasToDisplaySize 函数,进行多次计算。
我认为最好的方法是创建一个窗口事件列表器“调整大小”和一个布尔值,表示用户是否调整大小。在动画帧中,您可以检查用户是否调整了大小。如果他调整了大小,您可以调整动画帧中画布的大小。
let resized = false
// resize event listener
window.addEventListener('resize', function() {
resized = true
})
function animate(time) {
time *= 0.001
if (resized) resize()
// rotate the cube
cube.rotation.x += 0.01
cube.rotation.y += 0.01
// render the view
renderer.render(scene, camera)
// animate
requestAnimationFrame(animate)
}
function resize() {
resized = false
// update the size
renderer.setSize(window.innerWidth, window.innerHeight)
// update the camera
const canvas = renderer.domElement
camera.aspect = canvas.clientWidth/canvas.clientHeight
camera.updateProjectionMatrix()
}
【讨论】:
resized 变量。您可以在 addEventListener 中调用 resize 函数
也许我在这里漏掉了重点 - 但是为什么现有的建议涉及从动画循环中调整画布的大小?
您希望动画循环尽可能少地执行,因为理想情况下它每秒重复 30 次以上,并且应该进行优化以尽可能高效地运行 - 为运行它的最慢系统提供最大 fps .
我认为从 resize 事件监听器中调用 resize 函数并没有什么坏处——就像下面建议的 Meindert Stijfhals。
类似这样的:
var container = renderer.domElement.parentElement;
container.addEventListener('resize', onContainerResize);
function onContainerResize() {
var box = container.getBoundingClientRect();
renderer.setSize(box.width, box.height);
camera.aspect = box.width/box.height
camera.updateProjectionMatrix()
// optional animate/renderloop call put here for render-on-changes
}
如果您设置了某种只在更改时渲染,您可以在调整大小函数的末尾调用渲染函数。否则下次渲染循环触发时,它应该只使用新设置进行渲染。
【讨论】:
@gman 提供的答案当然是完整的答案。但是,要回顾可能的情况,必须假设两种可能性,这就是答案大相径庭的原因。
第一种可能是当容器是全局(头)对象时:window,或者可能是<body> 或<frameset>,在这种情况下,最方便的方法是使用window.addEventListener('resize', onResize()) { ... }。
第二种可能性,是最初提出的一种可能性:Three.js WebGL 输出的容器何时应该响应。在这种情况下,无论容器是画布、部分还是 div,处理调整大小的最佳方法是在 DOM 中定位容器,并通过将容器的 clientWidth 和 clientHeight 传递给它来使用 renderer.setsize() 方法,然后在渲染循环函数中调用onResize()函数,它的完整实现在@gman前面提到过。在这种特殊情况下,不需要使用addEventListener。
【讨论】:
下面的代码让我能够掌握画布渲染大小:
首先,场景在<div id="your_scene"> 内渲染,您可以选择尺寸:
<div class="relative h-512 w-512">
<div id="your_scene"></div>
</div>
CSS:
.relative {
position: relative;
}
h-512 {
height: 51.2rem;
}
w-512 {
width: 51.2rem;
}
#your_scene {
overflow: hidden;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
然后,你抓取它的尺寸:
this.mount = document.querySelector('#your_scene');
this.width = document.querySelector('#your_scene').offsetWidth;
this.height = document.querySelector('#your_scene').offsetHeight;
然后,您(在我的情况下)将渲染器背景设置为透明,设置像素比率,然后将容器的大小应用于渲染器。 Three.js 通过将 your_scene 容器与渲染器 <canvas> 元素附加来工作,这就是为什么这里的最后一步是 appendChild:
this.renderer = new THREE.WebGLRenderer({ alpha: true });
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(this.width, this.height);
this.mount.appendChild(this.renderer.domElement);
这应该足以让某人在他们的现场获得法医。工作最多的部分是setSize 调用,但您需要有正确的尺寸,此外,您的相机必须指向正确的位置。
如果您的代码与我的类似,但仍然无法正常工作,请查看您的相机视角并确保您的场景对象确实在视图中。
【讨论】:
我不确定从上面的 cmets 或 API 文档中是否对每个人都很明显,但以防万一其他人遇到我遇到的问题:请注意,渲染器的 setSize() 方法在设置画布的大小,而不是实际的设备像素。这意味着在高 DPI 屏幕上,它创建的实际像素数似乎基于width * window.devicePixelRatio 和height * window.devicePixelRatio。这很重要,例如,当您尝试执行我所做的事情时:根据来自 three.js 的帧捕获特定大小的 GIF。我最终做了以下事情:
camera.aspect = gifWidth / gifHeight;
camera.updateProjectionMatrix();
var scale = window.devicePixelRatio;
renderer.setSize( gifWidth / scale, gifHeight / scale, true );
这可能不是处理它的理想方法,但它对我有用。
【讨论】: