Browse Source

add gotchas article

Gregg Tavares 6 years ago
parent
commit
11f65c1675

BIN
threejs/lessons/resources/images/screencapture-413x313.png


+ 328 - 0
threejs/lessons/threejs-gotchas.md

@@ -0,0 +1,328 @@
+Title: Three.js Gotchas
+Description: Small issues that might trip you up using three.js
+
+This article is a collection of small issues you might run into
+using three.js that seemed too small to have their own article.
+
+<a id="screenshot"></a>
+
+# Taking A Screenshot of the Canvas
+
+In the browser there are effectively 2 functions that will take a screenshot.
+The old one 
+[`canvas.toDataURL`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL)
+and the new better one 
+[`canvas.toBlob`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob)
+
+So you'd think it would be easy to take a screenshot by just adding some code like
+
+```html
+<canvas id="c"></canvas>
++<button id="screenshot" type="button">Save...</button>
+```
+
+```js
+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();
+  };
+}());
+```
+
+Here's the example from [the article on responsiveness](threejs-responsive.html)
+with the code above added and some CSS to place the button
+
+{{{example url="../threejs-gotchas-screenshot-bad.html"}}}
+
+When I tried it I got this screenshot
+
+<div class="threejs_center"><img src="resources/images/screencapture-413x313.png"></div>
+
+Yes, it's just a black image.
+
+It's possible it worked for you depending on your browser/OS but in general
+it's not likely to work.
+
+The issue is that for performance and compatibility reasons, by default the browser
+will clear a WebGL canvas's drawing buffer after you've drawn to it.
+
+The solution is to call your rendering code just before capturing.
+
+In our code we need to adjust a few things. First let's separate
+out the rendering code.
+
+```js
++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);
+```
+
+Now that `render` is only concerned with actually rendering
+we can call it just before capturing the canvas.
+
+```js
+const elem = document.querySelector('#screenshot');
+elem.addEventListener('click', () => {
++  render();
+  canvas.toBlob((blob) => {
+    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
+  });
+});
+```
+
+And now it should work.
+
+{{{example url="../threejs-gotchas-screenshot-good.html" }}}
+
+For a different solution see the next item.
+
+<a id="preservedrawingbuffer"></a>
+
+# Preventing the canvas being cleared
+
+Let's say you wanted to let the user paint with an animated
+object. You need to pass in `preserveDrawingBuffer: true` when
+you create the `WebGLRenderer`. This prevents the browser from
+clearing the canvas. You also need to tell three.js not to clear
+the canvas as well.
+
+```js
+const canvas = document.querySelector('#c');
+-const renderer = new THREE.WebGLRenderer({canvas});
++const renderer = new THREE.WebGLRenderer({
++  canvas,
++  preserveDrawingBuffer: true,
++  alpha: true,
++});
++renderer.autoClearColor = false;
+```
+
+{{{example url="../threejs-gotchas-preservedrawingbuffer.html" }}}
+
+Note that if you were serious about making a drawing program this would not be a
+solution as the browser will still clear the canvas anytime we change its
+resolution. We're changing is resolution based on its display size. Its display
+size changes when the window changes size. That includes when the user downloads
+a file, even in another tab, and the browser adds a status bar. It also includes when
+the user turns their phone and the browser switches from portrait to landscape.
+
+If you really wanted to make a drawing program you'd
+[render to a texture using a render target](threejs-rendertargets.html).
+
+<a id="tabindex"></a>
+
+# Getting Keyboard Input
+
+Throughout these tutorials we've often attached event listeners to the `canvas`.
+While many events work, one that does not work by default is keyboard
+events.
+
+To get keyboard events, set the [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/tabIndex)
+of the canvas to 0 or more. Eg.
+
+```html
+<canvas tabindex="0"></canvas>
+```
+
+This ends up causing a new issue though. Anything that has a `tabindex` set
+will get highlighted when it has the focus. To fix that set its focus CSS outline
+to none
+
+```css
+canvas:focus {
+  outline:none;
+}
+```
+
+To demonstrate here are 3 canvases 
+
+```html
+<canvas id="c1"></canvas>
+<canvas id="c2" tabindex="0"></canvas>
+<canvas id="c3" tabindex="1"></canvas>
+```
+
+and some css just for the last canvas 
+
+```css
+#c3:focus {
+    outline: none;
+}
+```
+
+Let's attach the same event listeners to all of them
+
+```js
+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}`);
+  });
+});
+```
+
+Notice you can't get the first canvas to accept keyboard input.
+The second canvas you can but it gets highlighted. The 3rd
+canvas has both solutions applied.
+
+{{{example url="../threejs-gotchas-tabindex.html"}}}
+
+<a id="transparent-canvas"></a>
+ 
+# Making the Canvas Transparent
+
+By default THREE.js makes the canvas opaque. If you want the
+canvas to be transparent pass in [`alpha:true`](WebGLRenderer.alpha) when you create
+the `WebGLRenderer`
+
+```js
+const canvas = document.querySelector('#c');
+-const renderer = new THREE.WebGLRenderer({canvas});
++const renderer = new THREE.WebGLRenderer({
++  canvas,
++  alpha: true,
++});
+```
+
+You probably also want to tell it that your results are **not** using premultiplied alpha
+
+```js
+const canvas = document.querySelector('#c');
+const renderer = new THREE.WebGLRenderer({
+  canvas,
+  alpha: true,
++  premultipliedAlpha: false,
+});
+```
+
+Three.js defaults to the canvas using
+[`premultipliedAlpha: true`](WebGLRenderer.premultipliedAlpha) but defaults
+to materials outputting [`premultipliedAlpha: false`](Material.premultipliedAlpha).
+
+If you'd like a better understanding of when and when not to use premultiplied alpha
+here's [a good article on it](https://developer.nvidia.com/content/alpha-blending-pre-or-not-pre).
+
+In any case let's setup a simple example with a transparent canvas.
+
+We applied the settings above to the example from [the article on responsiveness](threejs-responsive.html).
+Let's also make the materials more transparent.
+
+```js
+function makeInstance(geometry, color, x) {
+-  const material = new THREE.MeshPhongMaterial({color});
++  const material = new THREE.MeshPhongMaterial({
++    color,
++    opacity: 0.5,
++  });
+
+...
+
+```
+
+And let's add some HTML content
+
+```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>
+```
+
+as well as some CSS to put the canvas in front
+
+```css
+body {
+    margin: 0;
+}
+#c {
+    width: 100vw;
+    height: 100vh;
+    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: 100vw;
++  height: 100vh;
++  display: flex;
++  justify-content: center;
++  align-items: center;
++}
+```
+
+note that `pointer-events: none` makes the canvas invisible to the mouse
+and touch events so you can select the text beneath.
+
+{{{example url="../threejs-gotchas-transparent-canvas.html" }}}

+ 7 - 0
threejs/lessons/toc.html

@@ -54,6 +54,13 @@
     <li><a href="/threejs/lessons/threejs-custom-geometry.html">Custom Geometry</a></li>
     <li><a href="/threejs/lessons/threejs-custom-buffergeometry.html">Custom BufferGeometry</a></li>
   </ul>
+  <li>Gotchas</li>
+  <ul>
+    <li><a href="/threejs/lessons/threejs-gotchas.html#screenshot">Taking a screenshot</a></li>
+    <li><a href="/threejs/lessons/threejs-gotchas.html#preservedrawingbuffer">Prevent the Canvas Being Cleared</a></li>
+    <li><a href="/threejs/lessons/threejs-gotchas.html#tabindex">Get Keyboard Input From a Canvas</a></li>
+    <li><a href="/threejs/lessons/threejs-gotchas.html#transparent-canvas">Make the Canvas Transparent</a></li>
+  </ul>
   <li>Reference</li>
   <ul>
     <li><a href="/threejs/lessons/threejs-material-table.html">Material Table</a></li>

+ 128 - 0
threejs/threejs-gotchas-preservedrawingbuffer.html

@@ -0,0 +1,128 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - PreserveDrawingBuffer</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+  </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r105/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({
+    canvas,
+    preserveDrawingBuffer: true,
+    alpha: true,
+  });
+  renderer.autoClearColor = false;
+
+  const camera = new THREE.OrthographicCamera(-2, 2, 1, -1, -1, 1);
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  const base = new THREE.Object3D();
+  scene.add(base);
+  base.scale.set(0.1, 0.1, 0.1);
+
+  function makeInstance(geometry, color, x, y, z) {
+    const material = new THREE.MeshPhongMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    base.add(cube);
+
+    cube.position.set(x, y, z);
+
+    return cube;
+  }
+
+  makeInstance(geometry, '#F00', -2,  0,  0);
+  makeInstance(geometry, '#FF0',  2,  0,  0);
+  makeInstance(geometry, '#0F0',  0, -2,  0);
+  makeInstance(geometry, '#0FF',  0,  2,  0);
+  makeInstance(geometry, '#00F',  0,  0, -2);
+  makeInstance(geometry, '#F0F',  0,  0,  2);
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  const state = { x: 0, y: 0 };
+
+  function render(time) {
+    time *= 0.001;  // convert to seconds
+
+    if (resizeRendererToDisplaySize(renderer)) {
+        const canvas = renderer.domElement;
+        camera.right = canvas.clientWidth / canvas.clientHeight;
+        camera.left = -camera.right;
+        camera.updateProjectionMatrix();
+      }
+
+      base.position.set(state.x, state.y, 0);
+      base.rotation.x = time;
+      base.rotation.y = time * 1.11;
+
+      renderer.render(scene, camera);
+
+      requestAnimationFrame(render);
+  }
+  requestAnimationFrame(render);
+
+  const temp = new THREE.Vector3();
+  function setPosition(e) {
+    const x = e.clientX / canvas.clientWidth  *  2 - 1;
+    const y = e.clientY / canvas.clientHeight * -2 + 1;
+    temp.set(x, y, 0).unproject(camera);
+    state.x = temp.x;
+    state.y = temp.y;
+  }
+
+  canvas.addEventListener('mousemove', setPosition);
+  canvas.addEventListener('touchmove', (e) => {
+    e.preventDefault();
+    setPosition(e.touches[0]);
+  }, {passive: false});
+}
+
+main();
+</script>
+</html>
+

+ 145 - 0
threejs/threejs-gotchas-screenshot-bad.html

@@ -0,0 +1,145 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Screenshot (bad)</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    #screenshot {
+      position: absolute;
+      left: 10px;
+      top: 10px;
+      padding: 10px;
+      background: rgba(0,0,0,0.9);
+      color: white;
+      border: 1px solid gray;
+      cursor: pointer;
+    }
+    #screenshot:hover {
+      background: #444;
+    }
+    #screenshot:focus {
+      outline: none;
+    }
+  </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+    <button id="screenshot" type="button">Save...</button>
+  </body>
+<script src="resources/threejs/r105/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2;
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshPhongMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44aa88,  0),
+    makeInstance(geometry, 0x8844aa, -2),
+    makeInstance(geometry, 0xaa8844,  2),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    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;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+
+  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();
+    };
+  }());
+}
+
+main();
+</script>
+</html>
+

+ 153 - 0
threejs/threejs-gotchas-screenshot-good.html

@@ -0,0 +1,153 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Screenshot (good)</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    #screenshot {
+      position: absolute;
+      left: 10px;
+      top: 10px;
+      padding: 10px;
+      background: rgba(0,0,0,0.9);
+      color: white;
+      border: 1px solid gray;
+      cursor: pointer;
+    }
+    #screenshot:hover {
+      background: #444;
+    }
+    #screenshot:focus {
+      outline: none;
+    }
+  </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+    <button id="screenshot" type="button">Save...</button>
+  </body>
+<script src="resources/threejs/r105/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2;
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshPhongMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44aa88,  0),
+    makeInstance(geometry, 0x8844aa, -2),
+    makeInstance(geometry, 0xaa8844,  2),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  const state = {
+    time: 0,
+  };
+
+  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 = state.time * speed;
+        cube.rotation.x = rot;
+        cube.rotation.y = rot;
+      });
+
+      renderer.render(scene, camera);
+  }
+
+  function animate(time) {
+    state.time = time * 0.001;
+
+    render();
+
+    requestAnimationFrame(animate);
+  }
+  requestAnimationFrame(animate);
+
+  const elem = document.querySelector('#screenshot');
+  elem.addEventListener('click', () => {
+    render();
+    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();
+    };
+  }());
+}
+
+main();
+</script>
+</html>
+

+ 68 - 0
threejs/threejs-gotchas-tabindex.html

@@ -0,0 +1,68 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - TabIndex</title>
+    <style>
+    .spread {
+      display: flex;
+      font-size: x-small;
+      text-align: center;
+    }
+    canvas {
+        margin: 5px;
+        background: pink;
+    }
+    #c3:focus {
+        outline: none;
+    }
+  </style>
+  </head>
+  <body>
+    <div class="spread">
+      <div>
+          <canvas width="100" height="100" id="c1"></canvas>
+          <div>tabindex not set</div>
+      </div>
+      <div>
+        <canvas width="100" height="100" id="c2" tabindex="0"></canvas>
+        <div>focus style not set</div>
+      </div>
+      <div>
+        <canvas width="100" height="100" id="c3" tabindex="1"></canvas>
+        <div>tabindex and<br/>focus style set</div>
+      </div>
+    </div>
+  </body>
+<script>
+'use strict';
+
+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}`);
+  });
+});
+
+</script>
+</html>
+

+ 143 - 0
threejs/threejs-gotchas-transparent-canvas.html

@@ -0,0 +1,143 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Transparent Canvas</title>
+    <style>
+    html {
+        box-sizing: border-box;
+    }
+    *, *:before, *:after {
+        box-sizing: inherit;
+    }
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        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: 100vw;
+        height: 100vh;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+    }
+  </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+    <div id="content">
+      <div>
+        <h1>Cubes-R-Us!</h1>
+        <p>We make the best cubes!</p>
+      </div>
+    </div>
+  </body>
+<script src="resources/threejs/r105/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({
+    canvas,
+    alpha: true,
+    premultipliedAlpha: false,
+  });
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2;
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshPhongMaterial({
+      color,
+      opacity: 0.5,
+    });
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44aa88,  0),
+    makeInstance(geometry, 0x8844aa, -2),
+    makeInstance(geometry, 0xaa8844,  2),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    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;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+