|
|
|
@ -1,6 +1,14 @@
@@ -1,6 +1,14 @@
|
|
|
|
|
var parade; |
|
|
|
|
var interview_sorter; |
|
|
|
|
|
|
|
|
|
var config = { |
|
|
|
|
speedFactor: 0.01, // speed of the shapes
|
|
|
|
|
distanceFactor: 20, // distance between the shapes
|
|
|
|
|
startOffset: 0.001, // starting position
|
|
|
|
|
shapeWidth: 100, // 8 shape of animation: width
|
|
|
|
|
shapeHeight: 1300 // 8 shape of animation: height;
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// polyfill
|
|
|
|
|
window.performance = window.performance || {}; |
|
|
|
|
performance.now = (function() { |
|
|
|
@ -26,7 +34,7 @@ fetch(request)
@@ -26,7 +34,7 @@ fetch(request)
|
|
|
|
|
console.debug(data); |
|
|
|
|
// loaded data from json, initialise interface
|
|
|
|
|
const canvas = document.getElementById("renderCanvas"); |
|
|
|
|
parade = new Parade(canvas, data['image']); |
|
|
|
|
parade = new Parade(canvas, data['image'], config); |
|
|
|
|
parade.createScene(); |
|
|
|
|
// parade.scene.debugLayer.show();
|
|
|
|
|
}).catch(error => { |
|
|
|
@ -35,24 +43,61 @@ fetch(request)
@@ -35,24 +43,61 @@ fetch(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Parade { |
|
|
|
|
constructor(canvasEl, images) { |
|
|
|
|
this.speedFactor = -.05; |
|
|
|
|
this.distanceFactor = 20; |
|
|
|
|
this.startOffset = 800; |
|
|
|
|
this.maxStepSize = 1; |
|
|
|
|
constructor(canvasEl, images, config) { |
|
|
|
|
// animation parameters:
|
|
|
|
|
this.config = config; |
|
|
|
|
|
|
|
|
|
this.prevRenderTime = null; |
|
|
|
|
this.canvasEl = canvasEl; |
|
|
|
|
this.engine = new BABYLON.Engine(canvasEl, true); |
|
|
|
|
this.engine = new BABYLON.Engine(canvasEl, true); // create 3d engine
|
|
|
|
|
this.isPlaying = false; |
|
|
|
|
|
|
|
|
|
// initialise image objects:
|
|
|
|
|
this.images = []; |
|
|
|
|
this.imagePositions = []; // index: position, value: image-index
|
|
|
|
|
let storedPositions = localStorage.getItem('imagePositions'); |
|
|
|
|
if(storedPositions) { |
|
|
|
|
this.imagePositions = JSON.parse(storedPositions); |
|
|
|
|
} |
|
|
|
|
// else or invalid:
|
|
|
|
|
if(!storedPositions || this.imagePositions.length != images.length) { |
|
|
|
|
// fill with default: order of the JSON file
|
|
|
|
|
for (let index = 0; index < images.length; index++) { |
|
|
|
|
this.imagePositions[index] = index; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for(let i in images) { |
|
|
|
|
i = parseInt(i); |
|
|
|
|
let pos = this.imagePositions.indexOf(i); |
|
|
|
|
// console.log('pos', , pos);
|
|
|
|
|
let img = new ParadeImage(images[i], pos); |
|
|
|
|
this.images.push(img); |
|
|
|
|
|
|
|
|
|
// for testing:
|
|
|
|
|
// if(this.images.length > 5)
|
|
|
|
|
// break;
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.setupSubtitles(); |
|
|
|
|
this.setupSorter(); |
|
|
|
|
|
|
|
|
|
// Watch for browser/canvas resize events
|
|
|
|
|
window.addEventListener("resize", function () { |
|
|
|
|
this.engine.resize(); |
|
|
|
|
}.bind(this)); |
|
|
|
|
|
|
|
|
|
//Register a render loop to repeatedly render the scene
|
|
|
|
|
this.engine.runRenderLoop(this.renderLoop.bind(this)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setupSubtitles() { |
|
|
|
|
this.audioEl = document.getElementById('interview'); // audio player
|
|
|
|
|
this.subtitleEl = document.getElementById('subtitles'); // the subtitle div
|
|
|
|
|
this.trackEl = this.audioEl.querySelector('track'); // the subtitle track
|
|
|
|
|
|
|
|
|
|
this.timeEl = document.getElementById('time'); // the subtitle track
|
|
|
|
|
|
|
|
|
|
// set up subtitles:
|
|
|
|
|
this.trackEl.addEventListener("cuechange", (event) => { |
|
|
|
|
console.log(event); |
|
|
|
|
let cues = event.target.track.activeCues; |
|
|
|
|
let content = ""; |
|
|
|
|
for(let cue of cues) { |
|
|
|
@ -62,22 +107,115 @@ class Parade {
@@ -62,22 +107,115 @@ class Parade {
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// initialise image objects:
|
|
|
|
|
this.images = []; |
|
|
|
|
for(let i in images) { |
|
|
|
|
this.images.push(new ParadeImage(images[i], i)); |
|
|
|
|
// for testing:
|
|
|
|
|
// if(this.images.length > 5)
|
|
|
|
|
// break;
|
|
|
|
|
} |
|
|
|
|
this.audioEl.addEventListener('timeupdate', this.updateTimestring.bind(this)); |
|
|
|
|
this.updateTimestring(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
updateTimestring() { |
|
|
|
|
let now = this.audioEl.currentTime; |
|
|
|
|
let duration = this.audioEl.duration; |
|
|
|
|
|
|
|
|
|
// Watch for browser/canvas resize events
|
|
|
|
|
window.addEventListener("resize", function () { |
|
|
|
|
this.engine.resize(); |
|
|
|
|
}.bind(this)); |
|
|
|
|
let timestring = String(Math.floor(now/60)).padStart(2, '0') + ":" |
|
|
|
|
+ String(Math.floor(now)%60).padStart(2, '0') |
|
|
|
|
+ " / " + String(Math.floor(duration/60)).padStart(2, '0') + ":" |
|
|
|
|
+ String(Math.floor(duration)%60).padStart(2, '0'); |
|
|
|
|
|
|
|
|
|
//Register a render loop to repeatedly render the scene
|
|
|
|
|
this.engine.runRenderLoop(this.renderLoop.bind(this)); |
|
|
|
|
this.timeEl.innerHTML = timestring; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setupSorter() { |
|
|
|
|
this.sorterTemplate = document.getElementById('annotationContentTemplate'); |
|
|
|
|
this.sorter = document.getElementById('annotation'); |
|
|
|
|
this.pauseTimeout = null; |
|
|
|
|
this.imagesToSort = this.images.slice(); // we will keep track of images that needs weighing (clone by reference)
|
|
|
|
|
|
|
|
|
|
let nextButton = document.getElementById('nextButton'); |
|
|
|
|
nextButton.addEventListener('click', this.nextSorterImage.bind(this)); |
|
|
|
|
|
|
|
|
|
// set maximum to last image index
|
|
|
|
|
this.sorterTemplate.content.querySelector('.weight').max = this.images.length - 1; |
|
|
|
|
|
|
|
|
|
this.changeSorterImage(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
changeSorterImage() { |
|
|
|
|
// pick random image
|
|
|
|
|
this.imageToSort = this.imagesToSort[Math.floor(Math.random()*this.imagesToSort.length)]; |
|
|
|
|
// this.imageToSort = this.imagesToSort[0]; // always the first instead of random
|
|
|
|
|
|
|
|
|
|
let clone = this.sorterTemplate.content.cloneNode(true); |
|
|
|
|
let imgEl = clone.querySelector(".img"); |
|
|
|
|
let descriptionEl = clone.querySelector(".description"); |
|
|
|
|
let nameEl = clone.querySelector(".name"); |
|
|
|
|
let weightEl = clone.querySelector(".weight"); |
|
|
|
|
imgEl.src = this.imageToSort.getImageUrl(); |
|
|
|
|
nameEl.innerHTML = this.imageToSort.info['image']; |
|
|
|
|
descriptionEl.innerHTML = this.imageToSort.info['set']; |
|
|
|
|
|
|
|
|
|
// weightEl.value = parseInt(image.info['weight']);
|
|
|
|
|
weightEl.value = parseInt(this.imageToSort.position); // don't use weight, but the image's actual position
|
|
|
|
|
weightEl.addEventListener('input', this.changeSorterWeight.bind(this)); |
|
|
|
|
|
|
|
|
|
let sorterContent = this.sorter.querySelector('#annotationContent'); |
|
|
|
|
sorterContent.innerHTML = ""; |
|
|
|
|
sorterContent.appendChild(clone); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
changeSorterWeight(ev) { |
|
|
|
|
this.play(); |
|
|
|
|
|
|
|
|
|
// make sure nr is never too big.
|
|
|
|
|
let newIndex = Math.min(this.images.length -1 , parseInt(ev.target.value)); |
|
|
|
|
this.setImagePosition(this.imageToSort, newIndex); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setImagePosition(image, new_position) { |
|
|
|
|
let id = this.images.indexOf(image); |
|
|
|
|
let old_position = this.imagePositions.indexOf(id); |
|
|
|
|
// move from old to new position in the array, see https://stackoverflow.com/a/5306832
|
|
|
|
|
this.imagePositions.splice( |
|
|
|
|
new_position, 0, |
|
|
|
|
this.imagePositions.splice(old_position, 1)[0] |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// update positions of image objects
|
|
|
|
|
for(let i in this.images) { |
|
|
|
|
i = parseInt(i); |
|
|
|
|
this.images[i].setPosition(this.imagePositions.indexOf(i), this.t + 2000); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// save:
|
|
|
|
|
localStorage.setItem('imagePositions', JSON.stringify(this.imagePositions)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
nextSorterImage() { |
|
|
|
|
this.play(); |
|
|
|
|
let i = this.imagesToSort.indexOf(this.imageToSort); |
|
|
|
|
this.imagesToSort.splice(i, 1); // remove from images to position
|
|
|
|
|
|
|
|
|
|
this.changeSorterImage(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
play() { |
|
|
|
|
// play audio for n seconds:
|
|
|
|
|
// extend timeout if it's already playing
|
|
|
|
|
const duration = 10; // seconds
|
|
|
|
|
if(this.pauseTimeout) { |
|
|
|
|
clearTimeout(this.pauseTimeout); |
|
|
|
|
} |
|
|
|
|
this.pauseTimeout = setTimeout(() => { |
|
|
|
|
this.pause(); |
|
|
|
|
}, duration * 1000); |
|
|
|
|
this.audioEl.play(); |
|
|
|
|
document.body.classList.remove('paused'); |
|
|
|
|
this.isPlaying = true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pause() { |
|
|
|
|
// pause audio and animation
|
|
|
|
|
this.audioEl.pause(); |
|
|
|
|
document.body.classList.add('paused'); |
|
|
|
|
this.isPlaying = false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
renderLoop() { |
|
|
|
@ -89,43 +227,32 @@ class Parade {
@@ -89,43 +227,32 @@ class Parade {
|
|
|
|
|
// if(camera.rotation['x'] > .3) {
|
|
|
|
|
// camera.rotation['x'] = .3
|
|
|
|
|
// }
|
|
|
|
|
this.animate(); |
|
|
|
|
this.animate(); // update positions of the objects
|
|
|
|
|
this.scene.render(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
animate() { |
|
|
|
|
let t = window.performance.now() - this.startTime; |
|
|
|
|
// let maxStepSize = Math.inf;
|
|
|
|
|
|
|
|
|
|
// if (this.prevRenderTime === null) {
|
|
|
|
|
|
|
|
|
|
// } else {
|
|
|
|
|
// let tdiff = t - this.prevRenderTime;
|
|
|
|
|
// if(tdiff < 1) {
|
|
|
|
|
// // sometimes multiple render calls within very short moment??
|
|
|
|
|
// // skip some work.
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// maxStepSize = Math.abs(this.maxStepSize * tdiff * this.speedFactor);
|
|
|
|
|
// }
|
|
|
|
|
let n = window.performance.now(); |
|
|
|
|
let dt = 0; |
|
|
|
|
if(this.isPlaying) { |
|
|
|
|
// todo: make it come to a slow halt, instead of sudden;
|
|
|
|
|
dt = n - this.lastRenderTime; |
|
|
|
|
this.t += dt; |
|
|
|
|
} |
|
|
|
|
this.lastRenderTime = n; // also increment when paused;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// this.prevRenderTime = t;
|
|
|
|
|
// console.log(tdiff);
|
|
|
|
|
t -= 800/this.speedFactor; // start offset ( milliseconds)
|
|
|
|
|
// console.log(maxStepSize);
|
|
|
|
|
for(let i of this.images){ |
|
|
|
|
i.updateMeshPosition(t*this.speedFactor/1000); |
|
|
|
|
t-= this.distanceFactor/this.speedFactor; |
|
|
|
|
i.updateMeshPosition(this.t, dt, this.config); |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
createScene() { |
|
|
|
|
// This creates a basic Babylon Scene object (non-mesh)
|
|
|
|
|
// This creates a basic Babylon Scene object
|
|
|
|
|
this.scene = new BABYLON.Scene(this.engine); |
|
|
|
|
// return this.scene;
|
|
|
|
|
// this.scene.clearColor = new BABYLON.Color3(0.6, 0.6, 0.); // color RGB
|
|
|
|
|
|
|
|
|
|
this.scene.clearColor = new BABYLON.Color3(.22,0.27,0.38); // color RGB
|
|
|
|
|
// this.scene.clearColor = new BABYLON.Color4(0.6, 0.6, 0.,0); // transparent - RGBA
|
|
|
|
|
this.scene.fogMode = BABYLON.Scene.FOGMODE_EXP; |
|
|
|
|
this.scene.fogColor = this.scene.clearColor; |
|
|
|
@ -134,8 +261,8 @@ class Parade {
@@ -134,8 +261,8 @@ class Parade {
|
|
|
|
|
// let camera = new BABYLON.UniversalCamera("camera1", new BABYLON.Vector3( 0.0, 0.0, 120.86337575312979), this.scene);
|
|
|
|
|
// camera.rotation = new BABYLON.Vector3(0, Math.PI, 0);
|
|
|
|
|
// camera.fov = 1.1; // default: 0.8
|
|
|
|
|
let camera = new BABYLON.UniversalCamera("camera2", new BABYLON.Vector3( -14.40894348196419, 0.9954991699417852, 17.582966906902247), this.scene); |
|
|
|
|
camera.rotation = new BABYLON.Vector3(0.023121520223909536, 2.5494248799675163, 0); |
|
|
|
|
let camera = new BABYLON.UniversalCamera("camera2", new BABYLON.Vector3( -11.79906036421707, -0.7028999316894499, 32.43524104627659 ), this.scene); |
|
|
|
|
camera.rotation = new BABYLON.Vector3(-0.16070762395712468, 2.4005702973639886, 0); |
|
|
|
|
camera.fov = 1.5; |
|
|
|
|
// camera.rotation = new BABYLON.Vector3(0.1, 0,0);
|
|
|
|
|
// camera.lowerRadiusLimit = 6;
|
|
|
|
@ -145,13 +272,13 @@ class Parade {
@@ -145,13 +272,13 @@ class Parade {
|
|
|
|
|
|
|
|
|
|
let radius = 60; |
|
|
|
|
for(let i in this.images) { |
|
|
|
|
// console.log(i, this.images, this);
|
|
|
|
|
// create the Meshes with the images in the scene
|
|
|
|
|
this.images[i].makeMesh(this.scene); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.startTime = window.performance.now(); |
|
|
|
|
// this.scene.registerBeforeRender(this.animate.bind(this));
|
|
|
|
|
this.lastRenderTime = window.performance.now(); |
|
|
|
|
this.t = 0; // playback timer
|
|
|
|
|
return this.scene; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -161,59 +288,85 @@ class ParadeImage {
@@ -161,59 +288,85 @@ class ParadeImage {
|
|
|
|
|
this.info = info; |
|
|
|
|
this.weight = info['weight']; |
|
|
|
|
this.position = position; // index in list
|
|
|
|
|
this.target_position = position; |
|
|
|
|
this.target_position_time = 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getImageUrl() { |
|
|
|
|
return 'dataset/small/'+ this.info['image']; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* |
|
|
|
|
* @param {int} new_position new position in parade |
|
|
|
|
* @param {float} time_complete time at which it should have transitioned to this position |
|
|
|
|
*/ |
|
|
|
|
setPosition(new_position, time_complete) { |
|
|
|
|
if(this.target_position == new_position) return; |
|
|
|
|
// console.log('changepos', this.target_position, new_position);
|
|
|
|
|
this.target_position = new_position; |
|
|
|
|
this.target_position_time = time_complete; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
makeMesh(scene) { |
|
|
|
|
// let videoSrc = this.images[i]['image'];
|
|
|
|
|
// random fluctuation in radius
|
|
|
|
|
let i = 1; |
|
|
|
|
let radius = 60 |
|
|
|
|
let localRadius = (Math.random() * .2 + 1) * radius; |
|
|
|
|
let x = Math.sin(2*Math.PI * i / 1) * localRadius; |
|
|
|
|
let y = Math.cos(2*Math.PI * i / 1) * localRadius; |
|
|
|
|
var planeOpts = { |
|
|
|
|
height: 5.4762, |
|
|
|
|
width: 7.3967, |
|
|
|
|
sideOrientation: BABYLON.Mesh.DOUBLESIDE |
|
|
|
|
}; |
|
|
|
|
this.mesh = BABYLON.MeshBuilder.CreatePlane("plane", planeOpts, scene); |
|
|
|
|
|
|
|
|
|
var ANote0VideoMat = new BABYLON.StandardMaterial("m", scene); |
|
|
|
|
var planeOptions = { |
|
|
|
|
// TODO: should size be dynamic?
|
|
|
|
|
height: 5.4762, |
|
|
|
|
width: 7.3967, |
|
|
|
|
sideOrientation: BABYLON.Mesh.DOUBLESIDE // texture should be visible from front and back
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
this.mesh = BABYLON.MeshBuilder.CreatePlane("plane", planeOptions, scene); |
|
|
|
|
|
|
|
|
|
var mat = new BABYLON.StandardMaterial("m", scene); |
|
|
|
|
|
|
|
|
|
mat.diffuseTexture = new BABYLON.Texture(this.getImageUrl(), scene); |
|
|
|
|
mat.roughness = 1; |
|
|
|
|
mat.emissiveColor = new BABYLON.Color3.White(); |
|
|
|
|
this.mesh.material = mat; |
|
|
|
|
|
|
|
|
|
let imgUrl = 'dataset/small/'+ this.info['image']; |
|
|
|
|
// console.log(imgUrl);
|
|
|
|
|
ANote0VideoMat.diffuseTexture = new BABYLON.Texture(imgUrl, scene); |
|
|
|
|
ANote0VideoMat.roughness = 1; |
|
|
|
|
ANote0VideoMat.emissiveColor = new BABYLON.Color3.White(); |
|
|
|
|
this.mesh.material = ANote0VideoMat; |
|
|
|
|
this.mesh.rotation.y = Math.PI; // rotate 180 deg. to avoid looking at backside from the start.
|
|
|
|
|
|
|
|
|
|
// this.updateMeshPosition(1, Math.inf); //TODO: remove when animating
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
updateMeshPosition(t){ |
|
|
|
|
// factors determine scale of animation
|
|
|
|
|
let target_x = Math.sin(2*t)*100; |
|
|
|
|
let target_z = Math.sin(t)*1300; |
|
|
|
|
updateMeshPosition(t, dt, config){ |
|
|
|
|
// first see how we are with our moving positions:
|
|
|
|
|
|
|
|
|
|
// let diff_x = target_x - this.mesh.position.x;
|
|
|
|
|
// let diff_z = target_z - this.mesh.position.z;
|
|
|
|
|
if(dt < 1) { |
|
|
|
|
// don't change positions, because we're paused
|
|
|
|
|
} |
|
|
|
|
else if(Math.abs(this.position - this.target_position) > 0.00001) { |
|
|
|
|
if(t > this.target_position_time) { |
|
|
|
|
console.error('were we too slow?'); |
|
|
|
|
this.position = this.target_position; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// let stepSize = Math.sqrt( Math.pow(diff_x, 2) + Math.pow(diff_z, 2) );
|
|
|
|
|
|
|
|
|
|
// if(stepSize > maxStepSize) {
|
|
|
|
|
// // console.log('throttle', maxStepSize, stepSize);
|
|
|
|
|
// // throttle the step, so switching items looks like movement
|
|
|
|
|
// diff_x *= maxStepSize/stepSize;
|
|
|
|
|
// diff_z *= maxStepSize/stepSize;
|
|
|
|
|
// }
|
|
|
|
|
let tdiff = this.target_position_time - t; |
|
|
|
|
if(tdiff != 0) { |
|
|
|
|
if(tdiff < dt) { |
|
|
|
|
// never place it too far...
|
|
|
|
|
this.position = this.target_position; |
|
|
|
|
} else { |
|
|
|
|
this.position += (this.target_position - this.position) / (tdiff/dt); |
|
|
|
|
// console.log(this.position, this.target_position_time, t);
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} else{ |
|
|
|
|
// it's practically the same, but set it to avoid any rounding uglyness.
|
|
|
|
|
this.position = this.target_position; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let p = t; //0; // if 0, disable movement for a bit.
|
|
|
|
|
p *= config.speedFactor/1000 |
|
|
|
|
p -= config.startOffset/config.speedFactor; // start offset ( milliseconds)
|
|
|
|
|
p -= config.distanceFactor * this.position / 1000; |
|
|
|
|
|
|
|
|
|
// // console.log(diff_x, diff_z, stepSize);
|
|
|
|
|
// this.mesh.position.x += diff_x;
|
|
|
|
|
// this.mesh.position.z += diff_z;
|
|
|
|
|
// factors determine scale of animation
|
|
|
|
|
let target_x = Math.sin(2*p)*config.shapeWidth; |
|
|
|
|
let target_z = Math.sin(p)*config.shapeHeight; |
|
|
|
|
this.mesh.position.x = target_x; |
|
|
|
|
this.mesh.position.z = target_z; |
|
|
|
|
|
|
|
|
|
// this.mesh.position = new BABYLON.Vector3(x,0,y);
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|