comparison interfaces/timeline.js @ 2479:dbe43b4ab7aa

Starting timeline.js interface
author Nicholas Jillings <nicholas.jillings@mail.bcu.ac.uk>
date Wed, 03 Aug 2016 14:52:04 +0100
parents
children 713a2d059a16
comparison
equal deleted inserted replaced
2477:f6f4693a84eb 2479:dbe43b4ab7aa
1 /**
2 * WAET Timeline
3 * This interface plots a waveform timeline per audio fragment on a page. Clicking on the fragment will generate a comment box for processing.
4 */
5
6 // Once this is loaded and parsed, begin execution
7 loadInterface();
8
9 function loadInterface() {
10 // Use this to do any one-time page / element construction. For instance, placing any stationary text objects,
11 // holding div's, or setting up any nodes which are present for the entire test sequence
12
13 interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema
14
15 interfaceContext.insertPoint = document.getElementById("topLevelBody");
16 var testContent = document.createElement("div");
17
18 // Create the top div and Title element
19 var title = document.createElement("div");
20 title.className = "title";
21 title.align = "center";
22 var titleSpan = document.createElement("span");
23 titleSpan.id = "test-title";
24 titleSpan.textContent = "Listening Test";
25 title.appendChild(titleSpan);
26
27 var pagetitle = document.createElement("div");
28 pagetitle.className = "pageTitle";
29 pagetitle.align = "center";
30 titleSpan = document.createElement("span");
31 titleSpan.id = "page-title";
32 pagetitle.appendChild(titleSpan);
33
34 // Create Interface buttons
35 var interfaceButtons = document.createElement("div");
36 interfaceButtons.id = 'interface-buttons';
37 interfaceButtons.style.height = "25px";
38
39 // Create playback start/stop points
40 var playback = document.createElement("button");
41 playback.innerHTML = "Stop";
42 playback.id = "playback-button";
43 playback.onclick = function() {
44 if (audioEngineContext.status == 1) {
45 audioEngineContext.stop();
46 this.innerHTML = "Stop";
47 var time = audioEngineContext.timer.getTestTime();
48 console.log("Stopped at "+time);
49 }
50 };
51 // Create Submit (save) button
52 var submit = document.createElement("button");
53 submit.innerHTML = 'Next';
54 submit.onclick = buttonSubmitClick;
55 submit.id = 'submit-button';
56 submit.style.float = 'left';
57 // Append the interface buttons into the interfaceButtons object.
58 interfaceButtons.appendChild(playback);
59 interfaceButtons.appendChild(submit);
60
61 // Create outside reference holder
62 var outsideRef = document.createElement("div");
63 outsideRef.id = "outside-reference-holder";
64
65 // Create content point
66 var content = document.createElement("div");
67 content.id = "timeline-test-content";
68
69 //Inject
70 testContent.appendChild(title);
71 testContent.appendChild(pagetitle);
72 testContent.appendChild(interfaceButtons);
73 testContent.appendChild(outsideRef);
74 testContent.appendChild(content);
75 interfaceContext.insertPoint.appendChild(testContent);
76
77 // Load the full interface
78 testState.initialise();
79 testState.advanceState();
80 };
81
82 function loadTest(page)
83 {
84 // Called each time a new test page is to be build. The page specification node is the only item passed in
85 var content = document.getElementById("timeline-test-content");
86 content.innerHTML = "";
87 var interfaceObj = page.interfaces;
88 if (interfaceObj.length > 1)
89 {
90 console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node");
91 }
92 interfaceObj = interfaceObj[0];
93
94 //Set the page title
95 if (typeof page.title == "string" && page.title.length > 0)
96 {
97 document.getElementById("test-title").textContent = page.title;
98 }
99
100 if (interfaceObj.title != null) {
101 document.getElementById("page-title").textContent = interfaceObj.title;
102 }
103
104 // Delete outside reference
105 var outsideReferenceHolder = document.getElementById("outside-reference-holder");
106 outsideReferenceHolder.innerHTML = "";
107
108 var commentBoxPrefix = "Comment on track";
109 if (interfaceObj.commentBoxPrefix != undefined) {
110 commentBoxPrefix = interfaceObj.commentBoxPrefix;
111 }
112
113 $(page.audioElements).each(function(index,element){
114 var audioObject = audioEngineContext.newTrack(element);
115 if (page.audioElements.type == 'outside-reference') {
116 var refNode = interfaceContext.outsideReferenceDOM(audioObject,index,outsideReferenceHolder);
117 audioObject.bindInterface(orNode);
118 } else {
119 switch(audioObject.specification.parent.label) {
120 case "none":
121 label = "";
122 break;
123 case "letter":
124 label = String.fromCharCode(97 + index);
125 break;
126 case "capital":
127 label = String.fromCharCode(65 + index);
128 break;
129 default:
130 label = ""+index;
131 break;
132 }
133 var node = new interfaceObject(audioObject,label);
134
135 content.appendChild(node.DOM);
136 audioObject.bindInterface(node);
137 }
138 });
139
140 resizeWindow();
141 }
142
143 function interfaceObject(audioObject,labelstr)
144 {
145 // Each audio object has a waveform guide and self-generated comments
146 this.parent = audioObject;
147 this.DOM = document.createElement("div");
148 this.DOM.className = "timeline-element";
149 this.DOM.id = audioObject.specification.id;
150
151 var root = document.createElement("div");
152 root.className = "timeline-element-content";
153 this.DOM.appendChild(root);
154
155 var label = document.createElement("div");
156 label.style.textAlign = "center";
157 var labelSpan = document.createElement("span");
158 labelSpan.textContent = "Fragment "+labelstr;
159 label.appendChild(labelSpan);
160 root.appendChild(label);
161
162 var canvasHolder = document.createElement("div");
163 canvasHolder.className = "timeline-element-canvas-holder";
164 var buttonHolder = document.createElement("div");
165 buttonHolder.className = "timeline-element-button-holder";
166 var commentHolder = document.createElement("div");
167 commentHolder.className = "timeline-element-comment-holder";
168
169 root.appendChild(canvasHolder);
170 root.appendChild(buttonHolder);
171 root.appendChild(commentHolder);
172
173 this.comments = {
174 parent: this,
175 list: [],
176 Comment: function(parent,time, str) {
177 this.parent = parent;
178 this.time = time;
179 this.DOM = document.createElement("div");
180 this.DOM.className = "comment-div";
181 this.title = document.createElement("span");
182 if (str != undefined) {
183 this.title.textContent = str;
184 } else {
185 this.title.textContent = "Time: "+time.toFixed(2)+"s";
186 }
187 this.textarea = document.createElement("textarea");
188 this.textarea.className = "trackComment";
189 this.DOM.appendChild(this.title);
190 this.DOM.appendChild(document.createElement("br"));
191 this.DOM.appendChild(this.textarea);
192 this.resize = function() {
193 var w = window.innerWidth;
194 w = Math.min(w,800);
195 w = Math.max(w,200);
196 var elem_w = w / 2.5;
197 elem_w = Math.max(elem_w,190);
198 this.DOM.style.width = elem_w+"px";
199 this.textarea.style.width = (elem_w-5)+"px";
200 }
201 this.resize();
202 },
203 newComment: function(time) {
204 var node = new this.Comment(this,time);
205 this.list.push(node);
206 commentHolder.appendChild(node.DOM);
207 return node;
208 },
209 deleteComment: function(comment) {
210
211 },
212 clearList: function() {
213
214 }
215 }
216
217 this.canvas = {
218 parent: this,
219 comments: this.comments,
220 layer1: document.createElement("canvas"),
221 layer2: document.createElement("canvas"),
222 layer3: document.createElement("canvas"),
223 layer4: document.createElement("canvas"),
224 resize: function(w) {
225 this.layer1.width = w;
226 this.layer2.width = w;
227 this.layer3.width = w;
228 this.layer4.width = w;
229 this.layer1.style.width = w+"px";
230 this.layer2.style.width = w+"px";
231 this.layer3.style.width = w+"px";
232 this.layer4.style.width = w+"px";
233 },
234 handleEvent: function(event) {
235 switch(event.currentTarget) {
236 case this.layer1:
237 switch(event.type) {
238 case "mousemove":
239 this.drawMouse(event);
240 break;
241 case "mouseleave":
242 this.clearCanvas(this.layer1);
243 break;
244 case "click":
245 var rect = this.layer1.getBoundingClientRect();
246 var pixX = event.clientX - rect.left;
247 var tpp = this.parent.parent.buffer.buffer.duration/this.layer1.width;
248 this.comments.newComment(pixX*tpp);
249 this.drawMarkers();
250 break;
251 }
252 break;
253 }
254 },
255 drawWaveform: function() {
256 var buffer = this.parent.parent.buffer.buffer;
257 var context = this.layer4.getContext("2d");
258 context.lineWidth = 1;
259 context.strokeStyle = "#888";
260 context.clearRect(0,0,this.layer4.width, this.layer4.height);
261 var data = buffer.getChannelData(0);
262 var t_per_pixel = buffer.duration/this.layer4.width;
263 var s_per_pixel = data.length/this.layer4.width;
264 var pixX = 0;
265 while (pixX < this.layer4.width) {
266 var start = Math.floor(s_per_pixel*pixX);
267 var end = Math.min(Math.ceil(s_per_pixel*(pixX+1)),data.length);
268 var frame = data.subarray(start,end);
269 var min = frame[0];
270 var max = min;
271 for (var n=0; n<frame.length; n++) {
272 if (frame[n] < min) {min = frame[n];}
273 if (frame[n] > max) {max = frame[n];}
274 }
275 // Assuming min/max normalised between [-1, 1] to map to [150, 0]
276 context.beginPath();
277 context.moveTo(pixX+0.5,(min+1)*-75+150);
278 context.lineTo(pixX+0.5,(max+1)*-75+150);
279 context.stroke();
280 pixX++;
281 }
282 },
283 drawMouse: function(event) {
284 var context = this.layer1.getContext("2d");
285 context.clearRect(0,0,this.layer1.width, this.layer1.height);
286 var rect = this.layer1.getBoundingClientRect();
287 var pixX = event.clientX - rect.left;
288 pixX = Math.floor(pixX)-0.5;
289 context.strokeStyle = "#800";
290 context.beginPath();
291 context.moveTo(pixX,0);
292 context.lineTo(pixX,this.layer1.height);
293 context.stroke();
294 },
295 drawTicker: function() {
296 var context = this.layer2.getContext("2d");
297 context.clearRect(0,0,this.layer2.width, this.layer2.height);
298 var time = this.parent.parent.getCurrentPosition();
299 var ratio = time / this.parent.parent.buffer.buffer.duration;
300 var pixX = Math.floor(ratio*this.layer2.width)+0.5;
301 context.strokeStyle = "#080";
302 context.beginPath();
303 context.moveTo(pixX,0);
304 context.lineTo(pixX,this.layer2.height);
305 context.stroke();
306 },
307 drawMarkers: function() {
308 var context = this.layer3.getContext("2d");
309 context.clearRect(0,0,this.layer3.width, this.layer3.height);
310 context.strokeStyle = "#008";
311 var tpp = this.parent.parent.buffer.buffer.duration/this.layer1.width;
312 for (var i=0; i<this.comments.list.length; i++) {
313 var comment = this.comments.list[i];
314 var pixX = Math.floor(comment.time/tpp)+0.5;
315 context.beginPath();
316 context.moveTo(pixX,0);
317 context.lineTo(pixX,this.layer3.height);
318 context.stroke();
319 }
320 },
321 clearCanvas: function(canvas) {
322 var context = canvas.getContext("2d");
323 context.clearRect(0,0,canvas.width, canvas.height);
324 }
325 }
326 this.canvas.layer1.className = "timeline-element-canvas canvas-layer1 canvas-disabled";
327 this.canvas.layer2.className = "timeline-element-canvas canvas-layer2";
328 this.canvas.layer3.className = "timeline-element-canvas canvas-layer3";
329 this.canvas.layer4.className = "timeline-element-canvas canvas-layer3";
330 this.canvas.layer1.height = "150";
331 this.canvas.layer2.height = "150";
332 this.canvas.layer3.height = "150";
333 this.canvas.layer4.height = "150";
334 canvasHolder.appendChild(this.canvas.layer1);
335 canvasHolder.appendChild(this.canvas.layer2);
336 canvasHolder.appendChild(this.canvas.layer3);
337 canvasHolder.appendChild(this.canvas.layer4);
338 this.canvas.layer1.addEventListener("mousemove",this.canvas);
339 this.canvas.layer1.addEventListener("mouseleave",this.canvas);
340 this.canvas.layer1.addEventListener("click",this.canvas);
341
342 var canvasIntervalID = null;
343
344 this.playButton = {
345 parent: this,
346 DOM: document.createElement("button"),
347 handleEvent: function(event) {
348 var id = this.parent.parent.id;
349 var str = this.DOM.textContent;
350 if (str == "Play") {
351 audioEngineContext.play(id);
352 } else if (str == "Stop") {
353 audioEngineContext.stop();
354 }
355 }
356 }
357 this.playButton.DOM.addEventListener("click",this.playButton);
358 this.playButton.DOM.className = "timeline-button timeline-button-disabled";
359 this.playButton.DOM.disabled = true;
360 this.playButton.DOM.textContent = "Wait";
361
362 this.clearButton = {
363 parent: this,
364 DOM: document.createElement("button"),
365 handleEvent: function(event) {
366 this.parent.comments.clearList();
367 }
368 }
369 this.clearButton.DOM.addEventListener("click",this.clearButton);
370 this.clearButton.DOM.className = "timeline-button";
371 this.clearButton.DOM.textContent = "Clear";
372
373
374 buttonHolder.appendChild(this.playButton.DOM);
375 buttonHolder.appendChild(this.clearButton.DOM);
376
377 this.resize = function() {
378 var w = window.innerWidth;
379 w = Math.min(w,800);
380 w = Math.max(w,200);
381 root.style.width = w+"px";
382 var c_w = w-100;
383 this.canvas.resize(c_w);
384 }
385
386 this.enable = function()
387 {
388 // This is used to tell the interface object that playback of this node is ready
389 this.canvas.layer1.addEventListener("click",this.canvas);
390 this.canvas.layer1.className = "timeline-element-canvas canvas-layer1";
391 this.playButton.DOM.className = "timeline-button timeline-button-play";
392 this.playButton.DOM.textContent = "Play";
393 this.playButton.DOM.disabled = false;
394
395 this.canvas.drawWaveform();
396 };
397 this.updateLoading = function(progress)
398 {
399 // progress is a value from 0 to 100 indicating the current download state of media files
400 progress = String(progress);
401 progress = progress.substr(0,5);
402 this.playButton.DOM.textContent = "Loading: "+progress+'%';
403 };
404 this.startPlayback = function()
405 {
406 // Called when playback has begun
407 canvasIntervalID = window.setInterval(this.canvas.drawTicker.bind(this.canvas),100);
408 this.playButton.DOM.textContent = "Stop";
409 };
410 this.stopPlayback = function()
411 {
412 // Called when playback has stopped. This gets called even if playback never started!
413 window.clearInterval(canvasIntervalID);
414 this.canvas.clearCanvas(this.canvas.layer2);
415 this.playButton.DOM.textContent = "Play";
416 };
417 this.getValue = function()
418 {
419 // Return the current value of the object. If there is no value, return 0
420 return 0;
421 };
422 this.getPresentedId = function()
423 {
424 // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale
425 return labelSpan.textContent;
426 };
427 this.canMove = function()
428 {
429 // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale.
430 // These are checked primarily if the interface check option 'fragmentMoved' is enabled.
431 return false;
432 };
433 this.exportXMLDOM = function(audioObject) {
434 // Called by the audioObject holding this element to export the interface <value> node.
435 // If there is no value node (such as outside reference), return null
436 // If there are multiple value nodes (such as multiple scale / 2D scales), return an array of nodes with each value node having an 'interfaceName' attribute
437 // Use storage.document.createElement('value'); to generate the XML node.
438 return null;
439 };
440 this.error = function() {
441 // If there is an error with the audioObject, this will be called to indicate a failure
442 }
443 };
444
445 function resizeWindow(event)
446 {
447 // Called on every window resize event, use this to scale your page properly
448 for (var i=0; i<audioEngineContext.audioObjects.length; i++) {
449 audioEngineContext.audioObjects[i].interfaceDOM.resize();
450 }
451 }
452
453 function buttonSubmitClick()
454 {
455 }
456
457 function pageXMLSave(store, pageSpecification)
458 {
459 // MANDATORY
460 // Saves a specific test page
461 // You can use this space to add any extra nodes to your XML <audioHolder> saves
462 // Get the current <page> information in store (remember to appendChild your data to it)
463 // pageSpecification is the current page node configuration
464 // To create new XML nodes, use storage.document.createElement();
465 }