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