improve song preview system
This commit is contained in:
@@ -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);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<a-mixin id="textFont" text="font: exo2semibold; letterSpacing: -1"></a-mixin>
|
||||
</a-assets>
|
||||
|
||||
<a-entity id="container" bind__recenter="enabled: menu.active">
|
||||
<a-entity id="container" bind__recenter="enabled: menu.active" recenter="target: #menu">
|
||||
{% include './templates/environment.html' %}
|
||||
{% include './templates/gameUi.html' %}
|
||||
{% include './templates/menu.html' %}
|
||||
|
||||
Reference in New Issue
Block a user