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