Tips

本文中我们总结了一些在使用three.js过程中可能会遇到的但又看起来不需要各自列出一章的小问题。


canvas截图

在浏览器中存在两种有效的方式进行截图。 旧的 canvas.toDataURL 与新的更好的 canvas.toBlob

所以你可能认为仅通过添加下列代码即可轻松实现截图功能

<canvas id="c"></canvas>
+<button id="screenshot" type="button">Save...</button>
const elem = document.querySelector('#screenshot');
elem.addEventListener('click', () => {
  canvas.toBlob((blob) => {
    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
  });
});

const saveBlob = (function() {
  const a = document.createElement('a');
  document.body.appendChild(a);
  a.style.display = 'none';
  return function saveData(blob, fileName) {
     const url = window.URL.createObjectURL(blob);
     a.href = url;
     a.download = fileName;
     a.click();
  };
}());

下面是来自介绍 响应式设计 并添加了上述代码与一些放置按钮的CSS的例子。

当我尝试截图得到了如下图片

是的,就是一张纯黑的图片而已。

取决于你的浏览器与系统的不同这个例子也有可能会正常生效,但是一般情况下这个例子是无法正常生效的。

这个问题的出现是因为基于性能和兼容性的考量,默认情况下浏览器会在绘制完成后清除WebGL canvas的缓存。

解决方案是在你捕获截图前调用一次渲染代码。

在我们的代码里我们只要进行小幅度调整即可。首先,分离出我们的渲染代码

+const state = {
+  time: 0,
+};

-function render(time) {
-  time *= 0.001;
+function render() {
  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }

  cubes.forEach((cube, ndx) => {
    const speed = 1 + ndx * .1;
-    const rot = time * speed;
+    const rot = state.time * speed;
    cube.rotation.x = rot;
    cube.rotation.y = rot;
  });

  renderer.render(scene, camera);

-  requestAnimationFrame(render);
}

+function animate(time) {
+  state.time = time * 0.001;
+
+  render();
+
+  requestAnimationFrame(animate);
+}
+requestAnimationFrame(animate);

现在 render 方法只与实际的渲染过程相关联了。我们可以在刚好要捕获canvas截图前调用它。

const elem = document.querySelector('#screenshot');
elem.addEventListener('click', () => {
+  render();
  canvas.toBlob((blob) => {
    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
  });
});

现在应该能正常生效了。

有关其他解决方案,请参阅下一项。


防止canvas被清空

如果你想要让用户使用动画对象进行绘图。你需要在创建 WebGLRenderer 的时候传入 preserveDrawingBuffer: true。这将阻止浏览器清理canvas。类似的,你也需要告诉three.js不要自动清理canvas。

const canvas = document.querySelector('#c');
-const renderer = new THREE.WebGLRenderer({canvas});
+const renderer = new THREE.WebGLRenderer({
+  canvas,
+  preserveDrawingBuffer: true,
+  alpha: true,
+});
+renderer.autoClearColor = false;

需要注意的是如果你确实需要制作一个画图程序的话这并不能解决你的问题,因为浏览器仍然会改变分辨率的时候随时有可能清空canvas。我们目前的方案是让canvas的分辨率跟随显示大小的改变。而canvas的显示大小也在随着窗口大小变化。这包括了即便用户在另一个标签页中下载了一个文件,浏览器添加了一个状态栏的情况。也包括了用户转动手机时浏览器从纵向切换至横向布局的情况

如果你切实需要制作一个绘图的程序,你可以 使用渲染目标的方式渲染到纹理上


获取键盘输入

在这些教程中,我们通常会将事件监听器绑定到canvas上 canvas。 虽然许多事件都能生效,但是默认情况下键盘事件不会正常响应。

为了获取键盘事件,我们将canvas的 tabindex 属性设置为0或更高。如下。

<canvas tabindex="0"></canvas>

这将导致一个新的问题,任何设置了 tabindex 的元素会在聚焦的时候突出显示。为了解决这个问题,我们在CSS中将它focus状态下的outline属性设置为none

