Mercurial > hg > webaudioevaluationtool
comparison interfaces/horizontal-sliders.js @ 1116:c44fbf72f7f2
All interfaces support comment boxes. Comment box identification matches presented tag (for instance, AB will be Comment on fragment A, rather than 1). Tighter buffer loading protocol, audioObjects register with the buffer rather than checking for buffer existence (which can be buggy depending on the buffer state). Buffers now have a state to ensure exact location in loading chain (downloading, decoding, LUFS, ready).
author | Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk> |
---|---|
date | Fri, 29 Jan 2016 11:11:57 +0000 |
parents | |
children | c0022a09c4f6 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 1116:c44fbf72f7f2 |
---|---|
1 // Once this is loaded and parsed, begin execution | |
2 loadInterface(); | |
3 | |
4 function loadInterface() { | |
5 // Use this to do any one-time page / element construction. For instance, placing any stationary text objects, | |
6 // holding div's, or setting up any nodes which are present for the entire test sequence | |
7 | |
8 // The injection point into the HTML page | |
9 interfaceContext.insertPoint = document.getElementById("topLevelBody"); | |
10 var testContent = document.createElement('div'); | |
11 testContent.id = 'testContent'; | |
12 | |
13 // Create the top div for the Title element | |
14 var titleAttr = specification.title; | |
15 var title = document.createElement('div'); | |
16 title.className = "title"; | |
17 title.align = "center"; | |
18 var titleSpan = document.createElement('span'); | |
19 | |
20 // Set title to that defined in XML, else set to default | |
21 if (titleAttr != undefined) { | |
22 titleSpan.textContent = titleAttr; | |
23 } else { | |
24 titleSpan.textContent = 'Listening test'; | |
25 } | |
26 // Insert the titleSpan element into the title div element. | |
27 title.appendChild(titleSpan); | |
28 | |
29 var pagetitle = document.createElement('div'); | |
30 pagetitle.className = "pageTitle"; | |
31 pagetitle.align = "center"; | |
32 var titleSpan = document.createElement('span'); | |
33 titleSpan.id = "pageTitle"; | |
34 pagetitle.appendChild(titleSpan); | |
35 | |
36 // Create Interface buttons! | |
37 var interfaceButtons = document.createElement('div'); | |
38 interfaceButtons.id = 'interface-buttons'; | |
39 interfaceButtons.style.height = '25px'; | |
40 | |
41 // Create playback start/stop points | |
42 var playback = document.createElement("button"); | |
43 playback.innerHTML = 'Stop'; | |
44 playback.id = 'playback-button'; | |
45 playback.style.float = 'left'; | |
46 // onclick function. Check if it is playing or not, call the correct function in the | |
47 // audioEngine, change the button text to reflect the next state. | |
48 playback.onclick = function() { | |
49 if (audioEngineContext.status == 1) { | |
50 audioEngineContext.stop(); | |
51 this.innerHTML = 'Stop'; | |
52 var time = audioEngineContext.timer.getTestTime(); | |
53 console.log('Stopped at ' + time); // DEBUG/SAFETY | |
54 } | |
55 }; | |
56 // Create Submit (save) button | |
57 var submit = document.createElement("button"); | |
58 submit.innerHTML = 'Submit'; | |
59 submit.onclick = buttonSubmitClick; | |
60 submit.id = 'submit-button'; | |
61 submit.style.float = 'left'; | |
62 // Append the interface buttons into the interfaceButtons object. | |
63 interfaceButtons.appendChild(playback); | |
64 interfaceButtons.appendChild(submit); | |
65 | |
66 // Create a slider box | |
67 var sliderBox = document.createElement('div'); | |
68 sliderBox.style.width = "100%"; | |
69 sliderBox.style.height = window.innerHeight - 200+12 + 'px'; | |
70 sliderBox.style.marginBottom = '10px'; | |
71 sliderBox.id = 'slider'; | |
72 var scaleHolder = document.createElement('div'); | |
73 scaleHolder.id = "scale-holder"; | |
74 scaleHolder.style.marginLeft = "107px"; | |
75 sliderBox.appendChild(scaleHolder); | |
76 var scaleText = document.createElement('div'); | |
77 scaleText.id = "scale-text-holder"; | |
78 scaleText.style.height = "25px"; | |
79 scaleText.style.width = "100%"; | |
80 scaleHolder.appendChild(scaleText); | |
81 var scaleCanvas = document.createElement('canvas'); | |
82 scaleCanvas.id = "scale-canvas"; | |
83 scaleCanvas.style.marginLeft = "100px"; | |
84 scaleHolder.appendChild(scaleCanvas); | |
85 var sliderObjectHolder = document.createElement('div'); | |
86 sliderObjectHolder.id = 'slider-holder'; | |
87 sliderObjectHolder.align = "center"; | |
88 sliderBox.appendChild(sliderObjectHolder); | |
89 | |
90 // Global parent for the comment boxes on the page | |
91 var feedbackHolder = document.createElement('div'); | |
92 feedbackHolder.id = 'feedbackHolder'; | |
93 | |
94 testContent.style.zIndex = 1; | |
95 interfaceContext.insertPoint.innerHTML = null; // Clear the current schema | |
96 | |
97 // Inject into HTML | |
98 testContent.appendChild(title); // Insert the title | |
99 testContent.appendChild(pagetitle); | |
100 testContent.appendChild(interfaceButtons); | |
101 testContent.appendChild(sliderBox); | |
102 testContent.appendChild(feedbackHolder); | |
103 interfaceContext.insertPoint.appendChild(testContent); | |
104 | |
105 // Load the full interface | |
106 testState.initialise(); | |
107 testState.advanceState(); | |
108 }; | |
109 | |
110 function loadTest(page) | |
111 { | |
112 // Called each time a new test page is to be build. The page specification node is the only item passed in | |
113 var id = page.id; | |
114 | |
115 var feedbackHolder = document.getElementById('feedbackHolder'); | |
116 feedbackHolder.innerHTML = null; | |
117 | |
118 var interfaceObj = page.interfaces; | |
119 if (interfaceObj.length > 1) | |
120 { | |
121 console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node"); | |
122 } | |
123 interfaceObj = interfaceObj[0]; | |
124 if(interfaceObj.title != null) | |
125 { | |
126 document.getElementById("pageTitle").textContent = interfaceObj.title; | |
127 } | |
128 | |
129 var interfaceOptions = specification.interfaces.options.concat(interfaceObj.options); | |
130 for (var option of interfaceOptions) | |
131 { | |
132 if (option.type == "show") | |
133 { | |
134 switch(option.name) { | |
135 case "playhead": | |
136 var playbackHolder = document.getElementById('playback-holder'); | |
137 if (playbackHolder == null) | |
138 { | |
139 playbackHolder = document.createElement('div'); | |
140 playbackHolder.style.width = "100%"; | |
141 playbackHolder.align = 'center'; | |
142 playbackHolder.appendChild(interfaceContext.playhead.object); | |
143 feedbackHolder.appendChild(playbackHolder); | |
144 } | |
145 break; | |
146 case "page-count": | |
147 var pagecountHolder = document.getElementById('page-count'); | |
148 if (pagecountHolder == null) | |
149 { | |
150 pagecountHolder = document.createElement('div'); | |
151 pagecountHolder.id = 'page-count'; | |
152 } | |
153 pagecountHolder.innerHTML = '<span>Page '+(page.presentedId+1)+' of '+specification.pages.length+'</span>'; | |
154 var inject = document.getElementById('interface-buttons'); | |
155 inject.appendChild(pagecountHolder); | |
156 break; | |
157 case "volume": | |
158 if (document.getElementById('master-volume-holder') == null) | |
159 { | |
160 feedbackHolder.appendChild(interfaceContext.volume.object); | |
161 } | |
162 break; | |
163 } | |
164 } | |
165 } | |
166 | |
167 // Delete outside reference | |
168 var outsideReferenceHolder = document.getElementById('outside-reference'); | |
169 if (outsideReferenceHolder != null) { | |
170 document.getElementById('interface-buttons').removeChild(outsideReferenceHolder); | |
171 } | |
172 | |
173 var sliderBox = document.getElementById('slider-holder'); | |
174 sliderBox.innerHTML = null; | |
175 | |
176 var commentBoxPrefix = "Comment on track"; | |
177 if (interfaceObj.commentBoxPrefix != undefined) { | |
178 commentBoxPrefix = interfaceObj.commentBoxPrefix; | |
179 } | |
180 var loopPlayback = page.loop; | |
181 | |
182 $(page.commentQuestions).each(function(index,element) { | |
183 var node = interfaceContext.createCommentQuestion(element); | |
184 feedbackHolder.appendChild(node.holder); | |
185 }); | |
186 | |
187 // Find all the audioElements from the audioHolder | |
188 var label = 0; | |
189 $(page.audioElements).each(function(index,element){ | |
190 // Find URL of track | |
191 // In this jQuery loop, variable 'this' holds the current audioElement. | |
192 | |
193 var audioObject = audioEngineContext.newTrack(element); | |
194 if (element.type == 'outside-reference') | |
195 { | |
196 // Construct outside reference; | |
197 var orNode = new outsideReferenceDOM(audioObject,index,document.getElementById('interface-buttons')); | |
198 audioObject.bindInterface(orNode); | |
199 } else { | |
200 // Create a slider per track | |
201 var sliderObj = new sliderObject(audioObject,label); | |
202 | |
203 if (typeof page.initialPosition === "number") | |
204 { | |
205 // Set the values | |
206 sliderObj.slider.value = page.initalPosition; | |
207 } else { | |
208 // Distribute it randomnly | |
209 sliderObj.slider.value = Math.random(); | |
210 } | |
211 sliderBox.appendChild(sliderObj.holder); | |
212 audioObject.bindInterface(sliderObj); | |
213 interfaceContext.createCommentBox(audioObject); | |
214 label += 1; | |
215 } | |
216 | |
217 }); | |
218 if (page.showElementComments) | |
219 { | |
220 interfaceContext.showCommentBoxes(feedbackHolder,true); | |
221 } | |
222 // Auto-align | |
223 resizeWindow(null); | |
224 } | |
225 | |
226 function sliderObject(audioObject,label) | |
227 { | |
228 // An example node, you can make this however you want for each audioElement. | |
229 // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following | |
230 // You attach them by calling audioObject.bindInterface( ) | |
231 this.parent = audioObject; | |
232 | |
233 this.holder = document.createElement('div'); | |
234 this.title = document.createElement('div'); | |
235 this.slider = document.createElement('input'); | |
236 this.play = document.createElement('button'); | |
237 | |
238 this.holder.className = 'track-slider'; | |
239 this.holder.style.width = window.innerWidth-200 + 'px'; | |
240 this.holder.appendChild(this.title); | |
241 this.holder.appendChild(this.slider); | |
242 this.holder.appendChild(this.play); | |
243 this.holder.setAttribute('trackIndex',audioObject.id); | |
244 this.title.textContent = label; | |
245 this.title.className = 'track-slider-title'; | |
246 | |
247 this.slider.type = "range"; | |
248 this.slider.className = "track-slider-range track-slider-not-moved"; | |
249 this.slider.min = "0"; | |
250 this.slider.max = "1"; | |
251 this.slider.step = "0.01"; | |
252 this.slider.style.width = window.innerWidth-420 + 'px'; | |
253 this.slider.onchange = function() | |
254 { | |
255 var time = audioEngineContext.timer.getTestTime(); | |
256 var id = Number(this.parentNode.getAttribute('trackIndex')); | |
257 audioEngineContext.audioObjects[id].metric.moved(time,this.value); | |
258 console.log('slider '+id+' moved to '+this.value+' ('+time+')'); | |
259 $(this).removeClass('track-slider-not-moved'); | |
260 }; | |
261 | |
262 this.play.className = 'track-slider-button'; | |
263 this.play.textContent = "Loading..."; | |
264 this.play.value = audioObject.id; | |
265 this.play.disabled = true; | |
266 this.play.onclick = function(event) | |
267 { | |
268 var id = Number(event.currentTarget.value); | |
269 //audioEngineContext.metric.sliderPlayed(id); | |
270 audioEngineContext.play(id); | |
271 }; | |
272 this.resize = function(event) | |
273 { | |
274 this.holder.style.width = window.innerWidth-200 + 'px'; | |
275 this.slider.style.width = window.innerWidth-420 + 'px'; | |
276 }; | |
277 this.enable = function() | |
278 { | |
279 // This is used to tell the interface object that playback of this node is ready | |
280 this.play.disabled = false; | |
281 this.play.textContent = "Play"; | |
282 $(this.slider).removeClass('track-slider-disabled'); | |
283 }; | |
284 this.updateLoading = function(progress) | |
285 { | |
286 // progress is a value from 0 to 100 indicating the current download state of media files | |
287 }; | |
288 this.startPlayback = function() | |
289 { | |
290 // Called when playback has begun | |
291 $(".track-slider").removeClass('track-slider-playing'); | |
292 $(this.holder).addClass('track-slider-playing'); | |
293 var outsideReference = document.getElementById('outside-reference'); | |
294 if (outsideReference != null) { | |
295 $(outsideReference).removeClass('track-slider-playing'); | |
296 } | |
297 }; | |
298 this.stopPlayback = function() | |
299 { | |
300 // Called when playback has stopped. This gets called even if playback never started! | |
301 $(this.holder).removeClass('track-slider-playing'); | |
302 }; | |
303 this.getValue = function() | |
304 { | |
305 // Return the current value of the object. If there is no value, return 0 | |
306 return this.slider.value; | |
307 }; | |
308 this.getPresentedId = function() | |
309 { | |
310 // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale | |
311 return this.title.textContent; | |
312 }; | |
313 this.canMove = function() | |
314 { | |
315 // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale. | |
316 // These are checked primarily if the interface check option 'fragmentMoved' is enabled. | |
317 return true; | |
318 }; | |
319 this.exportXMLDOM = function(audioObject) { | |
320 // Called by the audioObject holding this element to export the interface <value> node. | |
321 // If there is no value node (such as outside reference), return null | |
322 // 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 | |
323 // Use storage.document.createElement('value'); to generate the XML node. | |
324 var node = storage.document.createElement('value'); | |
325 node.textContent = this.slider.value; | |
326 return node; | |
327 }; | |
328 }; | |
329 | |
330 function outsideReferenceDOM(audioObject,index,inject) | |
331 { | |
332 this.parent = audioObject; | |
333 this.outsideReferenceHolder = document.createElement('button'); | |
334 this.outsideReferenceHolder.id = 'outside-reference'; | |
335 this.outsideReferenceHolder.className = 'outside-reference'; | |
336 this.outsideReferenceHolder.setAttribute('track-id',index); | |
337 this.outsideReferenceHolder.textContent = "Play Reference"; | |
338 this.outsideReferenceHolder.disabled = true; | |
339 | |
340 this.outsideReferenceHolder.onclick = function(event) | |
341 { | |
342 audioEngineContext.play(event.currentTarget.getAttribute('track-id')); | |
343 }; | |
344 inject.appendChild(this.outsideReferenceHolder); | |
345 this.enable = function() | |
346 { | |
347 if (this.parent.state == 1) | |
348 { | |
349 this.outsideReferenceHolder.disabled = false; | |
350 } | |
351 }; | |
352 this.updateLoading = function(progress) | |
353 { | |
354 if (progress != 100) | |
355 { | |
356 progress = String(progress); | |
357 progress = progress.split('.')[0]; | |
358 this.outsideReferenceHolder[0].children[0].textContent = progress+'%'; | |
359 } else { | |
360 this.outsideReferenceHolder[0].children[0].textContent = "Play Reference"; | |
361 } | |
362 }; | |
363 this.startPlayback = function() | |
364 { | |
365 // Called when playback has begun | |
366 $('.track-slider').removeClass('track-slider-playing'); | |
367 $('.comment-div').removeClass('comment-box-playing'); | |
368 $(this.outsideReferenceHolder).addClass('track-slider-playing'); | |
369 }; | |
370 this.stopPlayback = function() | |
371 { | |
372 // Called when playback has stopped. This gets called even if playback never started! | |
373 $(this.outsideReferenceHolder).removeClass('track-slider-playing'); | |
374 }; | |
375 this.exportXMLDOM = function(audioObject) | |
376 { | |
377 return null; | |
378 }; | |
379 this.getValue = function() | |
380 { | |
381 return 0; | |
382 }; | |
383 this.getPresentedId = function() | |
384 { | |
385 return 'reference'; | |
386 }; | |
387 this.canMove = function() | |
388 { | |
389 return false; | |
390 }; | |
391 } | |
392 | |
393 function resizeWindow(event) | |
394 { | |
395 // Called on every window resize event, use this to scale your page properly | |
396 | |
397 var numObj = document.getElementsByClassName('track-slider').length; | |
398 var totalHeight = (numObj * 125)-25; | |
399 document.getElementById('scale-holder').style.width = window.innerWidth-220 + 'px'; | |
400 var canvas = document.getElementById('scale-canvas'); | |
401 canvas.width = window.innerWidth-420; | |
402 canvas.height = totalHeight; | |
403 for (var i in audioEngineContext.audioObjects) | |
404 { | |
405 if (audioEngineContext.audioObjects[i].specification.type != 'outside-reference'){ | |
406 audioEngineContext.audioObjects[i].interfaceDOM.resize(event); | |
407 } | |
408 } | |
409 document.getElementById("slider").style.height = totalHeight+50+'px'; | |
410 drawScale(); | |
411 } | |
412 | |
413 function drawScale() | |
414 { | |
415 var interfaceObj = testState.currentStateMap.interfaces[0]; | |
416 var scales = testState.currentStateMap.interfaces[0].scales; | |
417 scales = scales.sort(function(a,b) { | |
418 return a.position - b.position; | |
419 }); | |
420 var canvas = document.getElementById('scale-canvas'); | |
421 var ctx = canvas.getContext("2d"); | |
422 var height = canvas.height; | |
423 var width = canvas.width; | |
424 var textHolder = document.getElementById('scale-text-holder'); | |
425 textHolder.innerHTML = null; | |
426 ctx.fillStyle = "#000000"; | |
427 ctx.setLineDash([1,4]); | |
428 for (var scale of scales) | |
429 { | |
430 var posPercent = scale.position / 100.0; | |
431 var posPix = Math.round(width * posPercent); | |
432 if(posPix<=0){posPix=1;} | |
433 if(posPix>=width){posPix=width-1;} | |
434 ctx.moveTo(posPix,0); | |
435 ctx.lineTo(posPix,height); | |
436 ctx.stroke(); | |
437 | |
438 var text = document.createElement('div'); | |
439 text.align = "center"; | |
440 var textC = document.createElement('span'); | |
441 textC.textContent = scale.text; | |
442 text.appendChild(textC); | |
443 text.className = "scale-text"; | |
444 textHolder.appendChild(text); | |
445 text.style.width = Math.ceil($(text).width())+'px'; | |
446 text.style.left = (posPix+100-($(text).width()/2)) +'px'; | |
447 } | |
448 } | |
449 | |
450 function buttonSubmitClick() // TODO: Only when all songs have been played! | |
451 { | |
452 var checks = []; | |
453 checks = checks.concat(testState.currentStateMap.interfaces[0].options); | |
454 checks = checks.concat(specification.interfaces.options); | |
455 var canContinue = true; | |
456 | |
457 // Check that the anchor and reference objects are correctly placed | |
458 if (interfaceContext.checkHiddenAnchor() == false) {return;} | |
459 if (interfaceContext.checkHiddenReference() == false) {return;} | |
460 | |
461 for (var i=0; i<checks.length; i++) { | |
462 if (checks[i].type == 'check') | |
463 { | |
464 switch(checks[i].name) { | |
465 case 'fragmentPlayed': | |
466 // Check if all fragments have been played | |
467 var checkState = interfaceContext.checkAllPlayed(); | |
468 if (checkState == false) {canContinue = false;} | |
469 break; | |
470 case 'fragmentFullPlayback': | |
471 // Check all fragments have been played to their full length | |
472 var checkState = interfaceContext.checkAllPlayed(); | |
473 if (checkState == false) {canContinue = false;} | |
474 console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead'); | |
475 break; | |
476 case 'fragmentMoved': | |
477 // Check all fragment sliders have been moved. | |
478 var checkState = interfaceContext.checkAllMoved(); | |
479 if (checkState == false) {canContinue = false;} | |
480 break; | |
481 case 'fragmentComments': | |
482 // Check all fragment sliders have been moved. | |
483 var checkState = interfaceContext.checkAllCommented(); | |
484 if (checkState == false) {canContinue = false;} | |
485 break; | |
486 //case 'scalerange': | |
487 // Check the scale is used to its full width outlined by the node | |
488 //var checkState = interfaceContext.checkScaleRange(); | |
489 //if (checkState == false) {canContinue = false;} | |
490 // break; | |
491 default: | |
492 console.log("WARNING - Check option "+checks[i].check+" is not supported on this interface"); | |
493 break; | |
494 } | |
495 | |
496 } | |
497 if (!canContinue) {break;} | |
498 } | |
499 | |
500 if (canContinue) { | |
501 if (audioEngineContext.status == 1) { | |
502 var playback = document.getElementById('playback-button'); | |
503 playback.click(); | |
504 // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options | |
505 } else | |
506 { | |
507 if (audioEngineContext.timer.testStarted == false) | |
508 { | |
509 alert('You have not started the test! Please press start to begin the test!'); | |
510 return; | |
511 } | |
512 } | |
513 testState.advanceState(); | |
514 } | |
515 } | |
516 | |
517 function pageXMLSave(store, pageSpecification) | |
518 { | |
519 // MANDATORY | |
520 // Saves a specific test page | |
521 // You can use this space to add any extra nodes to your XML <audioHolder> saves | |
522 // Get the current <page> information in store (remember to appendChild your data to it) | |
523 // pageSpecification is the current page node configuration | |
524 // To create new XML nodes, use storage.document.createElement(); | |
525 } |