Skip to content

Commit

Permalink
Handles resizing of ‹three-app› element (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
olange committed Feb 20, 2019
1 parent d5b1e3d commit 7452d93
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 59 deletions.
8 changes: 6 additions & 2 deletions demos/three-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
<script src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<script type="module" src="../node_modules/@petitatelier/three-app/three-app.js"></script>
<link rel="stylesheet" href="demo-component.css">
<style>
.fullbleed { margin: 0; height: 100vh }
main, three-app { height: 100% }
</style>
</head>
<body class="unresolved">
<body class="unresolved fullbleed">
<noscript>Please enable JavaScript to view this website.</noscript>

<header>
Expand All @@ -17,7 +21,7 @@
</header>

<main>
<three-app fps="50"></three-app>
<three-app fps="10"></three-app>
</main>

<script>
Expand Down
3 changes: 2 additions & 1 deletion packages/three-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ Three.js app container, that provides animation timing and registry of canvases,

1. Imports [THREE.js](https://github.com/mrdoob/three.js/) and makes it available in [Window global scope](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope);
2. Handles _resizing_ of its bounding box — propagating the change to the aspect ratio to the cameras and renderers;
3. Animates the scenes, cameras and objects — synchronized in the same animation frame, running at a desired frequency.
3. Animates the scenes and cameras — synchronized in the same animation step, running at a desired FPS (if possible);
4. Renders the current scene, at the same desired or actual FPS.
3 changes: 2 additions & 1 deletion packages/three-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"scripts": {},
"dependencies": {
"lit-element": "^2.0.1",
"lit-html": "^1.0.0"
"lit-html": "^1.0.0",
"three": "^0.101.1"
},
"keywords": [
"three.js",
Expand Down
206 changes: 151 additions & 55 deletions packages/three-app/three-app.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { LitElement, html, css } from "lit-element";
import * as THREE from "three";

const DEFAULTS = { fps: 60 };

export class ThreeApp extends LitElement {

static get styles() {
return css`
:host { display: block; }
:host([ hidden]) { display: none; }
:host { display: flex; flex-direction: column; position: relative }
:host([ hidden]) { display: none }
#display { height: 100% }
#info { position: absolute; top: 1em; right: 1em; color: white }
#info > * { margin: 0 0 1em }
`;
}

render() {
return html`
<h1>ThreeApp</h1>
<p>Desired ${this.fps} FPS (a frame about every ${this.interval} ms).</p>
<div id="info">
<p>Desired: ${this.fps} FPS (a frame about every ${this._interval} ms)<br/>
Actual: ${this._fpsActual} FPS (a frame about every ${this._intervalActual} ms)</p>
</div>
<canvas id="display"></canvas>
<slot></slot>
`;
}

Expand All @@ -23,7 +31,7 @@ export class ThreeApp extends LitElement {
*/
static get properties() {
return {
/** Target FPS */
/** Desired FPS */
fps: { type: Number, reflect: true }
};
}
Expand All @@ -35,14 +43,16 @@ export class ThreeApp extends LitElement {
const oldVal = this._fps;
// newVal is set to `null` by Lit-Element, when attribute is removed
this._fps = (newVal == null) ? DEFAULTS.fps : Math.floor( newVal);
this.interval = Math.floor( 1000 / this._fps); // ms
this._interval = Math.floor( 1000 / this._fps); // ms
this.requestUpdate( "fps", oldVal);
}

/**
* In the element constructor, assign default property values.
*/
constructor() {
console.log( "three-app › constructor()");

// Must call superconstructor first.
super();

Expand All @@ -53,93 +63,179 @@ export class ThreeApp extends LitElement {
this.resize = this.resizeCallback.bind( this);

// Initialize internal properties
this._fps = undefined;
this.interval = undefined;
this.time = undefined;
this.lastTime = undefined;
this._initialized = false;

this._canvas = undefined; // a reference to our ‹canvas› element
this._renderer = undefined; // the THREE WebGL renderer used to draw to our canvas
this._displayRatio = undefined; // current display ratio of the canvas, computed

this._fps = undefined; // defined by `fps` property setter
this._fpsActual = undefined; // computed by `this.tick()`
this._interval = undefined; // derived from `this.fps`, computed by `fps` property setter
this._intervalActual = undefined; // computed by `this.tick()`
this._time = undefined; // computed by `this.tick()`
this._lastTime = undefined; // computed by `this.tick()`

this.scenes = [];
this.cameras = [];
this.canvases = [];
this.renderers = [];

// Initialize public properties
// Initialize public properties (must come after internal properties)
this.fps = DEFAULTS.fps; // will trigger computation of this._interval

// Start the animation
this.start();
}

/**
* Implement firstUpdated to perform one-time work on first update:
* - Call a method to load the lazy element if necessary
* - Focus the checkbox
* Initializes the renderer and starts the animation loop.
*
* One-time call, which happens after the Shadow DOM was first
* rendered by Lit-Element. Ensures the ‹canvas› element is available.
*/
firstUpdated() {
console.log( "three-app › firstUpdated()");

// Initializes the WebGL renderer and links it to our ‹canvas› element
this.init();

// Start the animation loop and timer
this.start();
}

/**
* The main animation step, which actually updates the scenes and renders them.
* Called automatically from `tick()`, every time the animation interval elapsed
* (for instance, if `fps` property is set to 60, once about every 16ms).
* Intializes the WebGL renderer and our canvas.
*
* 1. Updates each scene in turn;
* 2. Renders each scene in turn.
* Should be called from within `firstUpdated()`, once the Shadow DOM
* was rendered by Lit-Element.
*/
init() {
console.log( "three-app › init()");

// Get and keep a reference to the our ‹canvas› element
this._canvas = this.shadowRoot.getElementById( "display");

// Instantiates a Three WebGL renderer, rendering to our ‹canvas› element
this._renderer = new THREE.WebGLRenderer(
{ antialias: true, canvas: this._canvas });

// Update size of the display buffer of the renderer
this.resize();

// From now on, `start()` can be called to animate and render the scenes
this._initialized = true;
}

/**
* Starts the main animation loop and timer.
*
* @param {number} time The current time; a high-resolution timer value, as it comes from `window.requestAnimationFrame()`.
* @param {number} delta The delta time in ms since the last animation interval.
* Should be called from within `firstUpdated()`, once the Shadow DOM
* was rendered by Lit-Element, and only after `init()` was called.
*/
stepCallback( time, delta) {
// console.log( { time, delta, lastTime: this.lastTime, fps: this.fps });
if( this.needsResize()) { this.resize(); }
// this.scenes.update( time, delta);
// this.cameras.update( time, delta);
// this.renderers.render();
start() {
console.assert( this._initialized, "three-app › start(): element incompletely initialized; call `init()` first.");
console.log( "three-app › start()");

this._lastTime = 0;
window.requestAnimationFrame( this.tick);
}

/**
* The main animation timer. Called automatically once per browser frame,
* as a result of `window.requestAnimationFrame()`. Actions performed:
* The main animation timer and loop. Called automatically once per
* browser frame, as a result of `window.requestAnimationFrame()`.
*
* Actions performed:
*
* 1. Updates the local tick value;
* 2. Updates and renders each scene in turn, if animation interval elapsed;
* 2. Updates and renders each scene in turn, at desired FPS, if possible;
* 3. Schedules another call to requestAnimationFrame.
*
* @param {number} time The current time; a high-resolution timer value,
* as it comes from `window.requestAnimationFrame()`.
*/
tickCallback( time) {
this.time = time;
const delta = time - this.lastTime;
this._time = time;
const delta = time - this._lastTime;

if( delta >= this.interval) {
if( delta >= this._interval) {
this.updateTimings( delta);
this.step( time, delta);
this.lastTime = this.time;
this._lastTime = time;
}

window.requestAnimationFrame( this.tick); // `this.tick()` is the `tickCallback()` bound to each instance of this class; see constructor
window.requestAnimationFrame( this.tick); // `this.tick()` is this `tickCallback()` bound to each instance of this class; see constructor
}

start() {
this.lastTime = 0;
window.requestAnimationFrame( this.tick);
/**
* Updates the actual FPS and actual interval timing properties,
* computed from the current `delta` interval.
* @param {number} delta Time in millisecond, elapsed since last tick.
*/
updateTimings( delta) {
const _intervalActualCurr = this._intervalActual;
if (delta !== _intervalActualCurr) {
const _fpsActualCurr = this._fpsActual;
this._intervalActual = delta;
this._fpsActual = Math.ceil(1000 / delta);
this.requestUpdate( "_intervalActual", _intervalActualCurr);
this.requestUpdate( "_fpsActual", _fpsActualCurr);
}
}

/**
* The main animation step, which actually updates the scenes and renders them.
* Called automatically from `tick()`, every time the animation interval elapsed
* (for instance, if `fps` property is set to 60, once about every 16ms).
*
* 1. Updates each scene in turn;
* 2. Renders each scene in turn.
*
* @param {number} time The current time; a high-resolution timer value, as it comes from `window.requestAnimationFrame()`.
* @param {number} delta The delta time in ms since the last animation interval.
*/
stepCallback( time, delta) {
if( this.needsResize()) { this.resize(); }
// this.scenes.update( time, delta);
// this.cameras.update( time, delta);
// this.renderers.render();
}

/**
* Returns the client width and height, as computed by the browser,
* and display ratio, of our canvas.
*/
getDisplaySize() {
const width = this._canvas.clientWidth,
height = this._canvas.clientHeight,
ratio = width / height;
return { width, height, ratio };
}

/**
* Returns true, when the size of the internal display buffer of
* the renderer does not match the actual client size of our canvas;
* false otherwise.
*/
needsResizeCallback() {
return false;
// const displayWidth = this._canvasElement.clientWidth;
// const displayHeight = this._canvasElement.clientHeight;
// const renderSize = this.renderer.getSize();
// return( renderSize.width != displayWidth || renderSize.height != displayHeight);
const { width, height } = this.getDisplaySize();
const renderSize = this._renderer.getSize();
return( renderSize.width !== width || renderSize.height !== height);
}

/**
* Updates the size of the display buffer of the renderer,
* as well as the aspect ratio and the projection matrix of all cameras,
* to match the actual client size of our canvas.
*/
resizeCallback() {
// const displayWidth = this._canvasElement.clientWidth;
// const displayHeight = this._canvasElement.clientHeight;
// const displayRatio = displayWidth / displayHeight;
// console.log( `my-anim › resize() to ${displayWidth}x${displayHeight} (1:${displayRatio})`);
// this.camera.aspect = displayRatio;
// this.camera.updateProjectionMatrix();
// this.renderer.setSize( displayWidth, displayHeight, false);
const { width, height, ratio } = this.getDisplaySize();
console.log( `three-app › resize() to ${width}x${height}px (ratio of 1:${ratio})`);

// Update the aspect and projection matrix of all cameras,
// to match the new display ratio
this.cameras.forEach(( camera) => {
camera.aspect = ratio;
camera.updateProjectionMatrix();
});

// Update the renderer display buffer, to match the new display size
this._renderer.setSize( width, height, false);
}
}

Expand Down

0 comments on commit 7452d93

Please sign in to comment.