canvas:focus {
  outline:none;
}

这里为了演示使用了3个canvas

<canvas id="c1"></canvas>
<canvas id="c2" tabindex="0"></canvas>
<canvas id="c3" tabindex="1"></canvas>

并且只为最后一个canvas设置css

#c3:focus {
    outline: none;
}

让我们用同样的事件监听器分别与它们相关联

document.querySelectorAll('canvas').forEach((canvas) => {
  const ctx = canvas.getContext('2d');

  function draw(str) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(str, canvas.width / 2, canvas.height / 2);
  }
  draw(canvas.id);

  canvas.addEventListener('focus', () => {
    draw('has focus press a key');
  });

  canvas.addEventListener('blur', () => {
    draw('lost focus');
  });

  canvas.addEventListener('keydown', (e) => {
    draw(`keyCode: ${e.keyCode}`);
  });
});

请注意,你无法让第一个canvas接收到键盘输入。第二个canvas虽然能接收到输入但是被突出显示了。第三个canvas同时解决了这这两个问题。


透明化canvas

默认情况下THREE.js让canvas显示为不透明。如果你需要让canvas变得透明可以在创建 WebGLRenderer 的时候传入 alpha:true

const canvas = document.querySelector('#c');
-const renderer = new THREE.WebGLRenderer({canvas});
+const renderer = new THREE.WebGLRenderer({
+  canvas,
+  alpha: true,
+});

你可能还想告诉它你的结果 使用 premultiplied alpha

const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({
  canvas,
  alpha: true,
+  premultipliedAlpha: false,
});

Three.js 使用 premultipliedAlpha: true 作为canvas的缺省值,但使用 premultipliedAlpha: false 作为材质的缺省值。

如果你想要更好的理解premultiplied alpha的使用与否,这里有一篇关于这个问题的好文章

不管怎样,让我们用透明canvas来设置一个简单的例子。

我们将上述配置应用到来自关于响应式设计的文章里的例子。让我们也将材质变得更透明。

function makeInstance(geometry, color, x) {
-  const material = new THREE.MeshPhongMaterial({color});
+  const material = new THREE.MeshPhongMaterial({
+    color,
+    opacity: 0.5,
+  });

...

并且添加一些HTML内容

<body>
  <canvas id="c"></canvas>
+  <div id="content">
+    <div>
+      <h1>Cubes-R-Us!</h1>
+      <p>We make the best cubes!</p>
+    </div>
+  </div>
</body>

还有一些将画布放置到前面的CSS

body {
    margin: 0;
}
#c {
    width: 100%;
    height: 100%;
    display: block;
+    position: fixed;
+    left: 0;
+    top: 0;
+    z-index: 2;
+    pointer-events: none;
}
+#content {
+  font-size: 7vw;
+  font-family: sans-serif;
+  text-align: center;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}

注意 pointer-events: none 使得canvas不响应鼠标与触摸事件,以至于你能够选中下面的文字。


使用three.js动画作为背景

一个常见的问题是如何使用three.js动画作为网站的背景。

这有两种显而易见的方法。

  • 将canvas的CSS position 属性如下设置为 fixed
#c {
 position: fixed;
 left: 0;
 top: 0;
 ...
}

你可简单的在上一个的例子里使用这个解决方案。只需要将 z-index 设为 -1 就可以看到立方体们显示到文字后面。

这个解决方案存在一个小缺点,那就是你的Javascript必须集成在页面中。而且如果你的页面实现很复杂的话,你需要保证页面里的three.js可视化代码不与实现其他功能的代码相冲突。

  • 使用 iframe

这种解决方案被应用在了 本站首页.

在你的网页种只需要插入一个iframe,像这样

<iframe id="background" src="responsive.html">
<div>
  Your content goes here.
</div>

然后修改样式使其填满窗口,并且处于背景中。这几乎和我们之前用到的canvas样式代码一样。只不过因为iframe存在默认边框,我们需要额外将 border 设为 none

#background {
    position: fixed;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    z-index: -1;
    border: none;
    pointer-events: none;
}