No pause when finished tagging. Resume where left of with tagging and playback. Two subs. Better playback duration.
This commit is contained in:
parent
9eef55db16
commit
67945cc32f
2 changed files with 154 additions and 35 deletions
30
index.html
30
index.html
|
@ -84,6 +84,22 @@
|
||||||
left: calc(50% - 150px);
|
left: calc(50% - 150px);
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#resetButton, #resumeButton{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
body.finished #resetButton{
|
||||||
|
display:inline;
|
||||||
|
}
|
||||||
|
body.finished.paused #resumeButton{
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
#nextButton{
|
||||||
|
}
|
||||||
|
body.finished #nextButton{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,20 +125,28 @@
|
||||||
<!-- contents of template will come here through the javascript -->
|
<!-- contents of template will come here through the javascript -->
|
||||||
</div>
|
</div>
|
||||||
<input type='button' value='Next image' id='nextButton'>
|
<input type='button' value='Next image' id='nextButton'>
|
||||||
|
<input type='button' value='Restart weighing' id='resetButton'>
|
||||||
|
|
||||||
</div>
|
<!-- button is needed because audio cannot auto-start without a user clicking on the page -->
|
||||||
|
<input type='button' value='Resume interview' id='resumeButton'>
|
||||||
|
|
||||||
<audio id='interview'
|
<audio id='interview'
|
||||||
src="/interview/interview-with-ellen.mp3">
|
src="/interview/interview-with-ellen.mp3">
|
||||||
|
|
||||||
<track default kind="captions"
|
<track kind="captions"
|
||||||
srclang="en"
|
srclang="en"
|
||||||
src="/interview/interview-with-ellen_v3.mp3.vtt"/>
|
src="/interview/interview-with-ellen_v3.mp3.vtt"/>
|
||||||
|
<track default kind="captions"
|
||||||
|
srclang="nl"
|
||||||
|
src="/interview/interview-with-ellen_v3.mp3 copy.vtt"/>
|
||||||
|
|
||||||
</audio>
|
</audio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id='subtitles'>
|
<div id='subtitles'>
|
||||||
|
<div id='sub_nl'></div>
|
||||||
|
<div id='sub_en'></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='workmsg'>
|
<div id='workmsg'>
|
||||||
|
|
129
parade.js
129
parade.js
|
@ -6,7 +6,8 @@ var config = {
|
||||||
distanceFactor: 20, // distance between the shapes
|
distanceFactor: 20, // distance between the shapes
|
||||||
startOffset: 0.001, // starting position
|
startOffset: 0.001, // starting position
|
||||||
shapeWidth: 100, // 8 shape of animation: width
|
shapeWidth: 100, // 8 shape of animation: width
|
||||||
shapeHeight: 1300 // 8 shape of animation: height;
|
shapeHeight: 1300, // 8 shape of animation: height;
|
||||||
|
playDuration: 16, // seconds. how long should audio play after dragging/pressing next. Something like audio duration / nr of images. Note that if everything is sorted interview will just continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// polyfill
|
// polyfill
|
||||||
|
@ -35,7 +36,6 @@ fetch(request)
|
||||||
// loaded data from json, initialise interface
|
// loaded data from json, initialise interface
|
||||||
const canvas = document.getElementById("renderCanvas");
|
const canvas = document.getElementById("renderCanvas");
|
||||||
parade = new Parade(canvas, data['image'], config);
|
parade = new Parade(canvas, data['image'], config);
|
||||||
parade.createScene();
|
|
||||||
// parade.scene.debugLayer.show();
|
// parade.scene.debugLayer.show();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -50,6 +50,7 @@ class Parade {
|
||||||
this.canvasEl = canvasEl;
|
this.canvasEl = canvasEl;
|
||||||
this.engine = new BABYLON.Engine(canvasEl, true); // create 3d engine
|
this.engine = new BABYLON.Engine(canvasEl, true); // create 3d engine
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
|
this.t = 0; // playback timer
|
||||||
|
|
||||||
// initialise image objects:
|
// initialise image objects:
|
||||||
this.images = [];
|
this.images = [];
|
||||||
|
@ -80,6 +81,7 @@ class Parade {
|
||||||
|
|
||||||
this.setupSubtitles();
|
this.setupSubtitles();
|
||||||
this.setupSorter();
|
this.setupSorter();
|
||||||
|
this.restoreTime();
|
||||||
|
|
||||||
// Watch for browser/canvas resize events
|
// Watch for browser/canvas resize events
|
||||||
window.addEventListener("resize", function () {
|
window.addEventListener("resize", function () {
|
||||||
|
@ -88,26 +90,47 @@ class Parade {
|
||||||
|
|
||||||
//Register a render loop to repeatedly render the scene
|
//Register a render loop to repeatedly render the scene
|
||||||
this.engine.runRenderLoop(this.renderLoop.bind(this));
|
this.engine.runRenderLoop(this.renderLoop.bind(this));
|
||||||
|
|
||||||
|
this.createScene();
|
||||||
|
|
||||||
|
// browsers block auto-play of audio
|
||||||
|
// if(this.imagesToSort.length < 1){
|
||||||
|
// this.play();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSubtitles() {
|
setupSubtitles() {
|
||||||
this.audioEl = document.getElementById('interview'); // audio player
|
this.audioEl = document.getElementById('interview'); // audio player
|
||||||
this.subtitleEl = document.getElementById('subtitles'); // the subtitle div
|
this.subtitleEls = {
|
||||||
this.trackEl = this.audioEl.querySelector('track'); // the subtitle track
|
'en': document.getElementById('sub_en'),
|
||||||
|
'nl': document.getElementById('sub_nl'),
|
||||||
|
};
|
||||||
|
this.trackEls = this.audioEl.querySelectorAll('track'); // the subtitle track
|
||||||
this.timeEl = document.getElementById('time'); // the subtitle track
|
this.timeEl = document.getElementById('time'); // the subtitle track
|
||||||
|
|
||||||
// set up subtitles:
|
// set up subtitles:
|
||||||
this.trackEl.addEventListener("cuechange", (event) => {
|
for(let trackEl of this.trackEls){
|
||||||
|
trackEl.addEventListener("cuechange", (event) => {
|
||||||
|
let lang = event.srcElement.srclang;
|
||||||
let cues = event.target.track.activeCues;
|
let cues = event.target.track.activeCues;
|
||||||
let content = "";
|
let content = "";
|
||||||
for(let cue of cues) {
|
for(let cue of cues) {
|
||||||
content += "<p>"+cue.text+"</p>";
|
content += "<p>"+cue.text+"</p>";
|
||||||
}
|
}
|
||||||
this.subtitleEl.innerHTML = content;
|
this.subtitleEls[lang].innerHTML = content;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
for(let track of this.audioEl.textTracks){
|
||||||
|
// by default only one (or none) is set to showing.
|
||||||
|
// for some reason setting it on trackEl doesn't work, while this does the trick.
|
||||||
|
track.mode = 'showing';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
this.audioEl.addEventListener('timeupdate', this.updateTimestring.bind(this));
|
this.audioEl.addEventListener('timeupdate', this.updateTimestring.bind(this));
|
||||||
|
this.audioEl.addEventListener('timeupdate', this.storeTime.bind(this));
|
||||||
|
|
||||||
|
|
||||||
this.updateTimestring();
|
this.updateTimestring();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,25 +146,87 @@ class Parade {
|
||||||
this.timeEl.innerHTML = timestring;
|
this.timeEl.innerHTML = timestring;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storeTime() {
|
||||||
|
// do we use this.t or audioEl...?
|
||||||
|
localStorage.setItem('playhead', String(this.audioEl.currentTime))
|
||||||
|
}
|
||||||
|
restoreTime() {
|
||||||
|
let time = localStorage.getItem('playhead');
|
||||||
|
|
||||||
|
this.audioEl.addEventListener('playing', (e) => {
|
||||||
|
if(!this.isPlaying) this.play(); // make sure we're playing.
|
||||||
|
});
|
||||||
|
// causes odd glitch
|
||||||
|
// this.audioEl.addEventListener('pause', (e) => {
|
||||||
|
// if(this.isPlaying) this.pause(true); // make sure we're playing.
|
||||||
|
// });
|
||||||
|
this.audioEl.addEventListener('seeking',(e)=>{
|
||||||
|
this.t = parseInt(e.target.currentTime * 1000);
|
||||||
|
console.log('restore timer',this.t,e);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!time) return;
|
||||||
|
time = parseFloat(time);
|
||||||
|
this.audioEl.currentTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
setupSorter() {
|
setupSorter() {
|
||||||
this.sorterTemplate = document.getElementById('annotationContentTemplate');
|
this.sorterTemplate = document.getElementById('annotationContentTemplate');
|
||||||
this.sorter = document.getElementById('annotation');
|
this.sorter = document.getElementById('annotation');
|
||||||
this.pauseTimeout = null;
|
this.pauseTimeout = null;
|
||||||
this.imagesToSort = this.images.slice(); // we will keep track of images that needs weighing (clone by reference)
|
let imagesToSort = localStorage.getItem('imagesToSort');
|
||||||
|
if(imagesToSort) {
|
||||||
|
this.imagesToSort = JSON.parse(imagesToSort);
|
||||||
|
}
|
||||||
|
// else or invalid:
|
||||||
|
if(!imagesToSort || this.imagesToSort.length > this.images.length) {
|
||||||
|
console.log('createimages to sort')
|
||||||
|
this.imagesToSort = []; // we will keep track of images that needs weighing
|
||||||
|
for (let index = 0; index < this.images.length; index++) {
|
||||||
|
this.imagesToSort[index] = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let nextButton = document.getElementById('nextButton');
|
let nextButton = document.getElementById('nextButton');
|
||||||
nextButton.addEventListener('click', this.nextSorterImage.bind(this));
|
nextButton.addEventListener('click', this.nextSorterImage.bind(this));
|
||||||
|
|
||||||
|
let resetButton = document.getElementById('resetButton');
|
||||||
|
resetButton.addEventListener('click', this.resetSortedImages.bind(this));
|
||||||
|
let resumeButton = document.getElementById('resumeButton');
|
||||||
|
resumeButton.addEventListener('click', this.play.bind(this));
|
||||||
|
|
||||||
// set maximum to last image index
|
// set maximum to last image index
|
||||||
this.sorterTemplate.content.querySelector('.weight').max = this.images.length - 1;
|
this.sorterTemplate.content.querySelector('.weight').max = this.images.length - 1;
|
||||||
|
|
||||||
this.changeSorterImage();
|
this.changeSorterImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetSortedImages() {
|
||||||
|
this.imagesToSort = []; // we will keep track of images that needs weighing
|
||||||
|
for (let index = 0; index < this.images.length; index++) {
|
||||||
|
this.imagesToSort[index] = index;
|
||||||
|
}
|
||||||
|
this.changeSorterImage(); // display an image to sort
|
||||||
|
this.play(); // make sure playback timer gets set
|
||||||
|
}
|
||||||
|
|
||||||
changeSorterImage() {
|
changeSorterImage() {
|
||||||
|
|
||||||
|
let sorterContent = this.sorter.querySelector('#annotationContent');
|
||||||
|
|
||||||
|
if(!this.imagesToSort.length) {
|
||||||
|
document.body.classList.add('finished');
|
||||||
|
sorterContent.innerHTML = "";
|
||||||
|
this.audioEl.controls = true;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('finished');
|
||||||
|
this.audioEl.controls = false;
|
||||||
|
}
|
||||||
|
|
||||||
// pick random image
|
// pick random image
|
||||||
this.imageToSort = this.imagesToSort[Math.floor(Math.random()*this.imagesToSort.length)];
|
this.imageIdToSort = this.imagesToSort[Math.floor(Math.random()*this.imagesToSort.length)];
|
||||||
// this.imageToSort = this.imagesToSort[0]; // always the first instead of random
|
this.imageToSort = this.images[this.imageIdToSort];
|
||||||
|
|
||||||
let clone = this.sorterTemplate.content.cloneNode(true);
|
let clone = this.sorterTemplate.content.cloneNode(true);
|
||||||
let imgEl = clone.querySelector(".img");
|
let imgEl = clone.querySelector(".img");
|
||||||
|
@ -156,7 +241,6 @@ class Parade {
|
||||||
weightEl.value = parseInt(this.imageToSort.position); // don't use weight, but the image's actual position
|
weightEl.value = parseInt(this.imageToSort.position); // don't use weight, but the image's actual position
|
||||||
weightEl.addEventListener('input', this.changeSorterWeight.bind(this));
|
weightEl.addEventListener('input', this.changeSorterWeight.bind(this));
|
||||||
|
|
||||||
let sorterContent = this.sorter.querySelector('#annotationContent');
|
|
||||||
sorterContent.innerHTML = "";
|
sorterContent.innerHTML = "";
|
||||||
sorterContent.appendChild(clone);
|
sorterContent.appendChild(clone);
|
||||||
}
|
}
|
||||||
|
@ -190,8 +274,10 @@ class Parade {
|
||||||
|
|
||||||
nextSorterImage() {
|
nextSorterImage() {
|
||||||
this.play();
|
this.play();
|
||||||
let i = this.imagesToSort.indexOf(this.imageToSort);
|
let idx = this.imagesToSort.indexOf(this.imageIdToSort);
|
||||||
this.imagesToSort.splice(i, 1); // remove from images to position
|
this.imagesToSort.splice(idx, 1); // remove from images to position
|
||||||
|
// store the array
|
||||||
|
localStorage.setItem('imagesToSort', JSON.stringify(this.imagesToSort));
|
||||||
|
|
||||||
this.changeSorterImage();
|
this.changeSorterImage();
|
||||||
}
|
}
|
||||||
|
@ -199,23 +285,32 @@ class Parade {
|
||||||
play() {
|
play() {
|
||||||
// play audio for n seconds:
|
// play audio for n seconds:
|
||||||
// extend timeout if it's already playing
|
// extend timeout if it's already playing
|
||||||
const duration = 10; // seconds
|
const duration = this.config.playDuration; // seconds
|
||||||
if(this.pauseTimeout) {
|
if(this.pauseTimeout) {
|
||||||
clearTimeout(this.pauseTimeout);
|
clearTimeout(this.pauseTimeout);
|
||||||
}
|
}
|
||||||
|
if(this.imagesToSort.length > 0){
|
||||||
this.pauseTimeout = setTimeout(() => {
|
this.pauseTimeout = setTimeout(() => {
|
||||||
this.pause();
|
this.pause();
|
||||||
}, duration * 1000);
|
}, duration * 1000);
|
||||||
|
} else {
|
||||||
|
// don't pause anymore when everything is sorted.
|
||||||
|
this.pauseTimeout = null;
|
||||||
|
}
|
||||||
this.audioEl.play();
|
this.audioEl.play();
|
||||||
document.body.classList.remove('paused');
|
document.body.classList.remove('paused');
|
||||||
this.isPlaying = true;
|
this.isPlaying = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause(dopause) {
|
||||||
|
if(this.imagesToSort.length < 1 && (typeof dopause == 'undefined' || false)){
|
||||||
|
console.log('do not pause');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// pause audio and animation
|
// pause audio and animation
|
||||||
this.audioEl.pause();
|
|
||||||
document.body.classList.add('paused');
|
document.body.classList.add('paused');
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
|
this.audioEl.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoop() {
|
renderLoop() {
|
||||||
|
@ -237,6 +332,8 @@ class Parade {
|
||||||
if(this.isPlaying) {
|
if(this.isPlaying) {
|
||||||
// todo: make it come to a slow halt, instead of sudden;
|
// todo: make it come to a slow halt, instead of sudden;
|
||||||
dt = n - this.lastRenderTime;
|
dt = n - this.lastRenderTime;
|
||||||
|
if(dt < 0)
|
||||||
|
console.log('change',dt);
|
||||||
this.t += dt;
|
this.t += dt;
|
||||||
}
|
}
|
||||||
this.lastRenderTime = n; // also increment when paused;
|
this.lastRenderTime = n; // also increment when paused;
|
||||||
|
@ -270,7 +367,6 @@ class Parade {
|
||||||
// This attaches the camera to the canvas
|
// This attaches the camera to the canvas
|
||||||
camera.attachControl(this.canvasEl, true);
|
camera.attachControl(this.canvasEl, true);
|
||||||
|
|
||||||
let radius = 60;
|
|
||||||
for(let i in this.images) {
|
for(let i in this.images) {
|
||||||
// create the Meshes with the images in the scene
|
// create the Meshes with the images in the scene
|
||||||
this.images[i].makeMesh(this.scene);
|
this.images[i].makeMesh(this.scene);
|
||||||
|
@ -278,7 +374,6 @@ class Parade {
|
||||||
|
|
||||||
|
|
||||||
this.lastRenderTime = window.performance.now();
|
this.lastRenderTime = window.performance.now();
|
||||||
this.t = 0; // playback timer
|
|
||||||
return this.scene;
|
return this.scene;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue