diff --git a/src/components/recenter.js b/src/components/recenter.js
index 0d7d0e2..800e6f8 100644
--- a/src/components/recenter.js
+++ b/src/components/recenter.js
@@ -3,10 +3,11 @@
*/
AFRAME.registerComponent('recenter', {
schema: {
- enabled: {default: true}
+ enabled: {default: true},
+ target: {default: ''}
},
- init: function() {
+ init: function () {
var sceneEl = this.el.sceneEl;
this.matrix = new THREE.Matrix4();
this.frustum = new THREE.Frustum();
@@ -16,32 +17,22 @@ AFRAME.registerComponent('recenter', {
this.menuPosition = new THREE.Vector3();
this.recenter = this.recenter.bind(this);
this.checkInViewAfterRecenter = this.checkInViewAfterRecenter.bind(this);
+ this.target = document.querySelector(this.data.target);
+
// Delay to make sure we have a valid pose.
- sceneEl.addEventListener('enter-vr', () => {
- setTimeout(() => { this.recenter(); }, 100);
- });
+ sceneEl.addEventListener('enter-vr', () => setTimeout(this.recenter, 100));
// User can also recenter the menu manually.
- sceneEl.addEventListener('menudown', () => {
- if (!this.data.enabled) { return; }
- this.recenter();
- });
- sceneEl.addEventListener('thumbstickdown', () => {
- if (!this.data.enabled) { return; }
- this.recenter();
- });
+ sceneEl.addEventListener('menudown', this.recenter);
+ sceneEl.addEventListener('thumbstickdown', this.recenter);
+ window.addEventListener('vrdisplaypresentchange', this.recenter);
},
- recenter: function(skipCheck) {
+ recenter: function () {
var euler = this.euler;
- euler.setFromRotationMatrix(
- this.el.sceneEl.camera.el.object3D.matrixWorld,
- 'YXZ'
- );
+ if (!this.data.enabled) { return; }
+ euler.setFromRotationMatrix(this.el.sceneEl.camera.el.object3D.matrixWorld, 'YXZ');
this.el.object3D.rotation.y = euler.y + this.rotationOffset;
// Check if the menu is in camera frustum in next tick after a frame has rendered.
- if (skipCheck) {
- return;
- }
setTimeout(this.checkInViewAfterRecenter, 0);
},
@@ -50,30 +41,36 @@ AFRAME.registerComponent('recenter', {
* Check if the menu is in the camera frustum after recenter it to
* decide if we apply an offset or not.
*/
- checkInViewAfterRecenter: function() {
- var camera = this.el.sceneEl.camera;
- var frustum = this.frustum;
- var menu = this.el;
- var menuPosition = this.menuPosition;
- camera.updateMatrix();
- camera.updateMatrixWorld();
- frustum.setFromMatrix(
- this.matrix.multiplyMatrices(
- camera.projectionMatrix,
- camera.matrixWorldInverse
- )
- );
- menu.object3D.updateMatrixWorld();
- menuPosition.setFromMatrixPosition(menu.object3D.matrixWorld);
- if (frustum.containsPoint(menuPosition)) {
- return;
- }
- this.rotationOffset = this.rotationOffset === 0 ? Math.PI : 0;
- // Recenter again with the new offset.
- this.recenter(true);
- },
+ checkInViewAfterRecenter: (function () {
+ var bottomVec3 = new THREE.Vector3();
+ var topVec3 = new THREE.Vector3();
- remove: function() {
+ return function () {
+ var camera = this.el.sceneEl.camera;
+ var frustum = this.frustum;
+ var menuPosition = this.menuPosition;
+
+ camera.updateMatrix();
+ camera.updateMatrixWorld();
+ frustum.setFromMatrix(this.matrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
+
+ // Check if menu position (and its bounds) are within the frustum.
+ // Check bounds in case looking angled up or down, rather than menu central.
+ menuPosition.setFromMatrixPosition(this.target.object3D.matrixWorld);
+ bottomVec3.copy(menuPosition).y -= 3;
+ topVec3.copy(menuPosition).y += 3;
+
+ if (frustum.containsPoint(menuPosition) ||
+ frustum.containsPoint(bottomVec3) ||
+ frustum.containsPoint(topVec3)) { return; }
+
+ this.rotationOffset = this.rotationOffset === 0 ? Math.PI : 0;
+ // Recenter again with the new offset.
+ this.recenter();
+ };
+ })(),
+
+ remove: function () {
this.el.sceneEl.removeEventListener('enter-vr', this.recenter);
- },
+ }
});
diff --git a/src/components/song-preview.js b/src/components/song-preview.js
index 777588c..84dfdcc 100644
--- a/src/components/song-preview.js
+++ b/src/components/song-preview.js
@@ -1,5 +1,8 @@
var utils = require('../utils');
+/**
+ * Song preview when search result selected with smart logic for preloading.
+ */
AFRAME.registerComponent('song-preview-system', {
schema: {
selectedChallengeId: {type: 'string'}
@@ -8,6 +11,7 @@ AFRAME.registerComponent('song-preview-system', {
init: function () {
this.audio = null;
this.audioStore = {};
+ this.preloadedAudioIds = [];
this.preloadQueue = [];
// anime.js animation to fade in volume.
@@ -28,6 +32,8 @@ AFRAME.registerComponent('song-preview-system', {
update: function (oldData) {
const data = this.data;
+ const preloadQueue = this.preloadQueue;
+
if (oldData.selectedChallengeId &&
oldData.selectedChallengeId !== data.selectedChallengeId) {
this.stopSong();
@@ -35,31 +41,79 @@ AFRAME.registerComponent('song-preview-system', {
if (data.selectedChallengeId &&
oldData.selectedChallengeId !== data.selectedChallengeId) {
+ if (!this.preloadedAudioIds.includes(data.selectedChallengeId)) {
+ // If not yet preloaded, pause the preload queue until this song is loaded.
+ console.log(`[song-preview] Prioritizing loading of ${data.selectedChallengeId}`);
+ this.loadingChallengeId = data.selectedChallengeId;
+ this.audioStore[data.selectedChallengeId].addEventListener('loadeddata', () => {
+ console.log(`[song-preview] Finished load of priority ${data.selectedChallengeId}`);
+ this.preloadedAudioIds.push(data.selectedChallengeId);
+ this.loadingChallengeId = '';
+ // Resume preloading queue.
+ if (preloadQueue.length) {
+ console.log(`[song-preview] Resuming queue with ${preloadQueue[0].challengeId}`);
+ this.preloadMetadata(preloadQueue[0]);
+ }
+ });
+
+ // Remove from preload queue.
+ for (let i = 0; i < preloadQueue.length; i++) {
+ if (preloadQueue[i].challengeId === data.selectedChallengeId) {
+ preloadQueue.splice(i, 1);
+ break;
+ }
+ }
+ }
+
this.playSong(data.selectedChallengeId);
}
},
- preloadSong: function (challengeId, previewStartTime) {
+ queuePreloadSong: function (challengeId, previewStartTime) {
+ if (this.audioStore[challengeId]) { return; }
const audio = document.createElement('audio');
audio.currentTime = previewStartTime;
- audio.src = utils.getS3FileUrl(challengeId, 'song.ogg');
audio.volume = 0;
this.audioStore[challengeId] = audio;
- if (this.preloadQueue.length === 0) {
- this.preloadMetadata(audio);
+ let src = utils.getS3FileUrl(challengeId, 'song.ogg');
+ if (!this.currentLoadingId) {
+ this.preloadMetadata({
+ audio: audio,
+ challengeId: challengeId,
+ src: src
+ });
} else {
- this.preloadQueue.push(audio);
+ this.preloadQueue.push({
+ audio: audio,
+ challengeId: challengeId,
+ src: src
+ });
}
},
- preloadMetadata: function (audio) {
+ /**
+ * Preload metadata of audio file for quick play.
+ * Set `src` and `preload`.
+ * A preload queue is set up so we only preload one at a time to not bog down
+ * the network. If a song is selected to preview, we can bump it to the front of the
+ * queue.
+ */
+ preloadMetadata: function (preloadItem) {
+ const audio = preloadItem.audio;
+ console.log(`[song-preview] Preloading song preview ${preloadItem.challengeId}`);
audio.addEventListener('loadedmetadata', () => {
- if (this.preloadQueue.length) {
- this.preloadMetadata(this.preloadQueue[0]);
+ console.log(`[song-preview] Finished preloading song preview ${preloadItem.challengeId}`);
+ this.preloadedAudioIds.push(preloadItem.challengeId);
+ this.currentLoadingId = '';
+ console.log(`[song-preview] ${this.preloadQueue.length} in queue`);
+ if (this.preloadQueue.length && !this.loadingChallengeId) {
+ this.preloadMetadata(this.preloadQueue.shift());
}
});
audio.preload = 'metadata';
+ audio.src = preloadItem.src;
+ this.currentLoadingId = preloadItem.challengeId;
},
stopSong: function () {
@@ -71,6 +125,7 @@ AFRAME.registerComponent('song-preview-system', {
playSong: function (challengeId) {
if (!challengeId) { return; }
this.audio = this.audioStore[challengeId];
+ this.audio.src = utils.getS3FileUrl(challengeId, 'song.ogg');
this.audio.volume = 0;
this.volumeTarget.volume = 0;
this.audio.play();
@@ -80,14 +135,13 @@ AFRAME.registerComponent('song-preview-system', {
clearSong: function (challengeId) {
let audio = this.audioStore[challengeId];
audio.preload = 'none';
- delete this.audioStore[challengeId];
- audio = null;
+ // Assume that paginating, clear the queue.
this.preloadQueue.length = 0;
}
});
/**
- * Handle song preview play, pause, fades.
+ * Data component attached to search result for song preview system.
*/
AFRAME.registerComponent('song-preview', {
schema: {
@@ -96,7 +150,7 @@ AFRAME.registerComponent('song-preview', {
},
play: function () {
- this.el.sceneEl.components['song-preview-system'].preloadSong(
+ this.el.sceneEl.components['song-preview-system'].queuePreloadSong(
this.data.challengeId, this.data.previewStartTime
);
},
diff --git a/src/index.html b/src/index.html
index 77e47f7..6780514 100644
--- a/src/index.html
+++ b/src/index.html
@@ -23,7 +23,7 @@
-
+
{% include './templates/environment.html' %}
{% include './templates/gameUi.html' %}
{% include './templates/menu.html' %}