Skip to content

Commit

Permalink
Defer initialization until end of aframe script or manual ready signal (
Browse files Browse the repository at this point in the history
#5481)

Co-authored-by: Noeri Huisman <mrxz@users.noreply.github.com>
Co-authored-by: Chris Chua <chris.sirhc@gmail.com>
  • Loading branch information
3 people committed Mar 15, 2024
1 parent af4523e commit 4a89bb6
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 58 deletions.
33 changes: 33 additions & 0 deletions docs/introduction/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,39 @@ Phones with Adreno 300 series GPUs are notoriously problematic. Set [renderer pr

Using A-Frame online sometimes is not possible or inconvenient, like for instance when traveling or during public events with poor Internet connectivity. A-Frame is mostly self-contained so including the build (aframe.min.js) in your project will be sufficient in many cases. Some specific parts are lazy loaded and only fetched when used. This is for example the case of the fonts for the text component and the 3D models for controllers. In order to make an A-Frame build work either offline or without relying on A-Frame hosting infrastructure (typically cdn.aframe.io), you can monitor network requests on your browser console. This will show precisely what assets are being loaded and thus as required for your specific experience. Fonts can be found via FONT_BASE_URL in the whereas controllers via MODEL_URLS. Both can be modified in the source and included in your own [custom build](https://github.com/aframevr/aframe#generating-builds)

## Can I load A-Frame as an ES module?

You can load A-Frame as an ES module using a [side effect import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#import_a_module_for_its_side_effects_only). A-Frame will then initialize any `<a-scene>` in the document. It's still important to register any components or systems you need before this happens:

```HTML
<head>
<script type="importmap">
{
"imports": {
"aframe": "https://aframe.io/releases/1.5.0/aframe.min.js",
}
}
</script>
<script type="module">
import 'aframe';
AFRAME.registerComponent('my-component', {
...
});
</script>
</head>
```

If it's not possible to register everything synchronously to importing A-Frame, you can set the `window.AFRAME_ASYNC` flag. This prevents A-Frame from initializing `<a-scene>` tags, until you give a ready signal by calling `window.AFRAME.emitReady()`. Note that this flag must be set before importing A-Frame, as shown in the following example:

```JS
window.AFRAME_ASYNC = true;
await import('aframe');

// Asynchronously register components/systems

window.AFRAME.ready();
```

## What order does A-Frame render objects in?

[sortTransparentObjects]: ../components/renderer.md#sorttransparentobjects
Expand Down
12 changes: 1 addition & 11 deletions src/core/a-assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ class AAssets extends ANode {
this.timeout = null;
}

connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
return;
}

this.doConnectedCallback();
}

doConnectedCallback () {
var self = this;
var i;
Expand All @@ -38,7 +28,7 @@ class AAssets extends ANode {
var timeout;
var children;

super.connectedCallback();
super.doConnectedCallback();

if (!this.parentNode.isScene) {
throw new Error('<a-assets> must be a child of a <a-scene>.');
Expand Down
15 changes: 1 addition & 14 deletions src/core/a-entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,13 @@ class AEntity extends ANode {
this.setEntityAttribute(attr, oldVal, newVal);
}

/**
* Add to parent, load, play.
*/
connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
return;
}

AEntity.prototype.doConnectedCallback.call(this);
}

doConnectedCallback () {
var self = this; // Component.
var assetsEl; // Asset management system element.
var sceneEl;

// ANode method.
super.connectedCallback();
super.doConnectedCallback();

sceneEl = this.sceneEl;

Expand Down
12 changes: 1 addition & 11 deletions src/core/a-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,8 @@ class AMixin extends ANode {
this.isMixin = true;
}

connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
return;
}

this.doConnectedCallback();
}

doConnectedCallback () {
super.connectedCallback();
super.doConnectedCallback();

this.sceneEl = this.closestScene();
this.id = this.getAttribute('id');
Expand Down
15 changes: 5 additions & 10 deletions src/core/a-node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* global customElements, CustomEvent, HTMLElement, MutationObserver */
var utils = require('../utils/');
var readyState = require('./readyState');

var warn = utils.debug('core:a-node:warn');

Expand Down Expand Up @@ -32,19 +33,13 @@ class ANode extends HTMLElement {
this.mixinEls = [];
}

onReadyStateChange () {
if (document.readyState === 'complete') {
this.doConnectedCallback();
}
}

connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
// Defer if not ready to initialize.
if (!readyState.canInitializeElements) {
document.addEventListener('aframeready', this.connectedCallback.bind(this));
return;
}
ANode.prototype.doConnectedCallback.call(this);
this.doConnectedCallback();
}

doConnectedCallback () {
Expand Down
35 changes: 35 additions & 0 deletions src/core/readyState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* global CustomEvent */

/**
* Flag indicating if A-Frame can initialize the scene or should wait.
*/
module.exports.canInitializeElements = false;

/**
* Waits for the document to be ready.
*/
function waitForDocumentReadyState () {
if (document.readyState === 'complete') {
emitReady();
return;
}

document.addEventListener('readystatechange', function onReadyStateChange () {
if (document.readyState !== 'complete') { return; }
document.removeEventListener('readystatechange', onReadyStateChange);
emitReady();
});
}
module.exports.waitForDocumentReadyState = waitForDocumentReadyState;

/**
* Signals A-Frame that everything is ready to initialize.
*/
function emitReady () {
if (module.exports.canInitializeElements) { return; }
module.exports.canInitializeElements = true;
setTimeout(function () {
document.dispatchEvent(new CustomEvent('aframeready'));
});
}
module.exports.emitReady = emitReady;
12 changes: 1 addition & 11 deletions src/core/scene/a-scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,6 @@ class AScene extends AEntity {
document.documentElement.classList.remove('a-fullscreen');
}

connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
return;
}

this.doConnectedCallback();
}

doConnectedCallback () {
var self = this;
var embedded = this.hasAttribute('embedded');
Expand All @@ -90,7 +80,7 @@ class AScene extends AEntity {
this.setAttribute('screenshot', '');
this.setAttribute('xr-mode-ui', '');
this.setAttribute('device-orientation-permission-ui', '');
super.connectedCallback();
super.doConnectedCallback();

// Renderer initialization
setupCanvas(this);
Expand Down
5 changes: 4 additions & 1 deletion src/core/system.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var components = require('./component');
var schema = require('./schema');
var utils = require('../utils/');
var ready = require('./readyState');

var parseProperties = schema.parseProperties;
var parseProperty = schema.parseProperty;
Expand Down Expand Up @@ -152,5 +153,7 @@ module.exports.registerSystem = function (name, definition) {
systems[name] = NewSystem;

// Initialize systems for existing scenes
for (i = 0; i < scenes.length; i++) { scenes[i].initSystem(name); }
if (ready.canInitializeElements) {
for (i = 0; i < scenes.length; i++) { scenes[i].initSystem(name); }
}
};
7 changes: 7 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var shaders = require('./core/shader').shaders;
var systems = require('./core/system').systems;
// Exports THREE to window so three.js can be used without alteration.
var THREE = window.THREE = require('./lib/three');
var readyState = require('./core/readyState');

var pkg = require('../package');

Expand All @@ -82,6 +83,11 @@ console.log('THREE Version (https://github.com/supermedium/three.js):',
pkg.dependencies['super-three']);
console.log('WebVR Polyfill Version:', pkg.dependencies['webvr-polyfill']);

// Wait for ready state, unless user asynchronously initializes A-Frame.
if (!window.AFRAME_ASYNC) {
readyState.waitForDocumentReadyState();
}

module.exports = window.AFRAME = {
AComponent: require('./core/component').Component,
AEntity: AEntity,
Expand All @@ -104,6 +110,7 @@ module.exports = window.AFRAME = {
schema: require('./core/schema'),
shaders: shaders,
systems: systems,
emitReady: readyState.emitReady,
THREE: THREE,
utils: utils,
version: pkg.version
Expand Down
10 changes: 10 additions & 0 deletions tests/core/AFRAME.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,14 @@ suite('AFRAME', function () {
test('exposes component prototype', function () {
assert.ok(AFRAME.AComponent);
});

test('exposes THREE.js as global and on AFRAME', function () {
assert.ok(window.THREE);
assert.ok(AFRAME.THREE);
assert.strictEqual(AFRAME.THREE, window.THREE);
});

test('exposes emitReady function', function () {
assert.ok(AFRAME.emitReady);
});
});
68 changes: 68 additions & 0 deletions tests/core/readyState.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* global AFRAME, assert, suite, test, setup */
var readyState = require('core/readyState');

suite('readyState', function () {
setup(function (done) {
// Test setup initializes AFRAME when document is already ready.
// This timeout ensures the readyState is reset before running the tests here.
setTimeout(function () {
readyState.canInitializeElements = false;
done();
});
});

suite('waitForDocumentReadyState', function () {
test('emits aframeready when document is ready', function (done) {
var listenerSpy = this.sinon.spy();
document.addEventListener('aframeready', listenerSpy);

assert.equal(document.readyState, 'complete');
readyState.waitForDocumentReadyState();

setTimeout(function () {
assert.ok(listenerSpy.calledOnce);
done();
});
});
});

suite('emitReady', function () {
test('emits aframeready', function (done) {
var listenerSpy = this.sinon.spy();
document.addEventListener('aframeready', listenerSpy);

assert.ok(listenerSpy.notCalled);
readyState.emitReady();

setTimeout(function () {
assert.ok(listenerSpy.calledOnce);
assert.ok(readyState.canInitializeElements);
done();
});
});

test('emits aframeready event only once', function (done) {
var listenerSpy = this.sinon.spy();
document.addEventListener('aframeready', listenerSpy);

assert.ok(listenerSpy.notCalled);
// Calling emitReady multiple times should result in only one event being emitted.
readyState.emitReady();
readyState.emitReady();

setTimeout(function () {
assert.ok(listenerSpy.calledOnce);
assert.ok(readyState.canInitializeElements);

// Calling again after the event fired should not emit.
readyState.emitReady();
setTimeout(function () {
// Assert total count is still only once.
assert.ok(listenerSpy.calledOnce);
assert.ok(readyState.canInitializeElements);
done();
});
});
});
});
});

0 comments on commit 4a89bb6

Please sign in to comment.