don't display h1, add a viewport tag, paint an initial image with explanations of...
[mandelbrot-web.git] / js / mandelbrot.js
CommitLineData
d0244cd3
RK
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/. */
6a7aa57d 4
51442fd4 5var gMainCanvas, gMainContext;
6a7aa57d
RK
6var gColorPalette = [];
7var gStartTime = 0;
becdac35
RK
8var gCurrentImageData;
9var gLastImageData;
10
11function Startup() {
51442fd4
RK
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);
becdac35
RK
27}
28
29function 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
119function 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
148function 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}
6a7aa57d
RK
181
182function drawImage() {
51442fd4
RK
183 var canvas = gMainCanvas;
184 var context = gMainContext;
6a7aa57d
RK
185
186 document.getElementById("calcTime").textContent = "--";
187
becdac35
RK
188 if (gCurrentImageData) {
189 gLastImageData = gCurrentImageData;
95d05599 190 document.getElementById("backButton").disabled = false;
becdac35
RK
191 }
192
6a7aa57d
RK
193 gColorPalette = getColorPalette(document.getElementById("palette").value);
194
becdac35
RK
195 var Cr_vals = getAdjustVal("last_image.Cr_*");
196 var Cr_min = Cr_vals.Cr_min;
197 var Cr_max = Cr_vals.Cr_max;
6a7aa57d 198
becdac35
RK
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");
6a7aa57d
RK
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
becdac35
RK
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
6a7aa57d
RK
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
234function 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
265function 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
271function complex(aReal, aImag) {
272 this.r = aReal;
273 this.i = aImag;
274}
275complex.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
288function 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
298function 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
313function 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
325function 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;
becdac35
RK
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;
6a7aa57d
RK
431 }
432 return palette;
433}
434
435function 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
becdac35
RK
451var zoomstart;
452var imgBackup;
453var zoomTouchID;
454
455var 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
95d05599
RK
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 }
becdac35
RK
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 }
95d05599 533 break;
becdac35
RK
534 }
535 }
536};
537
95d05599
RK
538function drawIfEmpty() {
539 if (!gCurrentImageData) {
540 drawImage();
541 }
542}
543
6a7aa57d
RK
544function 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 }
becdac35
RK
552}
553
554function 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;
95d05599 560 document.getElementById("backButton").disabled = true;
becdac35
RK
561 }
562}
563
564function setIter(aIter) {
565 document.getElementById("iterMax").value = aIter;
566}
567
568function setPalette(aPaletteID) {
569 document.getElementById("palette").value = aPaletteID;
570 gColorPalette = getColorPalette(aPaletteID);
571}
572
573function setAlgorithm(algoID) {
574 //document.getElementById("algorithm").value = algoID;
575}