hooking up loading, flow, audio

This commit is contained in:
Kevin Ngo
2018-07-21 10:21:30 +02:00
parent 2c19dfe50a
commit 3abd6ba9cf
17 changed files with 221 additions and 170 deletions

View File

@@ -10,16 +10,23 @@ AFRAME.registerComponent('beat-loader', {
},
update: function () {
var challengeId = this.data.challengeId;
if (!this.data.challengeId || !this.data.difficulty) { return; }
this.loadBeats(this.data.challengeId, this.data.difficulty);
},
/**
* XHR.
*/
loadBeats: function (id, difficulty) {
var el = this.el;
var xhr;
if (!challengeId || !diffjculty) { return; }
// Load beats.
let url = utils.getS3FileUrl(this.data.challengeId, `${this.data.difficulty}.json`);
xhr = new XMLHttpRequest();
el.emit('beatloaderstart');
xhr.open('GET', utils.getS3FileUrl(challengeId, `${this.data.difficulty}.json`));
console.log(`Fetching ${url}...`);
xhr.open('GET', url);
xhr.addEventListener('load', () => {
this.handleBeats(JSON.parse(xhr.responseText));
});
@@ -30,16 +37,7 @@ AFRAME.registerComponent('beat-loader', {
* TODO: Load the beat data into the game.
*/
handleBeats: function (beatData) {
var el = this.el;
history.pushState(
'',
challenge.songName,
updateQueryParam(window.location.href, 'challenge', this.data.challengeId)
);
document.title = `Super Saber - ${challenge.songName}`;
el.emit('beatloaderfinish');
this.el.sceneEl.emit('beatloaderfinish', beatData, false);
console.log('Finished loading challenge data.');
},
});

View File

@@ -1,6 +1,10 @@
AFRAME.registerComponent('console-shortcuts', {
play: function() {
play: function () {
window.$ = val => document.querySelector(val);
window.$$ = val => document.querySelectorAll(val);
window.$$$ = val => document.querySelector(`[${val}]`).getAttribute(val);
window.$$$$ = val => document.querySelector(`[${val}]`).components[val];
window.scene = this.el;
window.state = this.el.systems.state.state;
},
}
});

View File

@@ -1,16 +0,0 @@
AFRAME.registerComponent('discolight', {
schema: {
color: { type: 'color' },
speed: { default: 1.0 },
},
init: function() {
this.color = new THREE.Color(this.data.color);
this.hsl = this.color.getHSL();
},
tick: function(time, delta) {
this.hsl.h += delta * 0.0001 * this.data.speed;
if (this.hsl.l > 1.0) this.hsl.l = 0.0;
this.color.setHSL(this.hsl.h, this.hsl.s, this.hsl.l);
this.el.setAttribute('light', { color: this.color.getHex() });
},
});

View File

@@ -1,14 +0,0 @@
AFRAME.registerComponent('discotube', {
schema: {
speedX: { default: 1.0 },
speedY: { default: 0.1 },
},
init: function() {
this.material = this.el.object3D.children[0].material;
},
tick: function(time, delta) {
if (this.material == null) return;
this.material.map.offset.x -= delta * 0.0001 * this.data.speedX;
this.material.map.offset.y -= delta * 0.0001 * this.data.speedY;
},
});

20
src/components/history.js Normal file
View File

@@ -0,0 +1,20 @@
/**
* Update window title and history.
*/
AFRAME.registerComponent('history', {
schema: {
challengeId: {type: 'string'},
songName: {type: 'string'},
songSubName: {type: 'string'}
},
update: function () {
const data = this.data;
history.pushState(
'',
data.songName,
updateQueryParam(window.location.href, 'challenge', data.challengeId)
);
document.title = `Super Saber - ${data.songName}`;
}
});

View File

@@ -1,26 +0,0 @@
/**
* Play audio element on event.
*/
AFRAME.registerComponent('play-audio', {
schema: {
audio: { type: 'string' },
event: { type: 'string' },
volume: { type: 'number', default: 1 },
},
multiple: true,
init: function() {
var audio;
audio = document.querySelector(this.data.audio);
audio.volume = this.data.volume;
this.el.addEventListener(this.data.event, evt => {
if (!audio.paused) {
audio.pause();
audio.currentTime = 0;
}
audio.play();
});
},
});

View File

@@ -1,13 +0,0 @@
AFRAME.registerComponent('play-button', {
init: function() {
var el = this.el;
el.addEventListener('click', () => {
el.sceneEl.emit('playbuttonclick');
});
el.sceneEl.addEventListener('youtubefinished', evt => {
el.object3D.visible = false;
});
},
});

View File

@@ -0,0 +1,43 @@
var SoundPool = require('../lib/soundpool');
AFRAME.registerSystem('play-sound', {
init: function () {
this.lastSoundPlayed = '';
this.lastSoundPlayedTime = 0;
this.pools = {};
},
createPool: function (sound, volume) {
if (this.pools[sound]) { return; }
this.pools[sound] = new SoundPool(sound, volume);
},
playSound: function (sound, volume) {
this.createPool(sound, volume);
this.pools[sound].play();
this.lastSoundPlayed = sound;
this.lastSoundTime = this.el.time;
}
});
/**
* Play sound on event.
*/
AFRAME.registerComponent('play-sound', {
schema: {
enabled: {default: true},
event: {type: 'string'},
sound: {type: 'string'},
volume: {type: 'number', default: 1}
},
multiple: true,
init: function () {
this.el.addEventListener(this.data.event, evt => {
if (!this.data.enabled) { return; }
this.system.playSound(this.data.sound, this.data.volume);
});
}
});

View File

@@ -31,6 +31,7 @@ AFRAME.registerComponent('preview-song', {
},
update: function (oldData) {
// Stop.
if (oldData.challengeId && !this.data.challengeId) {
if (this.animation) { this.animation.pause(); }
this.audio.pause();

View File

@@ -14,9 +14,7 @@ AFRAME.registerComponent('recenter', {
this.checkInViewAfterRecenter = this.checkInViewAfterRecenter.bind(this);
// Delay to make sure we have a valid pose.
sceneEl.addEventListener('enter-vr', () => {
setTimeout(() => {
this.recenter();
}, 100);
setTimeout(() => { this.recenter(); }, 100);
});
// User can also recenter the menu manually.
sceneEl.addEventListener('menudown', () => {
@@ -49,7 +47,7 @@ AFRAME.registerComponent('recenter', {
checkInViewAfterRecenter: function() {
var camera = this.el.sceneEl.camera;
var frustum = this.frustum;
var menu = document.querySelector('#menu');
var menu = this.el;
var menuPosition = this.menuPosition;
camera.updateMatrix();
camera.updateMatrixWorld();

39
src/components/song.js Normal file
View File

@@ -0,0 +1,39 @@
const utils = require('../utils');
/**
* Active challenge song / audio.
*/
AFRAME.registerComponent('song', {
schema: {
challengeId: {default: ''},
isPlaying: {default: false}
},
init: function () {
// Use audio element for audioanalyser.
this.audio = document.createElement('audio');
this.audio.setAttribute('id', 'song');
this.el.sceneEl.appendChild(this.audio);
},
update: function (oldData) {
var el = this.el;
var data = this.data;
// Changed challenge.
if (data.challengeId !== oldData.challengeId) {
let songUrl = utils.getS3FileUrl(data.challengeId, 'song.ogg');
this.audio.currentTime = 0;
this.audio.src = data.challengeId ? songUrl : '';
console.log(`Playing ${songUrl}...`);
}
// Keep playback state up to date.
if ((data.isPlaying && data.challengeId) && this.audio.paused) {
this.audio.play();
return;
} else if ((!data.isPlaying || !data.challengeId) && !this.audio.paused) {
this.audio.pause();
}
}
});

View File

@@ -5,29 +5,25 @@
<script src="vendor/aframe.dev.js"></script>
<script src="build/build.js"></script>
</head>
<body>
<audio id="hoverSound" src="/assets/sounds/hover.ogg"></audio>
<a-scene bind__beat-loader="challengeId: challenge.id"
bind__preview-song="challengeId: menuSelectedChallenge.id"
challenge-loader
<a-scene bind__beat-loader="challengeId: challenge.id; difficulty: challenge.difficulty"
bind__preview-song="challengeId: menuSelectedChallenge.id; previewStartTime: menuSelectedChallenge.previewStartTime"
bind__song="challengeId: challenge.id; isPlaying: !menu.active && !challenge.isLoading"
console-shortcuts
search>
<a-assets timeout="10000">
<a-mixin id="raycaster" raycaster="objects: [raycastable]; far: 2"></a-mixin>
<audio id="hoverSound" src="/assets/sounds/hover.ogg"></audio>
<img id="gridImg" src="assets/img/grid.png">
<img id="playImg" src="assets/img/play.png">
<img id="resultImg" src="assets/img/result.png">
<img id="gridImg" src="assets/img/grid.png">
<img id="sliceImg" src="assets/img/slice.png">
<a-mixin id="slice" slice9="color: #050505; src: #sliceImg; left: 50; right: 52; top: 50; bottom: 52; padding: 0.04"></a-mixin>
<a-mixin id="font" text="font: exo2semibold; letterSpacing: -1"></a-mixin>
<a-mixin id="raycaster" raycaster="objects: [raycastable]; far: 2"></a-mixin>
<a-mixin id="slice" slice9="color: #050505; src: #sliceImg; left: 50; right: 52; top: 50; bottom: 52; padding: 0.04"></a-mixin>
<a-mixin id="textFont" text="font: exo2semibold; letterSpacing: -1"></a-mixin>
</a-assets>
<a-entity id="dustParticles" particle-system="maxAge: 100; positionSpread: 5 5 5; type: 2; rotationAxis: x; rotationAngle: 0.001; accelerationValue: 0.001 0.001 0.001; accelerationSpread: 0.001 0.001 0.001; velocityValue: 0.001 0.001 0.001; velocitySpread: 0.0125 0.0125 0.0125; color: #ffffff; size: 0.1; opacity: 0.85; direction: 0; particleCount: 150; texture: assets/img/smokeparticle.png"></a-entity>
<a-entity id="container">
<a-entity id="container" recenter>
{% include './templates/environment.html' %}
{% include './templates/gameUi.html' %}
{% include './templates/menu.html' %}
@@ -39,7 +35,8 @@
<a-entity id="rightHand" controller="hand: right"></a-entity>
</a-entity>
<a-entity id="mouseCursor" mixin="raycaster" cursor="rayOrigin: mouse" debug-cursor bind__isPlaying="!inVR"></a-entity>
<a-entity id="mouseCursor" mixin="raycaster" cursor="rayOrigin: mouse" debug-cursor
bind__raycaster="enabled: !inVR"></a-entity>
</a-scene>
</body>
</html>

View File

@@ -1,22 +1,27 @@
module.exports = function SoundPool (src, volume, size) {
module.exports = function SoundPool (src, volume) {
var currSound = 0;
var i;
var pool = [];
var sound;
for (i = 0; i < size; i++) {
sound = new Audio(src);
sound.volume = volume;
sound.load();
pool.push(sound);
}
sound = new Audio(src);
sound.volume = volume;
pool.push(sound);
return {
play: function () {
// Dynamic size pool.
if (pool[currSound].currentTime !== 0 || !pool[currSound].ended) {
sound = new Audio(src);
sound.volume = volume;
pool.push(sound);
currSound++;
}
if (pool[currSound].currentTime === 0 || pool[currSound].ended) {
pool[currSound].play();
}
currSound = (currSound + 1) % size;
currSound = (currSound + 1) % pool.length;
}
};
};

View File

@@ -9,79 +9,95 @@ AFRAME.registerState({
challenge: {
author: '',
difficulty: '',
downloads: '',
downloadsText: '',
id: AFRAME.utils.getUrlParameter('challenge'),
image: '',
isLoading: false,
songName: '',
songSubName: '',
songSubName: ''
},
inVR: false,
maxStreak: 0,
menu: {
active: true
active: true,
playButtonText: 'Play'
},
menuDifficulties: [],
menuSelectedChallenge: {
author: '',
difficulty: '',
downloads: '',
downloadsText: '',
id: '',
image: ''
image: '',
songName: '',
songSubName: ''
},
score: {
maxStreak: 0,
score: 0,
streak: 0
},
playButtonText: 'Play',
score: 0,
scoreText: '',
// screen: keep track of layers or depth. Like breadcrumbs.
screen: hasInitialChallenge ? 'challenge' : 'home',
screenHistory: [],
searchResults: [],
streak: 0
searchResults: []
},
handlers: {
beatloaderfinish: function (state, payload) {
beatloaderfinish: (state) => {
state.challenge.isLoading = false;
},
beatloaderstart: function (state, payload) {
beatloaderstart: (state) => {
state.challenge.isLoading = true;
},
challengeset: function (state, payload) {
state.challenge.id = payload.challengeId;
state.score = 0;
state.streak = 0;
state.maxStreak = 0;
state.menu.active = false;
state.menuSelectedChallenge.id = '';
setScreen(state, 'challenge');
},
/**
* Song clicked from menu.
*/
menuchallengeselect: function (state, id) {
menuchallengeselect: (state, id) => {
// Copy from challenge store populated from search results.
let challengeData = challengeDataStore[id];
Object.assign(state.menuSelectedChallenge, challengeData);
state.menuSelectedChallenge.id = id;
state.menuDifficulties.length = 0;
for (let i = 0; i < challengeData.difficulties.length; i++) {
state.menuDifficulties.push(challengeData.difficulties[i]);
}
state.menuSelectedChallenge.image = utils.getS3FileUrl(id, 'image.jpg');
state.menuSelectedChallenge.downloadsText = `${challengeData.downloads} Plays`;
// Choose first difficulty.
// TODO: Default and order by easiest to hardest.
state.menuSelectedChallenge.difficulty = state.menuDifficulties[0];
},
menudifficultyselect: function (state, difficulty) {
menudifficultyselect: (state, difficulty) => {
state.menuSelectedChallenge.difficulty = difficulty;
},
playbuttonclick: function (state) {
/**
* Start challenge.
* Transfer staged challenge to the active challenge.
*/
playbuttonclick: (state) => {
// Reset score.
state.score.maxStreak = 0;
state.score.score = 0;
state.score.streak = 0;
// Set challenge. `beat-loader` is listening.
Object.assign(state.challenge, state.menuSelectedChallenge);
// Reset menu.
state.menu.active = false;
state.menuSelectedChallenge.id = '';
},
/**
* Update search results. Will automatically render using `bind-for` (menu.html).
*/
searchresults: function (state, payload) {
searchresults: (state, payload) => {
var i;
state.searchResults.length = 0;
for (i = 0; i < 6; i++) {
@@ -93,21 +109,23 @@ AFRAME.registerState({
state.searchResults.__dirty = true;
},
togglemenu: function (state) {
togglemenu: (state) => {
state.menu.active = !state.menu.active;
},
'enter-vr': function (state, payload) {
'enter-vr': (state) => {
state.inVR = true;
},
'exit-vr': function (state, payload) {
'exit-vr': (state) => {
state.inVR = false;
}
},
computeState: function (state) {
state.scoreText = `Streak: ${state.streak} / Max Streak: ${state.maxStreak} / Score: ${state.score}`;
/**
* Post-process the state after each action.
*/
computeState: (state) => {
}
});

View File

@@ -1,4 +1,4 @@
<a-entity id="sky" cubemap="folder: /assets/img/env/space/"></a-entity>
<a-entity id="sky" cubemap="folder: assets/img/env/space/"></a-entity>
<a-entity id="skyFade"
bind__visible="menu.active"
@@ -20,3 +20,5 @@
<!-- Default lights -->
<a-entity light="type: ambient; color: #BBB"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.6" position="-0.5 1 1"></a-entity>
<a-entity id="dustParticles" particle-system="maxAge: 100; positionSpread: 5 5 5; type: 2; rotationAxis: x; rotationAngle: 0.001; accelerationValue: 0.001 0.001 0.001; accelerationSpread: 0.001 0.001 0.001; velocityValue: 0.001 0.001 0.001; velocitySpread: 0.0125 0.0125 0.0125; color: #ffffff; size: 0.1; opacity: 0.85; direction: 0; particleCount: 150; texture: assets/img/smokeparticle.png"></a-entity>

View File

@@ -1,36 +1,30 @@
<a-entity id="gameInfo"
bind__visible="isChallengeScreen"
bind__visible="!menu.active && !!challenge.id && !challenge.isLoading"
position="0 0 -80"
scale="100 100 100">
<a-entity mixin="textFont"
text="align:right; width:1.25; value:SCORE; color:#d6d955; anchor:right; letterSpacing:-2"
position="-0.089 0.183 -1.01"
bind__visible="!challenge.isLoading"></a-entity>
text="align: right; width: 1.25; value: SCORE; color: #d6d955; anchor: right; letterSpacing: -2"
position="-0.089 0.183 -1.01"></a-entity>
<a-entity mixin="textFont"
text="width:0.9; value:STREAK; color:#f95895; letterSpacing:-2; anchor:left"
position="0.097 0.183 -1.01"
bind__visible="!challenge.isLoading"></a-entity>
text="width: 0.9; value: STREAK; color: #f95895; letterSpacing: -2; anchor: left"
position="0.097 0.183 -1.01"></a-entity>
<a-entity mixin="textFont"
text="width:0.7; value:MAX; color:#f95895; anchor:left; letterSpacing:-2"
position="0.101 0.1 -1.01"
bind__visible="!challenge.isLoading"></a-entity>
text="width: 0.7; value: MAX; color: #f95895; anchor: left; letterSpacing: -2"
position="0.101 0.1 -1.01"></a-entity>
<a-entity id="score"
bind__text="value: score"
bind__text="value: score.score"
mixin="textFont"
text="align:right; width:2; color:#feffc1; anchor:right; letterSpacing:-2"
position="-0.08 0.132 -1"
bind__visible="!challenge.isLoading"></a-entity>
text="align: right; width: 2; color: #feffc1; anchor: right; letterSpacing: -2"
position="-0.08 0.132 -1"></a-entity>
<a-entity id="streak"
bind__text="value: streak"
bind__text="value: score.streak"
mixin="textFont"
text="width:1.25; color:#ffbdd6; anchor:left; letterSpacing:-2"
position="0.098 0.144 -1"
bind__visible="!challenge.isLoading"></a-entity>
text="width: 1.25; color: #ffbdd6; anchor: left; letterSpacing: -2"
position="0.098 0.144 -1"></a-entity>
<a-entity id="maxStreak"
bind__text="value: maxStreak"
bind__text="value: score.maxStreak"
mixin="textFont"
text="width:0.8; color:#ffbdd6; anchor:left; letterSpacing:-2"
position="0.1 0.074 -1"
bind__visible="!challenge.isLoading"></a-entity>
text="width: 0.8; color: #ffbdd6; anchor: left; letterSpacing: -2"
position="0.1 0.074 -1"></a-entity>
</a-entity>

View File

@@ -12,7 +12,7 @@
geometry="primitive: plane; width: 0.8; height: 0.1"
material="shader: flat; color: #111"
position="0 -0.13 -0.01"
play-audio="event: mouseenter; audio: #hoverSound; volume: 0.03"
play-sound="event: mouseenter; sound: #hoverSound; volume: 0.03"
animation__mouseenter="property: material.color; from: #111; to: #666; startEvents: mouseenter; pauseEvents: mouseleave; dur: 150"
animation__mouseleave="property: material.color; from: #666; to: #111; startEvents: mouseleave; pauseEvents: mouseenter; dur: 150"
raycastable></a-entity>
@@ -50,7 +50,7 @@
geometry="primitive: plane; width: 0.3; height: 0.1"
material="shader: flat; color: #111"
position="-0.4 -0.005 0"
play-audio="event: mouseenter; audio: #hoverSound; volume: 0.03"
play-sound="event: mouseenter; sound: #hoverSound; volume: 0.03"
animation__mouseenter="property: material.color; from: #111; to: #666; startEvents: mouseenter; pauseEvents: mouseleave; dur: 150"
animation__mouseleave="property: material.color; from: #666; to: #111; startEvents: mouseleave; pauseEvents: mouseenter; dur: 150"
bind-toggle__raycastable="menu.active && !!menuSelectedChallenge.id"></a-entity>
@@ -75,10 +75,11 @@
<a-entity class="menuSelectedChallengeDownloads" mixin="textFont" text="align: center; color: #FFF; wrapCount: 45" position="0 -0.09 0" bind__text="value: menuSelectedChallenge.downloadsText"></a-entity>
</a-entity>
<a-plane id="play"
<a-plane id="playButton"
play-button
play-audio="event: mouseenter; audio: #hoverSound; volume: 0.03"
play-sound="event: mouseenter; sound: #hoverSound; volume: 0.03"
position="0 -0.25 0"
proxy-event="event: click; to: a-scene; as: playbuttonclick"
material="shader: flat; src: #playImg; transparent: true; color: #BBB"
width="0.256"
height="0.128"