example.js revision f2477e01787aa58f445919b809d89e252beef54f
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 = '//commondatastorage.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