|
| 1 | +Title: WebGL2 小贴士 |
| 2 | +Description: 使用 WebGL 时可能遇到的一些小问题 |
| 3 | +TOC: 画布截屏 |
| 4 | + |
| 5 | +本文收集了一些你在使用 WebGL 时可能遇到的、看起来太小而不值得单独写一篇文章的问题。 |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +<a id="screenshot" data-toc="Taking a screenshot"></a> |
| 10 | + |
| 11 | +# 对 Canvas 截图 |
| 12 | + |
| 13 | +在浏览器中,实际上有两种函数可以对画布进行截图。 |
| 14 | +一种旧方法是: |
| 15 | +[`canvas.toDataURL`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL) |
| 16 | +另一种新的更好的方法是: |
| 17 | +[`canvas.toBlob`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) |
| 18 | + |
| 19 | +所以你可能会认为,只需添加如下代码就能轻松截图: |
| 20 | + |
| 21 | +```html |
| 22 | +<canvas id="c"></canvas> |
| 23 | ++<button id="screenshot" type="button">Save...</button> |
| 24 | +``` |
| 25 | + |
| 26 | +```js |
| 27 | +const elem = document.querySelector('#screenshot'); |
| 28 | +elem.addEventListener('click', () => { |
| 29 | + canvas.toBlob((blob) => { |
| 30 | + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); |
| 31 | + }); |
| 32 | +}); |
| 33 | + |
| 34 | +const saveBlob = (function() { |
| 35 | + const a = document.createElement('a'); |
| 36 | + document.body.appendChild(a); |
| 37 | + a.style.display = 'none'; |
| 38 | + return function saveData(blob, fileName) { |
| 39 | + const url = window.URL.createObjectURL(blob); |
| 40 | + a.href = url; |
| 41 | + a.download = fileName; |
| 42 | + a.click(); |
| 43 | + }; |
| 44 | +}()); |
| 45 | +``` |
| 46 | + |
| 47 | +这是来自[动画那篇文章](webgl-animation.html)的示例,在其中加入了上面的代码,并添加了一些 CSS 来放置按钮。 |
| 48 | + |
| 49 | +{{{example url="../webgl-tips-screenshot-bad.html"}}} |
| 50 | + |
| 51 | +当我尝试时,我得到了这样的截图。 |
| 52 | + |
| 53 | +<div class="webgl_center"><img src="resources/screencapture-398x298.png"></div> |
| 54 | + |
| 55 | +是的,这是一个空白图像。 |
| 56 | + |
| 57 | +根据你的浏览器/操作系统,它可能对你有效,但通常情况下它是无法工作的。 |
| 58 | + |
| 59 | +问题在于,出于性能和兼容性的考虑,浏览器默认会在你绘制完后,清除 WebGL 画布的绘图缓冲区。 |
| 60 | + |
| 61 | +有三种解决方案。 |
| 62 | + |
| 63 | +1. 在截图之前调用渲染代码 |
| 64 | + |
| 65 | + 我们使用的代码是一个 `drawScene` 函数。 |
| 66 | + 最好让这段代码不改变任何状态,这样我们就可以在截图时调用它来进行渲染。 |
| 67 | + |
| 68 | + ```js |
| 69 | + elem.addEventListener('click', () => { |
| 70 | + + drawScene(); |
| 71 | + canvas.toBlob((blob) => { |
| 72 | + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); |
| 73 | + }); |
| 74 | + }); |
| 75 | + ``` |
| 76 | + |
| 77 | +2. 在渲染循环中调用截图代码 |
| 78 | + |
| 79 | + 在这种情况下,我们只需设置一个标志表示我们想要截图,然后在渲染循环中实际执行截图操作。 |
| 80 | + |
| 81 | + ```js |
| 82 | + let needCapture = false; |
| 83 | + elem.addEventListener('click', () => { |
| 84 | + needCapture = true; |
| 85 | + }); |
| 86 | + ``` |
| 87 | + |
| 88 | + 然后在我们的渲染循环中,也就是当前实现于 `drawScene` 的函数中,在所有内容绘制完成之后的某个位置。 |
| 89 | + |
| 90 | + ```js |
| 91 | + function drawScene(time) { |
| 92 | + ... |
| 93 | +
|
| 94 | + + if (needCapture) { |
| 95 | + + needCapture = false; |
| 96 | + + canvas.toBlob((blob) => { |
| 97 | + + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); |
| 98 | + + }); |
| 99 | + + } |
| 100 | +
|
| 101 | + ... |
| 102 | + } |
| 103 | + ``` |
| 104 | + |
| 105 | +3. 在创建 WebGL 上下文时,设置 `preserveDrawingBuffer: true` |
| 106 | + |
| 107 | + ```js |
| 108 | + const gl = someCanvas.getContext('webgl2', {preserveDrawingBuffer: true}); |
| 109 | + ``` |
| 110 | + |
| 111 | + 这会让 WebGL 在将画布与页面其他部分合成后不清除画布,但会阻止某些*可能的*优化。 |
| 112 | + |
| 113 | +我会选择上面的第 1 种方法。对于这个特定示例,我首先会把更新状态的代码部分与绘制的代码部分分离开。 |
| 114 | + |
| 115 | +```js |
| 116 | + var then = 0; |
| 117 | +
|
| 118 | +- requestAnimationFrame(drawScene); |
| 119 | ++ requestAnimationFrame(renderLoop); |
| 120 | +
|
| 121 | ++ function renderLoop(now) { |
| 122 | ++ // Convert to seconds |
| 123 | ++ now *= 0.001; |
| 124 | ++ // Subtract the previous time from the current time |
| 125 | ++ var deltaTime = now - then; |
| 126 | ++ // Remember the current time for the next frame. |
| 127 | ++ then = now; |
| 128 | ++ |
| 129 | ++ // Every frame increase the rotation a little. |
| 130 | ++ rotation[1] += rotationSpeed * deltaTime; |
| 131 | ++ |
| 132 | ++ drawScene(); |
| 133 | ++ |
| 134 | ++ // Call renderLoop again next frame |
| 135 | ++ requestAnimationFrame(renderLoop); |
| 136 | ++ } |
| 137 | +
|
| 138 | + // Draw the scene. |
| 139 | ++ function drawScene() { |
| 140 | +- function drawScene(now) { |
| 141 | +- // Convert to seconds |
| 142 | +- now *= 0.001; |
| 143 | +- // Subtract the previous time from the current time |
| 144 | +- var deltaTime = now - then; |
| 145 | +- // Remember the current time for the next frame. |
| 146 | +- then = now; |
| 147 | +- |
| 148 | +- // Every frame increase the rotation a little. |
| 149 | +- rotation[1] += rotationSpeed * deltaTime; |
| 150 | +
|
| 151 | + webglUtils.resizeCanvasToDisplaySize(gl.canvas); |
| 152 | +
|
| 153 | + ... |
| 154 | +
|
| 155 | +- // Call drawScene again next frame |
| 156 | +- requestAnimationFrame(drawScene); |
| 157 | + } |
| 158 | +``` |
| 159 | + |
| 160 | +现在我们只需在截图之前调用 `drawScene` 即可 |
| 161 | + |
| 162 | +```js |
| 163 | +elem.addEventListener('click', () => { |
| 164 | ++ drawScene(); |
| 165 | + canvas.toBlob((blob) => { |
| 166 | + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); |
| 167 | + }); |
| 168 | +}); |
| 169 | +``` |
| 170 | + |
| 171 | +现在它应该可以正常工作了。 |
| 172 | + |
| 173 | +{{{example url="../webgl-tips-screenshot-good.html" }}} |
| 174 | + |
| 175 | +如果你实际检查捕获的图像,会看到背景是透明的。 |
| 176 | +详情请参见[这篇文章](webgl-and-alpha.html)。 |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +<a id="preservedrawingbuffer" data-toc="Prevent the Canvas Being Cleared"></a> |
| 181 | + |
| 182 | +# 防止画布被清除 |
| 183 | + |
| 184 | +假设你想让用户用一个动画对象进行绘画。 |
| 185 | +在创建 WebGL 上下文时,需要传入 `preserveDrawingBuffer: true`。 |
| 186 | +这可以防止浏览器清除画布。 |
| 187 | + |
| 188 | +采用[动画那篇文章](webgl-animation.html)中的最后一个示例 |
| 189 | + |
| 190 | +```js |
| 191 | +var canvas = document.querySelector("#canvas"); |
| 192 | +-var gl = canvas.getContext("webgl2"); |
| 193 | ++var gl = canvas.getContext("webgl2", {preserveDrawingBuffer: true}); |
| 194 | +``` |
| 195 | + |
| 196 | +并修改对 `gl.clear` 的调用,使其只清除深度缓冲区。 |
| 197 | + |
| 198 | +``` |
| 199 | +-// Clear the canvas. |
| 200 | +-gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); |
| 201 | ++// Clear the depth buffer. |
| 202 | ++gl.clear(gl.DEPTH_BUFFER_BIT); |
| 203 | +``` |
| 204 | +
|
| 205 | +{{{example url="../webgl-tips-preservedrawingbuffer.html" }}} |
| 206 | +
|
| 207 | +注意,如果你真想做一个绘图程序,这不是一个解决方案, |
| 208 | +因为每当我们改变画布的分辨率时,浏览器仍然会清除画布。 |
| 209 | +我们是根据显示尺寸来改变分辨率的。显示尺寸会在窗口大小改变时变化, |
| 210 | +这可能发生在用户下载文件时,甚至在另一个标签页,浏览器添加状态栏时。 |
| 211 | +还包括用户旋转手机,浏览器从竖屏切换到横屏时。 |
| 212 | +
|
| 213 | +如果你真的想做绘图程序,应该[渲染到纹理](webgl-render-to-texture.html)。 |
| 214 | +
|
| 215 | +--- |
| 216 | +
|
| 217 | +<a id="tabindex" data-toc="Get Keyboard Input From a Canvas"></a> |
| 218 | +
|
| 219 | +# 获取键盘输入 |
| 220 | +
|
| 221 | +如果你制作的是全页面/全屏的 WebGL 应用,那么你可以随意处理, |
| 222 | +但通常你希望某个 canvas 只是页面的一部分, |
| 223 | +并希望用户点击 canvas 时它能接收键盘输入。 |
| 224 | +不过 canvas 默认是无法获取键盘输入的。为了解决这个问题, |
| 225 | +需要将 canvas 的 [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/tabIndex) |
| 226 | +设置为 0 或更大。例如: |
| 227 | +
|
| 228 | +```html |
| 229 | +<canvas tabindex="0"></canvas> |
| 230 | +``` |
| 231 | + |
| 232 | +不过这会引发一个新问题。任何设置了 `tabindex` 的元素在获得焦点时都会被高亮显示。 |
| 233 | +为了解决这个问题,需要将其获得焦点时的 CSS 边框(outline)设置为 none。 |
| 234 | + |
| 235 | +```css |
| 236 | +canvas:focus { |
| 237 | + outline:none; |
| 238 | +} |
| 239 | +``` |
| 240 | + |
| 241 | +为演示起见,这里有三个 canvas |
| 242 | + |
| 243 | +```html |
| 244 | +<canvas id="c1"></canvas> |
| 245 | +<canvas id="c2" tabindex="0"></canvas> |
| 246 | +<canvas id="c3" tabindex="1"></canvas> |
| 247 | +``` |
| 248 | + |
| 249 | +以及仅针对最后一个 canvas 的一些 CSS |
| 250 | + |
| 251 | +```css |
| 252 | +#c3:focus { |
| 253 | + outline: none; |
| 254 | +} |
| 255 | +``` |
| 256 | + |
| 257 | +让我们给所有 canvas 都附加相同的事件监听器 |
| 258 | + |
| 259 | +```js |
| 260 | +document.querySelectorAll('canvas').forEach((canvas) => { |
| 261 | + const ctx = canvas.getContext('2d'); |
| 262 | + |
| 263 | + function draw(str) { |
| 264 | + ctx.clearRect(0, 0, canvas.width, canvas.height); |
| 265 | + ctx.textAlign = 'center'; |
| 266 | + ctx.textBaseline = 'middle'; |
| 267 | + ctx.fillText(str, canvas.width / 2, canvas.height / 2); |
| 268 | + } |
| 269 | + draw(canvas.id); |
| 270 | + |
| 271 | + canvas.addEventListener('focus', () => { |
| 272 | + draw('has focus press a key'); |
| 273 | + }); |
| 274 | + |
| 275 | + canvas.addEventListener('blur', () => { |
| 276 | + draw('lost focus'); |
| 277 | + }); |
| 278 | + |
| 279 | + canvas.addEventListener('keydown', (e) => { |
| 280 | + draw(`keyCode: ${e.keyCode}`); |
| 281 | + }); |
| 282 | +}); |
| 283 | +``` |
| 284 | + |
| 285 | +注意,第一个 canvas 无法接受键盘输入。 |
| 286 | +第二个 canvas 可以,但它会被高亮显示。 |
| 287 | +第三个 canvas 同时应用了这两个解决方案。 |
| 288 | + |
| 289 | +{{{example url="../webgl-tips-tabindex.html"}}} |
| 290 | + |
| 291 | +--- |
| 292 | + |
| 293 | +<a id="html-background" data-toc="Use WebGL2 as Background in HTML"></a> |
| 294 | + |
| 295 | +# 将背景设为WebGL动画 |
| 296 | + |
| 297 | +一个常见问题是如何将WebGL动画设置为网页背景。 |
| 298 | + |
| 299 | +以下是两种最常用的实现方式: |
| 300 | + |
| 301 | +* 将Canvas的CSS `position` 设置为 `fixed`,如下所示: |
| 302 | + |
| 303 | +```css |
| 304 | +#canvas { |
| 305 | + position: fixed; |
| 306 | + left: 0; |
| 307 | + top: 0; |
| 308 | + z-index: -1; |
| 309 | + ... |
| 310 | +} |
| 311 | +``` |
| 312 | + |
| 313 | +并将 `z-index` 设为 -1。 |
| 314 | + |
| 315 | +这种方案的一个小缺点是:你的 JavaScript 代码必须与页面其他部分兼容,如果页面比较复杂,就需要确保 WebGL 代码中的 JavaScript 不会与页面其他功能的 JavaScript 产生冲突。 |
| 316 | + |
| 317 | +* 使用 `iframe` |
| 318 | + |
| 319 | +这正是本站[首页](/)采用的解决方案。 |
| 320 | + |
| 321 | +在您的网页中,只需插入一个iframe即可实现,例如: |
| 322 | + |
| 323 | +```html |
| 324 | +<iframe id="background" src="background.html"></iframe> |
| 325 | +<div> |
| 326 | + Your content goes here. |
| 327 | +</div> |
| 328 | +``` |
| 329 | + |
| 330 | +接下来将这个iframe设置为全屏背景样式,本质上和我们之前设置canvas的代码相同——只是需要额外将 `border` 设为 `none`,因为iframe默认带有边框。具体实现如下: |
| 331 | + |
| 332 | +```css |
| 333 | +#background { |
| 334 | + position: fixed; |
| 335 | + width: 100vw; |
| 336 | + height: 100vh; |
| 337 | + left: 0; |
| 338 | + top: 0; |
| 339 | + z-index: -1; |
| 340 | + border: none; |
| 341 | + pointer-events: none; |
| 342 | +} |
| 343 | +``` |
| 344 | + |
| 345 | +{{{example url="../webgl-tips-html-background.html"}}} |
0 commit comments