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() { return performance.now || performance.mozNow || performance.msNow || performance.oNow || performance.webkitNow || Date.now /*none found - fallback to browser default */ })(); const request = new Request('dataset/images_prop.json'); fetch(request) .then(response => { if (response.status === 200) { return response.json(); } else { throw new Error('Something went wrong on api server!'); } }) .then(data => { console.debug(data); // loaded data from json, initialise interface const canvas = document.getElementById("renderCanvas"); parade = new Parade(canvas, data['image'], config); parade.createScene(); // parade.scene.debugLayer.show(); }).catch(error => { console.error(error); }); class Parade { constructor(canvasEl, images, config) { // animation parameters: this.config = config; this.canvasEl = canvasEl; 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) => { let cues = event.target.track.activeCues; let content = ""; for(let cue of cues) { content += "

"+cue.text+"

"; } this.subtitleEl.innerHTML = content; }); this.audioEl.addEventListener('timeupdate', this.updateTimestring.bind(this)); this.updateTimestring(); } updateTimestring() { let now = this.audioEl.currentTime; let duration = this.audioEl.duration; 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'); 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() { // limit camera movement: // let camera = this.scene.cameras[0]; // if(camera.rotation['x'] < 0.1) { // camera.rotation['x'] = 0.1 // } // if(camera.rotation['x'] > .3) { // camera.rotation['x'] = .3 // } this.animate(); // update positions of the objects this.scene.render(); } animate() { 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; // console.log(maxStepSize); for(let i of this.images){ i.updateMeshPosition(this.t, dt, this.config); } } createScene() { // This creates a basic Babylon Scene object this.scene = new BABYLON.Scene(this.engine); 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; this.scene.fogDensity = 0.002; // 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( -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; // camera.upperRadiusLimit = 20; // This attaches the camera to the canvas camera.attachControl(this.canvasEl, true); let radius = 60; for(let i in this.images) { // create the Meshes with the images in the scene this.images[i].makeMesh(this.scene); } this.lastRenderTime = window.performance.now(); this.t = 0; // playback timer return this.scene; } } class ParadeImage { constructor(info, position) { 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) { 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; this.mesh.rotation.y = Math.PI; // rotate 180 deg. to avoid looking at backside from the start. } updateMeshPosition(t, dt, config){ // first see how we are with our moving positions: 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 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; // 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; } }