1// Copyright (c) 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7var naclModule = null; 8var presets = [ 9 [[15.2,32.1,7.6],[3,0.329,0.15,0.321,0.145,0.709,3,2,4,0.269,0.662],[0,"#000000",3,"#f5f5c1",12,"#158a34",68,"#89e681",100]], 10 [[15.2,32.1,5],[3,0.273,0.117,0.288,0.243,0.348,3,2,4,0.269,0.662],[1,"#000000",3,"#f5f5c1",8,"#158a34",17,"#89e681",20]], 11 [[4,12,1],[2,0.115,0.269,0.523,0.34,0.746,3,4,4,0.028,0.147],[0,"#36065e",0,"#c24242",77,"#8a19b0",91,"#ff9900",99,"#f5c816",99]], 12 [[4,12,1],[3,0.12,0.218,0.267,0.365,0.445,3,4,4,0.028,0.147],[0,"#000000",0,"#0f8a84",38,"#f5f5c1",43,"#158a34",70,"#89e681",100]], 13 [[4,12,1],[0,0.09,0.276,0.27,0.365,0.445,1,4,4,0.028,0.147],[0,"#93afd9",11,"#9cf0ff",92,"#edfdff",100]], 14 [[10.4,12,1],[2,0.082,0.302,0.481,0.35,0.749,2,3,4,0.028,0.147],[0,"#000000",11,"#ffffff",22,"#19a68a",85,"#6b0808",98]], 15 [[7.8,27.2,2.6],[3,0.21,0.714,0.056,0.175,0.838,2,0,2,0.132,0.311],[0,"#0a1340",0,"#ffffff",55,"#4da8a3",83,"#2652ab",99,"#2f1e75",46]], 16 [[4,12,1],[2,0.115,0.269,0.496,0.34,0.767,3,4,4,0.028,0.147],[0,"#b8cfcf",0,"#3f5a5c",77,"#1a330a",91,"#c0e0dc",99]], 17 [[10.6,31.8,1],[1,0.157,0.092,0.256,0.098,0.607,3,4,4,0.015,0.34],[0,"#4d3e3e",0,"#9a1ac9",77,"#aaf09e",100]], 18 [[3.2,34,14.8],[1,0.26,0.172,0.370,0.740,0.697,1,1,4,0.772,0.280],[0,"#3b8191",18,"#66f24f",82,"#698ffe",100]], 19 [[15.3,5.5,33.2],[1,0.746,0.283,0.586,0.702,0.148,1,2,0,0.379,0.633],[1,"#42ae80",77,"#fd1e2e",79,"#58103f",93,"#cf9750",96]], 20 [[2.5,3.5,7.7],[3,0.666,0.779,0.002,0.558,0.786,3,1,3,0.207,0.047],[0,"#a2898d",78,"#60d14e",86,"#5c4dea",90]], 21[[7.6,7.6,9.0],[1,0.158,0.387,0.234,0.810,0.100,3,0,2,0.029,0.533],[0,"#568b8a",5,"#18ce42",92]] 22]; 23 24var palettePresets = [ 25 [], // Placeholder for the palette of the currently selected preset. 26 [0, '#ffffff', 0, '#000000', 100], 27 [0, '#000000', 0, '#ffffff', 100], 28 [0, '#000000', 0, '#ff00ff', 50, '#000000', 100], 29 [0,"#000000",0,"#0f8a84",38,"#f5f5c1",43,"#158a34",70,"#89e681",100], 30 [1,"#000000",3,"#f5f5c1",8,"#158a34",17,"#89e681",20], 31 [0,"#36065e",0,"#c24242",77,"#8a19b0",91,"#ff9900",99,"#f5c816",99], 32 [0,"#93afd9",11,"#9cf0ff",92,"#edfdff",100], 33 [0,"#000000",11,"#ffffff",22,"#19a68a",85,"#6b0808",98], 34 [0,"#0a1340",0,"#ffffff",55,"#4da8a3",83,"#2652ab",99,"#2f1e75",46], 35 [0,"#b8cfcf",0,"#3f5a5c",77,"#1a330a",91,"#c0e0dc",99], 36 [0,"#4d3e3e",0,"#9a1ac9",77,"#aaf09e",100], 37 [1,"#52d2a1",3,"#c7c5ca",46,"#be6e88",72,"#f5a229",79,"#f0e0d1",94,"#6278d8",100] 38]; 39 40/** 41 * A helper function to abbreviate getElementById. 42 * 43 * @param {string} elementId The id to get. 44 * @return {Element} 45 */ 46function $(elementId) { 47 return document.getElementById(elementId); 48} 49 50/** 51 * MIME type for PNaCl 52 * 53 * @return {string} MIME type 54 */ 55function PNaClmimeType() { 56 return 'application/x-pnacl'; 57} 58 59/** 60 * Check if the browser supports PNaCl. 61 * 62 * @return {bool} 63 */ 64function browserSupportsPNaCl() { 65 var mimetype = PNaClmimeType(); 66 return navigator.mimeTypes[mimetype] !== undefined; 67} 68 69/** 70 * Get the URL for Google Cloud Storage. 71 * 72 * @param {string} name The relative path to the file. 73 * @return {string} 74 */ 75function getDataURL(name) { 76 var revision = '236779'; 77 var baseUrl = '//storage.googleapis.com/gonacl/demos/publish/'; 78 return baseUrl + revision + '/smoothlife/' + name; 79} 80 81/** 82 * Create the Native Client <embed> element as a child of the DOM element 83 * named "listener". 84 * 85 * @param {string} name The name of the example. 86 * @param {number} width The width to create the plugin. 87 * @param {number} height The height to create the plugin. 88 * @param {Object} attrs Dictionary of attributes to set on the module. 89 */ 90function createNaClModule(name, width, height, attrs) { 91 var moduleEl = document.createElement('embed'); 92 moduleEl.setAttribute('name', 'nacl_module'); 93 moduleEl.setAttribute('id', 'nacl_module'); 94 moduleEl.setAttribute('width', width); 95 moduleEl.setAttribute('height', height); 96 moduleEl.setAttribute('path', ''); 97 moduleEl.setAttribute('src', getDataURL(name + '.nmf')); 98 moduleEl.setAttribute('type', PNaClmimeType()); 99 100 // Add any optional arguments 101 if (attrs) { 102 for (var key in attrs) { 103 moduleEl.setAttribute(key, attrs[key]); 104 } 105 } 106 107 // The <EMBED> element is wrapped inside a <DIV>, which has both a 'load' 108 // and a 'message' event listener attached. This wrapping method is used 109 // instead of attaching the event listeners directly to the <EMBED> element 110 // to ensure that the listeners are active before the NaCl module 'load' 111 // event fires. 112 var listenerDiv = $('listener'); 113 listenerDiv.appendChild(moduleEl); 114} 115 116/** 117 * Add the default event listeners to the element with id "listener". 118 */ 119function attachDefaultListeners() { 120 var listenerDiv = $('listener'); 121 listenerDiv.addEventListener('load', moduleDidLoad, true); 122 listenerDiv.addEventListener('error', moduleLoadError, true); 123 listenerDiv.addEventListener('progress', moduleLoadProgress, true); 124 listenerDiv.addEventListener('message', handleMessage, true); 125 listenerDiv.addEventListener('crash', handleCrash, true); 126 attachListeners(); 127} 128 129/** 130 * Called when the Browser can not communicate with the Module 131 * 132 * This event listener is registered in attachDefaultListeners above. 133 * 134 * @param {Object} event 135 */ 136function handleCrash(event) { 137 if (naclModule.exitStatus == -1) { 138 updateStatus('CRASHED'); 139 } else { 140 updateStatus('EXITED [' + naclModule.exitStatus + ']'); 141 } 142} 143 144/** 145 * Called when the NaCl module is loaded. 146 * 147 * This event listener is registered in attachDefaultListeners above. 148 */ 149function moduleDidLoad() { 150 var bar = $('progress-bar'); 151 bar.style.width = 100; 152 naclModule = $('nacl_module'); 153 hideStatus(); 154 setSize(256); 155 setThreadCount(2); 156 setMaxScale(1); 157 loadPreset(0); 158} 159 160/** 161 * Hide the status field and progress bar. 162 */ 163function hideStatus() { 164 $('loading-cover').style.display = 'none'; 165} 166 167/** 168 * Called when the plugin fails to load. 169 * 170 * @param {Object} event 171 */ 172function moduleLoadError(event) { 173 updateStatus('Load failed.'); 174} 175 176/** 177 * Called when the plugin reports progress events. 178 * 179 * @param {Object} event 180 */ 181function moduleLoadProgress(event) { 182 $('progress').style.display = 'block'; 183 184 var loadPercent = 0.0; 185 var bar = $('progress-bar'); 186 187 if (event.lengthComputable && event.total > 0) { 188 loadPercent = event.loaded / event.total * 100.0; 189 } else { 190 // The total length is not yet known. 191 loadPercent = 10; 192 } 193 bar.style.width = loadPercent + "%"; 194} 195 196/** 197 * If the element with id 'statusField' exists, then set its HTML to the status 198 * message as well. 199 * 200 * @param {string} opt_message The message to set. 201 */ 202function updateStatus(opt_message) { 203 var statusField = $('statusField'); 204 if (statusField) { 205 statusField.style.display = 'block'; 206 statusField.textContent = opt_message; 207 } 208} 209 210/** 211 * Add event listeners after the NaCl module has loaded. These listeners will 212 * forward messages to the NaCl module via postMessage() 213 */ 214function attachListeners() { 215 $('preset').addEventListener('change', loadSelectedPreset); 216 $('palette').addEventListener('change', loadSelectedPalette); 217 $('reset').addEventListener('click', loadSelectedPreset); 218 $('clear').addEventListener('click', function() { clear(0); }); 219 $('splat').addEventListener('click', function() { splat(); }); 220 $('brushSizeRange').addEventListener('change', function() { 221 var radius = parseFloat(this.value); 222 setBrushSize(radius, 1.0); 223 $('brushSize').textContent = radius.toFixed(1); 224 }); 225 $('threadCount').addEventListener('change', function() { 226 setThreadCount(parseInt(this.value, 10)); 227 }); 228 $('simSize').addEventListener('change', function() { 229 setSize(parseInt(this.value, 10)); 230 // changing the simulation size clears everything, so reset. 231 loadSelectedPreset(); 232 }); 233 $('scale').addEventListener('change', function() { 234 var scale = parseFloat(this.value); 235 setMaxScale(scale); 236 updateScaleText(); 237 }); 238 239 setInterval(function() { 240 if (!naclModule) 241 return; 242 243 // Get the size of the embed and the size of the simulation, and 244 // determine the maximum scale. 245 var rect = naclModule.getBoundingClientRect(); 246 var embedScale = Math.min(rect.width, rect.height); 247 var simSize = parseInt($('simSize').value, 10); 248 var maxScale = embedScale / simSize; 249 var scaleEl = $('scale'); 250 251 if (scaleEl.max != maxScale) { 252 var minScale = scaleEl.min; 253 scaleEl.disabled = false; 254 var clampedScale = Math.min(maxScale, Math.max(minScale, scaleEl.value)); 255 256 scaleEl.max = maxScale; 257 258 // Normally the minScale is 0.5, but sometimes maxScale can be less 259 // than that. In that case, set the minScale to maxScale. 260 scaleEl.min = Math.min(maxScale, 0.5); 261 262 // Reset the value so the input range updates. 263 scaleEl.value = 0; 264 scaleEl.value = clampedScale; 265 updateScaleText(); 266 267 // If max scale is too small, disable zoom. 268 scaleEl.disabled = maxScale < minScale; 269 } 270 }, 100); 271 272 function updateScaleText() { 273 var percent = (parseFloat($('scale').value) * 100).toFixed(0) + '%' 274 $('scaleValue').textContent = percent; 275 } 276} 277 278function loadSelectedPreset() { 279 loadPreset($('preset').value); 280} 281 282function loadPreset(index) { 283 var preset = presets[index]; 284 var selectedPalette = $('palette').value; 285 286 clear(0); 287 setKernel.apply(null, preset[0]); 288 setSmoother.apply(null, preset[1]); 289 // Only change the palette if it is set to "Default", which means to use 290 // the preset default palette. 291 if (selectedPalette == 0) 292 setPalette.apply(null, preset[2]); 293 splat(); 294 295 // Save the current palette in slot 0: "Default". 296 palettePresets[0] = preset[2]; 297} 298 299function loadSelectedPalette() { 300 loadPalette($('palette').value); 301} 302 303function loadPalette(index) { 304 setPalette.apply(null, palettePresets[index]); 305} 306 307function clear(color) { 308 naclModule.postMessage({cmd: 'clear', color: color}); 309} 310 311function setSize(size) { 312 naclModule.postMessage({cmd: 'setSize', size: size}); 313} 314 315function setMaxScale(scale) { 316 naclModule.postMessage({cmd: 'setMaxScale', scale: scale}); 317} 318 319function setThreadCount(threadCount) { 320 naclModule.postMessage({cmd: 'setThreadCount', threadCount: threadCount}); 321} 322 323 324function setKernel(discRadius, ringRadius, blendRadius) { 325 naclModule.postMessage({ 326 cmd: 'setKernel', 327 discRadius: discRadius, 328 ringRadius: ringRadius, 329 blendRadius: blendRadius}); 330} 331 332function setSmoother(type, dt, b1, d1, b2, d2, mode, sigmoid, mix, sn, sm) { 333 naclModule.postMessage({ 334 cmd: 'setSmoother', 335 type: type, dt: dt, 336 b1: b1, d1: d1, b2: b2, d2: d2, 337 mode: mode, sigmoid: sigmoid, mix: mix, 338 sn: sn, sm: sm}); 339} 340 341function setPalette() { 342 var repeating = arguments[0] !== 0; 343 var colors = [] 344 var stops = [] 345 for (var i = 1; i < arguments.length; i += 2) { 346 colors.push(arguments[i]); 347 stops.push(arguments[i + 1]); 348 } 349 naclModule.postMessage({ 350 cmd: 'setPalette', 351 repeating: repeating, 352 colors: colors, 353 stops: stops}); 354} 355 356function splat() { 357 naclModule.postMessage({cmd: 'splat'}); 358} 359 360function setBrushSize(radius, color) { 361 naclModule.postMessage({cmd: 'setBrush', radius: radius, color: color}); 362} 363 364 365/** 366 * Handle a message coming from the NaCl module. 367 * @param {Object} message_event 368 */ 369function handleMessage(message_event) { 370 // Update FPS 371 $('fps').textContent = message_event.data.toFixed(1); 372} 373 374/** 375 * Listen for the DOM content to be loaded. This event is fired when parsing of 376 * the page's document has finished. 377 */ 378document.addEventListener('DOMContentLoaded', function() { 379 updateStatus('Loading...'); 380 if (!browserSupportsPNaCl()) { 381 updateStatus('Browser does not support PNaCl or PNaCl is disabled'); 382 } else if (naclModule == null) { 383 createNaClModule('smoothnacl', '100%', '100%'); 384 attachDefaultListeners(); 385 } else { 386 // It's possible that the Native Client module onload event fired 387 // before the page's onload event. In this case, the status message 388 // will reflect 'SUCCESS', but won't be displayed. This call will 389 // display the current message. 390 updateStatus('Waiting.'); 391 } 392}); 393