diff --git a/src/html/index.html b/src/html/index.html
index a746c22..d592826 100644
--- a/src/html/index.html
+++ b/src/html/index.html
@@ -55,7 +55,7 @@
-
+
@@ -63,7 +63,7 @@
-
+
0:00
diff --git a/src/js/index.js b/src/js/index.js
index e660d2b..8a815ed 100644
--- a/src/js/index.js
+++ b/src/js/index.js
@@ -129,8 +129,6 @@ for (let i = 0; i < 2; i++) {
controller.standingMatrix = controls.getStandingMatrix();
scene.add(controller);
controllers.push(controller);
-
- // controller.addEventListener('thumbpaddown', toggleEditing);
}
/*
@@ -259,6 +257,7 @@ light.shadow.camera.near = 1;
scene.add(new THREE.AmbientLight(0x666666));
+const playButton = document.getElementById('play');
const timecode = document.getElementById('timecode');
const editingCheckbox = document.getElementById('editing');
@@ -298,18 +297,20 @@ function updateButtons() {
document.getElementById('edit-buttons').style.display = isEditing ? '' : 'none';
document.getElementById('sound-effects').style.display = isEditing ? '' : 'none';
document.getElementById('editing-label').style.display = puppetShow.isCreator ? '' : 'none';
- document.getElementById('play').disabled = !puppetShow.ready;
+
+ playButton.disabled = !puppetShow.ready;
+ playButton.innerText = puppetShow.playing ? 'Pause' : 'Play';
if (isEditing) {
const recordButton = document.getElementById('record');
recordButton.disabled = !(puppetShowRecorder && puppetShowRecorder.ready);
if (puppetShowRecorder && puppetShowRecorder.recording) {
- recordButton.innerHTML = 'Stop';
+ recordButton.innerText = 'Stop';
} else if (puppetShow.duration === 0) {
- recordButton.innerHTML = 'Record';
+ recordButton.innerText = 'Record';
} else {
- recordButton.innerHTML = 'Reset';
+ recordButton.innerText = 'Reset';
}
}
}
@@ -385,7 +386,7 @@ function initializeEditor() {
if (puppetShowRecorder.recording) {
puppetShowRecorder.recordEvent('sound', {
name
- });
+ }, null, effect.duration);
}
});
});
@@ -420,10 +421,22 @@ function updateEditingState() {
updateButtons();
}
-// function toggleEditing() {
-// isEditing = !isEditing && puppetShow.isCreator;
-// updateEditingState();
-// }
+function togglePlaying() {
+ if (puppetShowRecorder && puppetShowRecorder.recording) {
+ return;
+ }
+
+ if (puppetShow.playing) {
+ puppetShow.pause();
+ } else {
+ puppetShow.play();
+ }
+}
+
+function toggleEditing() {
+ isEditing = !isEditing && puppetShow.isCreator;
+ updateEditingState();
+}
function startEditing() {
isEditing = puppetShow.isCreator;
@@ -453,10 +466,32 @@ puppetShow
console.log('error loading puppet show', id);
// todo: clear stage, force redraw and report error
})
+ .on('play', updateButtons)
+ .on('pause', updateButtons)
.on('ready', updateButtons)
- .on('unready', updateButtons);
+ .on('unready', updateButtons)
+ .on('event', event => {
+ if (puppetShowRecorder && puppetShowRecorder.recording) {
+ return;
+ }
+
+ if (event.type === 'puppet') {
+ const puppet = puppets[event.index];
+ if (!puppet) {
+ console.warn('Puppet index out of range', event);
+ return;
+ }
+
+ puppet.position.copy(event.params.position);
+ const rot = event.params.rotation;
+ puppet.rotation.set(rot.x, rot.y, rot.z);
+ // todo: move this out somewhere
+ puppet.visible = true;
+ return;
+ }
+ });
// Request animation frame loop function
let vrDisplay = null;
@@ -468,6 +503,7 @@ function animate(timestamp) {
// Update VR headset position and apply to camera.
controls.update();
+ const isRecording = puppetShowRecorder && puppetShowRecorder.recording;
controllers.forEach((c, i) => {
c.update();
@@ -485,7 +521,7 @@ function animate(timestamp) {
puppet.visible = true;
}
- if (puppet) {
+ if (puppet && !puppetShow.playing) {
// apply standing matrix
c.matrix.decompose(c.position, c.quaternion, c.scale);
@@ -497,11 +533,10 @@ function animate(timestamp) {
puppet.position.clamp(stageBounds.min, stageBounds.max);
// todo: constrain puppet on all sides, not just bottom
- if (puppetShowRecorder && puppetShowRecorder.recording) {
+ if (isRecording) {
const pos = puppet.position;
const rot = puppet.rotation;
puppetShowRecorder.recordEvent('puppet', {
- puppet: i,
position: {
x: pos.x,
y: pos.y,
@@ -512,12 +547,16 @@ function animate(timestamp) {
y: rot.y,
z: rot.z
}
- });
+ }, i);
}
}
}
});
+ if (!isRecording) {
+ puppetShow.update();
+ }
+
// render shadows once per cycle (not for each eye)
renderer.shadowMap.needsUpdate = true;
@@ -539,11 +578,12 @@ function animate(timestamp) {
renderer.render(scene, windowCamera);
}
- // temp
+ // todo: format time properly w/ duration
+ // todo: show progress bar?
if (puppetShowRecorder && puppetShowRecorder.recording) {
- // todo: format time properly
timecode.innerText = puppetShowRecorder.currentTime.toFixed(2);
- // todo: if in playback mode, show play time
+ } else {
+ timecode.innerText = puppetShow.currentTime.toFixed(2);
}
// Keep looping.
@@ -634,14 +674,15 @@ window.addEventListener('keydown', event => {
vrDisplay.exitPresent();
}
}
- // if (event.keyCode === 32) { // space
- // toggleEditing();
- // } else
- if (event.keyCode === 86) { // v
+ if (event.keyCode === 32) { // space
+ togglePlaying();
+ } else if (event.keyCode === 86) { // v
togglePreview();
}
}, true);
+playButton.addEventListener('click', togglePlaying);
+
// load from URL or create a new one
const showIdResults = /^#([a-z0-9\-_]+)/i.exec(window.location.hash);
if (showIdResults && showIdResults[1]) {
@@ -661,4 +702,9 @@ editingCheckbox.addEventListener('change', () => {
updateEditingState();
});
+controllers.forEach(controller => {
+ controller.addEventListener('triggerdown', toggleEditing);
+ controller.addEventListener('thumbpaddown', togglePlaying);
+});
+
updateEditingState();
\ No newline at end of file
diff --git a/src/js/puppet-show-recorder.js b/src/js/puppet-show-recorder.js
index 4261818..3921870 100644
--- a/src/js/puppet-show-recorder.js
+++ b/src/js/puppet-show-recorder.js
@@ -182,6 +182,8 @@ function PuppetShowRecorder(options) {
return;
}
+ puppetShow.pause();
+
recording = false;
endTime = now();
audioRecorder.stop();
@@ -202,6 +204,9 @@ function PuppetShowRecorder(options) {
// return;
// }
+ puppetShow.pause();
+ puppetShow.rewind();
+
if (audioRecorder) {
audioRecorder.clear();
}
@@ -214,16 +219,12 @@ function PuppetShowRecorder(options) {
this.emit('reset');
};
- this.recordEvent = (eventType, params, time) => {
+ this.recordEvent = (eventType, params, index, dur) => {
if (!enabled) {
return;
}
- if (recording || isNaN(time)) {
- time = this.currentTime;
- }
-
- puppetShow.addEvent(eventType, params, time);
+ puppetShow.addEvent(eventType, params, index, dur, this.currentTime);
};
// todo: allow querying of recording devices
diff --git a/src/js/puppet-show.js b/src/js/puppet-show.js
index 0cb03b3..315bd90 100644
--- a/src/js/puppet-show.js
+++ b/src/js/puppet-show.js
@@ -21,9 +21,10 @@ todo: replace with production credentials
todo: move metadata into a sub-structure
*/
-const eventEmitter = require('event-emitter');
+import now from './now';
+import eventEmitter from 'event-emitter';
+import firebase from 'firebase';
-const firebase = require('firebase');
const ServerValue = firebase.database.ServerValue;
firebase.initializeApp({
apiKey: 'AIzaSyCkvi50P1OfJTHTw0xs4G8D_ca6C8Bv2z4',
@@ -112,11 +113,18 @@ function PuppetShow(options) {
let ready = false; // ready = ready to play
let duration = 0;
let assetsToLoad = 0;
+
+ // playback state
+ let playStartTime = 0;
+ let playEndTime = 0;
+ let playing = 0;
+ let lastUpdateTime = 0;
+ let playEventIndex = 0;
+ const currentEventsByType = new Map();
+ const previousEventsByType = new Map();
/*
todo:
- set/get methods for metadata (arbitrary key/value)
- - methods for reading/creating/deleting events
- - method for full reset/erase (should have confirmation in UI)
- track status of unsaved data and fire events accordingly
*/
@@ -158,12 +166,9 @@ function PuppetShow(options) {
title: '', // todo: set random words if not provided?
// todo: any additional metadata
- // todo: see if Firebase can set time stamps on server?
createTime: ServerValue.TIMESTAMP,
modifyTime: ServerValue.TIMESTAMP,
creator: userId
-
- // todo: empty lists for assets and events (or have firebase do it?)
});
audioAssetsRef = audioStorageRef.child(showId);
@@ -189,8 +194,6 @@ function PuppetShow(options) {
- reset metadata
- disable saving to or loading from Firebase
- stop playing (if we handle playback in here?)
- - remove all events from list
- - unload any audio or other media
*/
audioAssets.clear();
@@ -324,6 +327,9 @@ function PuppetShow(options) {
return;
}
+ this.pause();
+ this.rewind();
+
// clear events and assets from local memory
audioAssets.clear();
events.length = 0;
@@ -343,7 +349,7 @@ function PuppetShow(options) {
*/
};
- this.addEvent = (type, params, time) => {
+ this.addEvent = (type, params, index, dur, time) => {
if (!showRef) {
return;
}
@@ -359,11 +365,20 @@ function PuppetShow(options) {
params
};
+ if (index !== undefined) {
+ event.index = index;
+ }
+
+ if (dur > 0) {
+ event.duration = dur;
+ }
+
events.push(event);
showRef.child('events').push(event);
- duration = Math.max(duration, time);
+ duration = Math.max(duration, time + (dur || 0));
showRef.child('duration').set(duration);
+ showRef.child('modifyTime').set(ServerValue.TIMESTAMP);
};
this.addAudio = (encodedBlob, time) => {
@@ -435,6 +450,7 @@ function PuppetShow(options) {
console.log('saved audio file', id, snapshot);
});
+ // showRef.child('modifyTime').set(ServerValue.TIMESTAMP);
// todo: add to list of events
if (wasReady) {
@@ -442,6 +458,90 @@ function PuppetShow(options) {
}
};
+ this.play = () => {
+ if (!ready) {
+ console.error('Cannot play show. Not fully loaded yet.');
+ return;
+ }
+
+ if (playing) {
+ return;
+ }
+
+ if (this.currentTime >= duration) {
+ this.rewind();
+ }
+
+ playing = true;
+ playStartTime = now();
+
+ // todo: adjust playStartTime for resuming
+
+ this.emit('play');
+ this.update();
+ };
+
+ this.pause = () => {
+ if (!playing) {
+ return;
+ }
+
+ playEndTime = now();
+ playing = false;
+
+ this.emit('pause');
+ this.update();
+ };
+
+ this.rewind = () => {
+ playStartTime = playEndTime = now();
+ // currentEventsByType.clear();
+ this.update();
+ };
+
+ this.update = () => {
+ // find any current events of each type/index and fire an event
+ // we assume the event handler will take care of ending the event
+ const currentTime = this.currentTime;
+ let index = currentTime >= lastUpdateTime ? playEventIndex : 0;
+
+ currentEventsByType.forEach(map => map.clear());
+
+ while (index < events.length) {
+ const event = events[index];
+ if (event.time > currentTime) {
+ break;
+ }
+
+ if (event.duration) {
+ // events with durations can happen simultaneously
+ this.emit('event', event);
+ } else {
+ // events w/o duration are one at a time, so only fire the latest
+ const type = event.type;
+ let currentEventsByIndex = currentEventsByType.get(type);
+ if (!currentEventsByIndex) {
+ currentEventsByIndex = new Map();
+ currentEventsByType.set(type, currentEventsByIndex);
+ }
+ currentEventsByIndex.set(event.index, event);
+ }
+ // const previous = previousEventsByIndex.get(event.index || null);
+ index++;
+ }
+
+ currentEventsByType.forEach(map => {
+ map.forEach(event => this.emit('event', event));
+ });
+
+ playEventIndex = index;
+ lastUpdateTime = currentTime;
+
+ if (currentTime >= duration) {
+ this.pause();
+ }
+ };
+
Object.defineProperties(this, {
id: {
get: () => showId
@@ -467,6 +567,14 @@ function PuppetShow(options) {
events: {
value: events
},
+
+ playing: {
+ get: () => playing
+ },
+ currentTime: {
+ get: () => Math.min(duration, ((playing ? now() : playEndTime) - playStartTime) / 1000)
+ },
+
title: {
get: () => title,
set: newTitle => {
diff --git a/src/js/sound-effect.js b/src/js/sound-effect.js
index 9953417..25c62cc 100644
--- a/src/js/sound-effect.js
+++ b/src/js/sound-effect.js
@@ -15,6 +15,7 @@ function SoundEffect(options) {
xhr.onload = () => {
context.decodeAudioData(xhr.response, decodedBuffer => {
buffer = decodedBuffer;
+ this.duration = buffer.duration;
console.log('loaded buffer', src, buffer);
});
};
@@ -60,6 +61,8 @@ function SoundEffect(options) {
};
this.name = options.name || 'Sound';
+
+ this.duration = NaN;
}
export default SoundEffect;
\ No newline at end of file