385ff2eeb7c0ce7d6f16cfce02e35dfa1c5fec08
[mandelbrot-web.git] / js / mandelbrot.js
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 var gMainCanvas, gMainContext;
6 var gColorPalette = [];
7 var gStartTime = 0;
8 var gCurrentImageData;
9 var gLastImageData;
10
11 function Startup() {
12   gMainCanvas = document.getElementById("mbrotImage");
13   gMainContext = gMainCanvas.getContext("2d");
14
15   gMainCanvas.addEventListener("mouseup", imgEvHandler, false);
16   gMainCanvas.addEventListener("mousedown", imgEvHandler, false);
17   gMainCanvas.addEventListener("mousemove", imgEvHandler, false);
18   gMainCanvas.addEventListener("touchstart", imgEvHandler, false);
19   gMainCanvas.addEventListener("touchend", imgEvHandler, false);
20   gMainCanvas.addEventListener("touchcancel", imgEvHandler, false);
21   gMainCanvas.addEventListener("touchleave", imgEvHandler, false);
22   gMainCanvas.addEventListener("touchmove", imgEvHandler, false);
23
24   var initTile = new Image();
25   initTile.src = "style/initial-overview.png";
26   gMainContext.drawImage(initTile, 0, 0);
27 }
28
29 function getAdjustVal(aName) {
30   var value;
31   switch (aName) {
32     case "image.width":
33     case "image.height":
34       value = 0;
35       try {
36         value = document.getElementById(aName.replace(".", "_")).value;
37       }
38       catch (e) { }
39       if ((value < 10) || (value > 5000)) {
40         value = 300;
41         //document.getElementById(aName.replace(".", "_")).value = value;
42       }
43       return value;
44     case "last_image.Cr_*":
45       var Cr_min = -2.0;
46       var Cr_max = 1.0;
47       try {
48         Cr_min = parseFloat(document.getElementById("Cr_min").value);
49         Cr_max = parseFloat(document.getElementById("Cr_max").value);
50       }
51       catch (e) { }
52       if ((Cr_min < -3) || (Cr_min > 2) ||
53           (Cr_max < -3) || (Cr_max > 2) || (Cr_min >= Cr_max)) {
54         Cr_min = -2.0; Cr_max = 1.0;
55       }
56       document.getElementById("Cr_min").value = Cr_min;
57       document.getElementById("Cr_max").value = Cr_max;
58       return {Cr_min: Cr_min, Cr_max: Cr_max};
59     case "last_image.Ci_*":
60       var Ci_min = -1.5;
61       var Ci_max = 1.5;
62       try {
63         Ci_min = parseFloat(document.getElementById("Ci_min").value);
64         Ci_max = parseFloat(document.getElementById("Ci_max").value);
65       }
66       catch (e) { }
67       if ((Ci_min < -2.5) || (Ci_min > 2.5) ||
68           (Ci_max < -2.5) || (Ci_max > 2.5) || (Ci_min >= Ci_max)) {
69         Ci_min = -1.5; Ci_max = 1.5;
70       }
71       document.getElementById("Ci_min").value = Ci_min;
72       document.getElementById("Ci_max").value = Ci_max;
73       return {Ci_min: Ci_min, Ci_max: Ci_max};
74     case "iteration_max":
75       value = 500;
76       try {
77         value = document.getElementById("iterMax").value;
78       }
79       catch (e) {
80         setIter(value);
81       }
82       if (value < 10 || value > 10000) {
83         value = 500;
84         setIter(value);
85       }
86       return value;
87     case "use_algorithm":
88       value = "numeric";
89       try {
90         value = document.getElementById("algorithm").value;
91       }
92       catch (e) {
93         setAlgorithm(value);
94       }
95       return value;
96    case "color_palette":
97       value = "kairo";
98       try {
99         value = document.getElementById("palette").value;
100       }
101       catch(e) {
102         setPalette(value);
103       }
104       return value;
105    case "syncProportions":
106       value = true;
107       try {
108         value = document.getElementById("proportional").value;
109       }
110       catch(e) {
111         document.getElementById("proportional").value = value;
112       }
113       return value;
114     default:
115       return false;
116   }
117 }
118
119 function setVal(aName, aValue) {
120   switch (aName) {
121     case "image.width":
122     case "image.height":
123       document.getElementById(aName.replace(".", "_")).value = value;
124       break;
125     case "last_image.Cr_*":
126       document.getElementById("Cr_min").value = aValue.Cr_min;
127       document.getElementById("Cr_max").value = aValue.Cr_max;
128       break;
129     case "last_image.Ci_*":
130       document.getElementById("Ci_min").value = aValue.Ci_min;
131       document.getElementById("Ci_max").value = aValue.Ci_max;
132       break;
133     case "iteration_max":
134       setIter(aValue);
135       break;
136     case "use_algorithm":
137       setAlgorithm(aValue);
138       break;
139    case "color_palette":
140       setPalette(valueaValue);
141       break;
142    case "syncProportions":
143       document.getElementById("proportional").value = aValue;
144       break;
145   }
146 }
147
148 function adjustCoordsAndDraw(aC_min, aC_max) {
149   var iWidth = getAdjustVal("image.width");
150   var iHeight = getAdjustVal("image.height");
151
152   // correct coordinates
153   if (aC_min.r < -2)
154     aC_min.r = -2;
155   if (aC_max.r > 2)
156     aC_max.r = 2;
157   if ((aC_min.r > 2) || (aC_max.r < -2) || (aC_min.r >= aC_max.r)) {
158     aC_min.r = -2.0; aC_max.r = 1.0;
159   }
160   if (aC_min.i < -2)
161     aC_min.i = -2;
162   if (aC_max.i > 2)
163     aC_max.i = 2;
164   if ((aC_min.i > 2) || (aC_max.i < -2) || (aC_min.i >= aC_max.i)) {
165     aC_min.i = -1.3; aC_max.i = 1.3;
166   }
167
168   var CWidth = aC_max.r - aC_min.r;
169   var CHeight = aC_max.i - aC_min.i;
170   var C_mid = new complex(aC_min.r + CWidth / 2, aC_min.i + CHeight / 2);
171
172   var CRatio = Math.max(CWidth / iWidth, CHeight / iHeight);
173
174   setVal("last_image.Cr_*", {Cr_min: C_mid.r - iWidth * CRatio / 2,
175                              Cr_max: C_mid.r + iWidth * CRatio / 2});
176   setVal("last_image.Ci_*", {Ci_min: C_mid.i - iHeight * CRatio / 2,
177                              Ci_max: C_mid.i + iHeight * CRatio / 2});
178
179   drawImage();
180 }
181
182 function drawImage() {
183   var canvas = gMainCanvas;
184   var context = gMainContext;
185
186   document.getElementById("calcTime").textContent = "--";
187
188   if (gCurrentImageData) {
189     gLastImageData = gCurrentImageData;
190     document.getElementById("backButton").disabled = false;
191   }
192
193   gColorPalette = getColorPalette(document.getElementById("palette").value);
194
195   var Cr_vals = getAdjustVal("last_image.Cr_*");
196   var Cr_min = Cr_vals.Cr_min;
197   var Cr_max = Cr_vals.Cr_max;
198
199   var Ci_vals = getAdjustVal("last_image.Ci_*");
200   var Ci_min = Ci_vals.Ci_min;
201   var Ci_max = Ci_vals.Ci_max;
202
203   var iterMax = getAdjustVal("iteration_max");
204   var algorithm = getAdjustVal("use_algorithm");
205
206   var iWidth = canvas.width;
207   if ((iWidth < 10) || (iWidth > 5000)) {
208     iWidth = 300;
209     canvas.width = iWidth;
210   }
211   var iHeight = canvas.height;
212   if ((iHeight < 10) || (iHeight > 5000)) {
213     iHeight = 300;
214     canvas.height = iHeight;
215   }
216
217   gCurrentImageData = {
218     C_min: new complex(Cr_min, Ci_min),
219     C_max: new complex(Cr_max, Ci_max),
220     iWidth: iWidth,
221     iHeight: iHeight,
222     iterMax: iterMax
223   };
224
225   context.fillStyle = "rgba(255, 255, 255, 127)";
226   context.fillRect(0, 0, canvas.width, canvas.height);
227
228   gStartTime = new Date();
229
230   drawLine(0, [Cr_min, Cr_max, Ci_min, Ci_max],
231               canvas, context, iterMax, algorithm);
232 }
233
234 function drawLine(line, dimensions, canvas, context, iterMax, algorithm) {
235     var Cr_min = dimensions[0];
236     var Cr_max = dimensions[1];
237     var Cr_scale = Cr_max - Cr_min;
238
239     var Ci_min = dimensions[2];
240     var Ci_max = dimensions[3];
241     var Ci_scale = Ci_max - Ci_min;
242
243     var lines = Math.min(canvas.height - line, 8);
244     var imageData = context.createImageData(canvas.width, lines);
245     var pixels = imageData.data;
246     var idx = 0;
247     for (var img_y = line; img_y < canvas.height && img_y < line+8; img_y++)
248       for (var img_x = 0; img_x < canvas.width; img_x++) {
249         var C = new complex(Cr_min + (img_x / canvas.width) * Cr_scale,
250                             Ci_min + (img_y / canvas.height) * Ci_scale);
251         var colors = drawPoint(context, img_x, img_y, C, iterMax, algorithm);
252         pixels[idx++] = colors[0];
253         pixels[idx++] = colors[1];
254         pixels[idx++] = colors[2];
255         pixels[idx++] = colors[3];
256       }
257     context.putImageData(imageData, 0, line);
258
259     if (img_y < canvas.height)
260       setTimeout(drawLine, 0, img_y, dimensions, canvas, context, iterMax, algorithm);
261     else if (gStartTime)
262       EndCalc();
263 }
264
265 function EndCalc() {
266   var endTime = new Date();
267   var timeUsed = (endTime.getTime() - gStartTime.getTime()) / 1000;
268   document.getElementById("calcTime").textContent = timeUsed.toFixed(3) + " seconds";
269 }
270
271 function complex(aReal, aImag) {
272   this.r = aReal;
273   this.i = aImag;
274 }
275 complex.prototype = {
276   square: function() {
277     return new complex(this.r * this.r - this.i * this.i,
278                        2 * this.r * this.i);
279   },
280   dist: function() {
281     return Math.sqrt(this.r * this.r + this.i * this.i);
282   },
283   add: function(aComplex) {
284     return new complex(this.r + aComplex.r, this.i + aComplex.i);
285   }
286 }
287
288 function mandelbrotValueOO (aC, aIterMax) {
289   // this would be nice code in general but it looks like JS objects are too heavy for normal use.
290   var Z = new complex(0.0, 0.0);
291   for (var iter = 0; iter < aIterMax; iter++) {
292     Z = Z.square().add(aC);
293     if (Z.r * Z.r + Z.i * Z.i > 256) { break; }
294   }
295   return iter;
296 }
297
298 function mandelbrotValueNumeric (aC, aIterMax) {
299   // optimized numeric code for fast calculation
300   var Cr = aC.r, Ci = aC.i;
301   var Zr = 0.0, Zi = 0.0;
302   var Zr2 = Zr * Zr, Zi2 = Zi * Zi;
303   for (var iter = 0; iter < aIterMax; iter++) {
304     Zi = 2 * Zr * Zi + Ci;
305     Zr = Zr2 - Zi2 + Cr;
306
307     Zr2 = Zr * Zr; Zi2 = Zi * Zi;
308     if (Zr2 + Zi2 > 256) { break; }
309   }
310   return iter;
311 }
312
313 function getColor(aIterValue, aIterMax) {
314   var standardizedValue = Math.round(aIterValue * 1024 / aIterMax);
315   if (gColorPalette && gColorPalette.length)
316     return gColorPalette[standardizedValue];
317
318   // fallback to simple b/w if for some reason we don't have a palette
319   if (aIterValue == aIterMax)
320     return [0, 0, 0, 255];
321   else
322     return [255, 255, 255, 255];
323 }
324
325 function getColorPalette(palName) {
326   var palette = [];
327   switch (palName) {
328     case 'bw':
329       for (var i = 0; i < 1024; i++) {
330         palette[i] = [255, 255, 255, 255];
331       }
332       palette[1024] = [0, 0, 0, 255];
333       break;
334     case 'kairo':
335       // outer areas
336       for (var i = 0; i < 32; i++) {
337         var cc1 = Math.floor(i * 127 / 31);
338         var cc2 = 170 - Math.floor(i * 43 / 31);
339         palette[i] = [cc1, cc2, cc1, 255];
340       }
341       // inner areas
342       for (var i = 0; i < 51; i++) {
343         var cc = Math.floor(i * 170 / 50);
344         palette[32 + i] = [cc, 0, (170-cc), 255];
345       }
346       // corona
347       for (var i = 0; i < 101; i++) {
348         var cc = Math.floor(i * 200 / 100);
349         palette[83 + i] = [255, cc, 0, 255];
350       }
351       // inner corona
352       for (var i = 0; i < 201; i++) {
353         var cc1 = 255 - Math.floor(i * 85 / 200);
354         var cc2 = 200 - Math.floor(i * 30 / 200);
355         var cc3 = Math.floor(i * 170 / 200);
356         palette[184 + i] = [cc1, cc2, cc3, 255];
357       }
358       for (var i = 0; i < 301; i++) {
359         var cc1 = 170 - Math.floor(i * 43 / 300);
360         var cc2 = 170 + Math.floor(i * 85 / 300);
361         palette[385 + i] = [cc1, cc1, cc2, 255];
362       }
363       for (var i = 0; i < 338; i++) {
364         var cc = 127 + Math.floor(i * 128 / 337);
365         palette[686 + i] = [cc, cc, 255, 255];
366       }
367       palette[1024] = [0, 0, 0, 255];
368       break;
369     case 'rainbow-linear1':
370       for (var i = 0; i < 256; i++) {
371         palette[i] = [i, 0, 0, 255];
372         palette[256 + i] = [255, i, 0, 255];
373         palette[512 + i] = [255 - i, 255, i, 255];
374         palette[768 + i] = [i, 255-i, 255, 255];
375       }
376       palette[1024] = [0, 0, 0, 255];
377       break;
378     case 'rainbow-squared1':
379       for (var i = 0; i < 34; i++) {
380         var cc = Math.floor(i * 255 / 33);
381         palette[i] = [cc, 0, 0, 255];
382       }
383       for (var i = 0; i < 137; i++) {
384         var cc = Math.floor(i * 255 / 136);
385         palette[34 + i] = [255, cc, 0, 255];
386       }
387       for (var i = 0; i < 307; i++) {
388         var cc = Math.floor(i * 255 / 306);
389         palette[171 + i] = [255 - cc, 255, cc, 255];
390       }
391       for (var i = 0; i < 546; i++) {
392         var cc = Math.floor(i * 255 / 545);
393         palette[478 + i] = [cc, 255 - cc, 255, 255];
394       }
395       palette[1024] = [0, 0, 0, 255];
396       break;
397     case 'rainbow-linear2':
398       for (var i = 0; i < 205; i++) {
399         var cc = Math.floor(i * 255 / 204);
400         palette[i] = [255, cc, 0, 255];
401         palette[204 + i] = [255 - cc, 255, 0, 255];
402         palette[409 + i] = [0, 255, cc, 255];
403         palette[614 + i] = [0, 255 - cc, 255, 255];
404         palette[819 + i] = [cc, 0, 255, 255];
405       }
406       palette[1024] = [0, 0, 0, 255];
407       break;
408     case 'rainbow-squared2':
409       for (var i = 0; i < 19; i++) {
410         var cc = Math.floor(i * 255 / 18);
411         palette[i] = [255, cc, 0, 255];
412       }
413       for (var i = 0; i < 74; i++) {
414         var cc = Math.floor(i * 255 / 73);
415         palette[19 + i] = [255 - cc, 255, 0, 255];
416       }
417       for (var i = 0; i < 168; i++) {
418         var cc = Math.floor(i * 255 / 167);
419         palette[93 + i] = [0, 255, cc, 255];
420       }
421       for (var i = 0; i < 298; i++) {
422         var cc = Math.floor(i * 255 / 297);
423         palette[261 + i] = [0, 255 - cc, 255, 255];
424       }
425       for (var i = 0; i < 465; i++) {
426         var cc = Math.floor(i * 255 / 464);
427         palette[559 + i] = [cc, 0, 255, 255];
428       }
429       palette[1024] = [0, 0, 0, 255];
430       break;
431   }
432   return palette;
433 }
434
435 function drawPoint(context, img_x, img_y, C, iterMax, algorithm) {
436   var itVal;
437   switch (algorithm) {
438     case 'oo':
439       itVal = mandelbrotValueOO(C, iterMax);
440       break;
441     case 'numeric':
442     default:
443       itVal = mandelbrotValueNumeric(C, iterMax);
444       break;
445   }
446   return getColor(itVal, iterMax);
447 }
448
449 // ########## UI functions ##########
450
451 var zoomstart;
452 var imgBackup;
453 var zoomTouchID;
454
455 var imgEvHandler = {
456   handleEvent: function(aEvent) {
457     var canvas = document.getElementById("mbrotImage");
458     var context = canvas.getContext("2d");
459     var touchEvent = aEvent.type.indexOf('touch') != -1;
460
461     // Bail out if this is neither a touch nor left-click.
462     if (!touchEvent && aEvent.button != 0)
463       return;
464
465     // Bail out if the started touch can't be found.
466     if (touchEvent && zoomstart &&
467         !aEvent.changedTouches.identifiedTouch(zoomTouchID))
468       return;
469
470     var coordObj = touchEvent ?
471                    aEvent.changedTouches.identifiedTouch(zoomTouchID) :
472                    aEvent;
473
474     switch (aEvent.type) {
475       case 'mousedown':
476       case 'touchstart':
477         if (touchEvent) {
478           zoomTouchID = aEvent.changedTouches.item(0).identifier;
479           coordObj = aEvent.changedTouches.identifiedTouch(zoomTouchID);
480         }
481         // left button - start dragzoom
482         zoomstart = {x: coordObj.clientX - canvas.offsetLeft,
483                      y: coordObj.clientY - canvas.offsetTop};
484         imgBackup = context.getImageData(0, 0, canvas.width, canvas.height);
485         break;
486       case 'mouseup':
487       case 'touchend':
488         if (zoomstart) {
489           context.putImageData(imgBackup, 0, 0);
490           var zoomend = {x: coordObj.clientX - canvas.offsetLeft,
491                          y: coordObj.clientY - canvas.offsetTop};
492
493           // make sure zoomend is bigger than zoomstart
494           if ((zoomend.x == zoomstart.x) || (zoomend.y == zoomstart.y)) {
495             // cannot zoom what has no area, discard it
496             zoomstart = undefined;
497             return;
498           }
499           if (zoomend.x < zoomstart.x)
500             [zoomend.x, zoomstart.x] = [zoomstart.x, zoomend.x];
501           if (zoomend.y < zoomstart.y)
502             [zoomend.y, zoomstart.y] = [zoomstart.y, zoomend.y];
503
504           if (gCurrentImageData) {
505             // determine new "coordinates"
506             var CWidth = gCurrentImageData.C_max.r - gCurrentImageData.C_min.r;
507             var CHeight = gCurrentImageData.C_max.i - gCurrentImageData.C_min.i;
508             var newC_min = new complex(
509                 gCurrentImageData.C_min.r + zoomstart.x / gCurrentImageData.iWidth * CWidth,
510                 gCurrentImageData.C_min.i + zoomstart.y / gCurrentImageData.iHeight * CHeight);
511             var newC_max = new complex(
512                 gCurrentImageData.C_min.r + zoomend.x / gCurrentImageData.iWidth * CWidth,
513                 gCurrentImageData.C_min.i + zoomend.y / gCurrentImageData.iHeight * CHeight);
514           }
515           else {
516             var newC_min = new complex(-2, -1.5);
517             var newC_max = new complex(1, 1.5);
518           }
519
520           adjustCoordsAndDraw(newC_min, newC_max);
521         }
522         zoomstart = undefined;
523         break;
524       case 'mousemove':
525       case 'touchmove':
526         if (zoomstart) {
527           context.putImageData(imgBackup, 0, 0);
528           context.strokeStyle = "rgb(255,255,31)";
529           context.strokeRect(zoomstart.x, zoomstart.y,
530                              coordObj.clientX - canvas.offsetLeft - zoomstart.x,
531                              coordObj.clientY - canvas.offsetTop - zoomstart.y);
532         }
533         break;
534     }
535   }
536 };
537
538 function drawIfEmpty() {
539   if (!gCurrentImageData) {
540     drawImage();
541   }
542 }
543
544 function toggleSettings() {
545   var fs = document.getElementById("settings");
546   if (fs.style.display != "block") {
547     fs.style.display = "block";
548   }
549   else {
550     fs.style.display = "none";
551   }
552 }
553
554 function goBack() {
555   if (gLastImageData) {
556     document.getElementById("iterMax").value = gLastImageData.iterMax;
557     // use gLastImageData.iWidth, gLastImageData.iHeight ???
558     adjustCoordsAndDraw(gLastImageData.C_min, gLastImageData.C_max);
559     gLastImageData = undefined;
560     document.getElementById("backButton").disabled = true;
561   }
562 }
563
564 function setIter(aIter) {
565   document.getElementById("iterMax").value = aIter;
566 }
567
568 function setPalette(aPaletteID) {
569   document.getElementById("palette").value = aPaletteID;
570   gColorPalette = getColorPalette(aPaletteID);
571 }
572
573 function setAlgorithm(algoID) {
574   //document.getElementById("algorithm").value = algoID;
575 }