123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- Title: Three.js Tips
- Description: Small issues that might trip you up using three.js
- TOC: #
- 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" data-toc="Taking a 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-tips-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-tips-screenshot-good.html" }}}
- For a different solution see the next item.
- ---
- <a id="preservedrawingbuffer" data-toc="Prevent the Canvas Being Cleared"></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-tips-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" data-toc="Get Keyboard Input From a Canvas"></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-tips-tabindex.html"}}}
- ---
- <a id="transparent-canvas" data-toc="Make the Canvas Transparent"></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: 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;
- +}
- ```
- 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-tips-transparent-canvas.html" }}}
- ---
- <a id="html-background" data-toc="Use three.js as Background in HTML"></a>
- # Making your background a three.js animation
- A common question is how to make a three.js animation be the background of
- a webpage.
- There are 2 obvious ways.
- * Set the canvas CSS `position` to `fixed` as in
- ```css
- #c {
- position: fixed;
- left: 0;
- top: 0;
- ...
- }
- ```
- You can basically see this exact solution on the previous example. Just set `z-index` to -1
- and the cubes will appear behind the text.
- A small disadvantage to this solution is your JavaScript must integrate with the page
- and if you have a complex page then you need to make sure none of the JavaScript in your
- three.js visualization conflict with the JavaScript doing other things in the page.
- * Use an `iframe`
- This is the solution used on [the front page of this site](/).
- In your webpage just insert an iframe, for example
- ```html
- <iframe id="background" src="threejs-responsive.html">
- <div>
- Your content goes here.
- </div>
- ```
- Then style the iframe to fill the window and be in the background
- which is basically the same code as we used above for the canvas
- except we also need to set `border` to `none` since iframes have
- a border by default.
- ```
- #background {
- position: fixed;
- width: 100%;
- height: 100%;
- left: 0;
- top: 0;
- z-index: -1;
- border: none;
- pointer-events: none;
- }
- ```
- {{{example url="../threejs-tips-html-background.html"}}}
|