save and load prefs correctly by creating a sync variant to deal with them (via init...
[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 // Get the best-available indexedDB object.
6 window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
7 var mainDB;
8
9 var gDebug = false;
10 var gAction, gActionLabel;
11 var gMainCanvas, gMainContext;
12 var gColorPalette = [];
13 var gStartTime = 0;
14 var gCurrentImageData;
15 var gLastImageData;
16
17 function Startup() {
18   gAction = document.getElementById("action");
19   gActionLabel = document.getElementById("actionlabel");
20   gAction.addEventListener("dbinit-done", initPrefs, false);
21   initDB();
22
23   gMainCanvas = document.getElementById("mbrotImage");
24   gMainContext = gMainCanvas.getContext("2d");
25
26   gMainCanvas.addEventListener("mouseup", imgEvHandler, false);
27   gMainCanvas.addEventListener("mousedown", imgEvHandler, false);
28   gMainCanvas.addEventListener("mousemove", imgEvHandler, false);
29   gMainCanvas.addEventListener("touchstart", imgEvHandler, false);
30   gMainCanvas.addEventListener("touchend", imgEvHandler, false);
31   gMainCanvas.addEventListener("touchcancel", imgEvHandler, false);
32   gMainCanvas.addEventListener("touchleave", imgEvHandler, false);
33   gMainCanvas.addEventListener("touchmove", imgEvHandler, false);
34
35   var initTile = new Image();
36   initTile.src = "style/initial-overview.png";
37   initTile.onload = function() { gMainContext.drawImage(initTile, 0, 0); };
38 }
39
40 function initDB() {
41   // Open DB.
42   var request = window.indexedDB.open("MainDB-mandelbrot", 1);
43   request.onerror = function(event) {
44     // Errors can be handled here. Error codes explain in:
45     // https://developer.mozilla.org/en/IndexedDB/IDBDatabaseException#Constants
46     if (gDebug)
47       console.log("error opening mainDB: " + event.target.errorCode);
48   };
49   request.onsuccess = function(event) {
50     //document.getElementById("debug").textContent = "mainDB opened.";
51     mainDB = request.result;
52     var throwEv = new CustomEvent("dbinit-done");
53     gAction.dispatchEvent(throwEv);
54   };
55   request.onupgradeneeded = function(event) {
56     mainDB = request.result;
57     var ver = mainDB.version || 0; // version is empty string for a new DB
58     if (gDebug)
59       console.log("mainDB has version " + ver + ", upgrade needed.");
60     if (!mainDB.objectStoreNames.contains("prefs")) {
61       // Create a "prefs" objectStore.
62       var prefsStore = mainDB.createObjectStore("prefs");
63     }
64     if (!mainDB.objectStoreNames.contains("bookmarks")) {
65       // Create a "bookmarks" objectStore.
66       var prefsStore = mainDB.createObjectStore("bookmarks");
67     }
68     mainDB.onversionchange = function(event) {
69       mainDB.close();
70       mainDB = undefined;
71       initDB();
72     };
73   };
74 }
75
76 function initPrefs() {
77   if (gDebug)
78     console.log("initializing prefs...");
79   gSyncPrefs.init(function () {
80     // Update the various settings fields to values from prefs.
81     if (gSyncPrefs.get("image.width"))
82       document.getElementById("image_width").value = gSyncPrefs.get("image.width");
83     if (gSyncPrefs.get("image.height"))
84       document.getElementById("image_height").value = gSyncPrefs.get("image.height");
85     if (gSyncPrefs.get("Cr_min"))
86       document.getElementById("Cr_min").value = gSyncPrefs.get("Cr_min");
87     if (gSyncPrefs.get("Cr_max"))
88       document.getElementById("Cr_max").value = gSyncPrefs.get("Cr_max");
89     if (gSyncPrefs.get("Ci_min"))
90       document.getElementById("Ci_min").value = gSyncPrefs.get("Ci_min");
91     if (gSyncPrefs.get("Ci_max"))
92       document.getElementById("Ci_max").value = gSyncPrefs.get("Ci_max");
93     if (gSyncPrefs.get("iteration_max"))
94       document.getElementById("iterMax").value = gSyncPrefs.get("iteration_max");
95     if (gSyncPrefs.get("color_palette"))
96       document.getElementById("palette").value = gSyncPrefs.get("color_palette");
97     if (gSyncPrefs.get("syncProportions") === true || gSyncPrefs.get("syncProportions") === false)
98       document.getElementById("proportional").checked = gSyncPrefs.get("syncProportions");
99     if (gSyncPrefs.get("use_algorithm"))
100      document.getElementById("algorithm").value = gSyncPrefs.get("use_algorithm");
101     if (gDebug)
102       console.log("prefs loaded.");
103   });
104 }
105
106 function getAdjustVal(aName) {
107   var value;
108   switch (aName) {
109     case "image.width":
110     case "image.height":
111       value = 0;
112       try {
113         value = document.getElementById(aName.replace(".", "_")).value;
114       }
115       catch (e) { }
116       if (!value || (value < 10) || (value > 5000)) {
117         value = 300;
118         gSyncPrefs.set(aName, value);
119         document.getElementById(aName.replace(".", "_")).value = value;
120       }
121       return value;
122     case "last_image.Cr_*":
123       var Cr_min = -2.0;
124       var Cr_max = 1.0;
125       try {
126         Cr_min = parseFloat(document.getElementById("Cr_min").value);
127         Cr_max = parseFloat(document.getElementById("Cr_max").value);
128       }
129       catch (e) { }
130       if ((Cr_min < -3) || (Cr_min > 2) ||
131           (Cr_max < -3) || (Cr_max > 2) || (Cr_min >= Cr_max)) {
132         Cr_min = -2.0; Cr_max = 1.0;
133       }
134       gSyncPrefs.set("Cr_min", Cr_min);
135       gSyncPrefs.set("Cr_max", Cr_max);
136       document.getElementById("Cr_min").value = Cr_min;
137       document.getElementById("Cr_max").value = Cr_max;
138       document.getElementById("Cr_scale").value = Cr_max - Cr_min;
139       return {Cr_min: Cr_min, Cr_max: Cr_max};
140     case "last_image.Ci_*":
141       var Ci_min = -1.5;
142       var Ci_max = 1.5;
143       try {
144         Ci_min = parseFloat(document.getElementById("Ci_min").value);
145         Ci_max = parseFloat(document.getElementById("Ci_max").value);
146       }
147       catch (e) { }
148       if ((Ci_min < -2.5) || (Ci_min > 2.5) ||
149           (Ci_max < -2.5) || (Ci_max > 2.5) || (Ci_min >= Ci_max)) {
150         Ci_min = -1.5; Ci_max = 1.5;
151       }
152       gSyncPrefs.set("Ci_min", Ci_min);
153       gSyncPrefs.set("Ci_max", Ci_max);
154       document.getElementById("Ci_min").value = Ci_min;
155       document.getElementById("Ci_max").value = Ci_max;
156       document.getElementById("Ci_scale").value = Ci_max - Ci_min;
157       return {Ci_min: Ci_min, Ci_max: Ci_max};
158     case "iteration_max":
159       value = 500;
160       try {
161         value = document.getElementById("iterMax").value;
162       }
163       catch (e) {
164         setIter(value);
165       }
166       if (value < 10 || value > 10000) {
167         value = 500;
168         setIter(value);
169       }
170       return value;
171     case "use_algorithm":
172       value = "numeric";
173       try {
174         value = document.getElementById("algorithm").value;
175       }
176       catch (e) {
177         setAlgorithm(value);
178       }
179       return value;
180    case "color_palette":
181       value = "kairo";
182       try {
183         value = document.getElementById("palette").value;
184       }
185       catch(e) {
186         setPalette(value);
187       }
188       return value;
189    case "syncProportions":
190       value = true;
191       try {
192         value = document.getElementById("proportional").value;
193       }
194       catch(e) {
195         gSyncPrefs.set(prefname, value);
196         document.getElementById("proportional").value = value;
197       }
198       return value;
199     default:
200       return false;
201   }
202 }
203
204 function setVal(aName, aValue) {
205   switch (aName) {
206     case "image.width":
207     case "image.height":
208       gSyncPrefs.set(aName, aValue);
209       document.getElementById(aName.replace(".", "_")).value = aValue;
210       break;
211     case "last_image.Cr_*":
212       gSyncPrefs.set("Cr_min", aValue.Cr_min);
213       gSyncPrefs.set("Cr_max", aValue.Cr_max);
214       document.getElementById("Cr_min").value = aValue.Cr_min;
215       document.getElementById("Cr_max").value = aValue.Cr_max;
216       break;
217     case "last_image.Ci_*":
218       gSyncPrefs.set("Ci_min", aValue.Ci_min);
219       gSyncPrefs.set("Ci_max", aValue.Ci_max);
220       document.getElementById("Ci_min").value = aValue.Ci_min;
221       document.getElementById("Ci_max").value = aValue.Ci_max;
222       break;
223     case "iteration_max":
224       setIter(aValue);
225       break;
226     case "use_algorithm":
227       setAlgorithm(aValue);
228       break;
229    case "color_palette":
230       setPalette(aValue);
231       break;
232    case "syncProportions":
233       gSyncPrefs.set(aName, aValue);
234       document.getElementById("proportional").value = aValue;
235       break;
236   }
237 }
238
239 function checkISValue(textbox, type) {
240   if (type == "coord") {
241     textbox.value = roundCoord(parseFloat(textbox.value));
242   }
243   else if (type == "dim") {
244     textbox.value = parseInt(textbox.value);
245     if (textbox.id == "image_width")
246       gSyncPrefs.set("image.width", textbox.value);
247     if (textbox.id == "image_height")
248       gSyncPrefs.set("image.height", textbox.value);
249   }
250 }
251
252 function recalcCoord(coord, target) {
253   var othercoord = (coord == "Ci") ? "Cr" : "Ci";
254   var owndim = (coord == "Ci") ? "height" : "width";
255   var otherdim = (coord == "Ci") ? "width" : "height";
256   var myscale;
257   if (target == "scale") {
258     myscale =
259       parseFloat(document.getElementById(coord + "_max").value) -
260       parseFloat(document.getElementById(coord + "_min").value);
261     document.getElementById(coord + "_scale").value = roundCoord(myscale);
262   }
263   else if (target == 'max') {
264     var mymax =
265       parseFloat(document.getElementById(coord + "_min").value) +
266       parseFloat(document.getElementById(coord + "_scale").value);
267     document.getElementById(coord + "_max").value = roundCoord(mymax);
268     myscale = document.getElementById(coord + "_scale").value;
269   }
270   if (document.getElementById("proportional").checked) {
271     var otherscale = myscale *
272       document.getElementById("image_" + otherdim).value /
273       document.getElementById("image_" + owndim).value;
274     document.getElementById(othercoord + "_scale").value = roundCoord(otherscale);
275     var othermax =
276       parseFloat(document.getElementById(othercoord + "_min").value) +
277       parseFloat(document.getElementById(othercoord + "_scale").value);
278     document.getElementById(othercoord + "_max").value = roundCoord(othermax);
279   }
280 }
281
282 function checkProportions() {
283   var prop = document.getElementById("proportional").checked;
284   if (!prop) {
285     recalcCoord("Cr", "scale");
286   }
287   gSyncPrefs.set("syncProportions", prop);
288 }
289
290 function roundCoord(floatval) {
291   // We should round to 10 decimals here or so
292   return parseFloat(floatval.toFixed(10));
293 }
294
295 function adjustCoordsAndDraw(aC_min, aC_max) {
296   var iWidth = getAdjustVal("image.width");
297   var iHeight = getAdjustVal("image.height");
298
299   // correct coordinates
300   if (aC_min.r < -2)
301     aC_min.r = -2;
302   if (aC_max.r > 2)
303     aC_max.r = 2;
304   if ((aC_min.r > 2) || (aC_max.r < -2) || (aC_min.r >= aC_max.r)) {
305     aC_min.r = -2.0; aC_max.r = 1.0;
306   }
307   if (aC_min.i < -2)
308     aC_min.i = -2;
309   if (aC_max.i > 2)
310     aC_max.i = 2;
311   if ((aC_min.i > 2) || (aC_max.i < -2) || (aC_min.i >= aC_max.i)) {
312     aC_min.i = -1.3; aC_max.i = 1.3;
313   }
314
315   var CWidth = aC_max.r - aC_min.r;
316   var CHeight = aC_max.i - aC_min.i;
317   var C_mid = new complex(aC_min.r + CWidth / 2, aC_min.i + CHeight / 2);
318
319   var CRatio = Math.max(CWidth / iWidth, CHeight / iHeight);
320
321   setVal("last_image.Cr_*", {Cr_min: C_mid.r - iWidth * CRatio / 2,
322                              Cr_max: C_mid.r + iWidth * CRatio / 2});
323   setVal("last_image.Ci_*", {Ci_min: C_mid.i - iHeight * CRatio / 2,
324                              Ci_max: C_mid.i + iHeight * CRatio / 2});
325
326   drawImage();
327 }
328
329 function drawImage() {
330   var canvas = gMainCanvas;
331   var context = gMainContext;
332
333   document.getElementById("calcTime").textContent = "--";
334
335   if (gCurrentImageData) {
336     gLastImageData = gCurrentImageData;
337     document.getElementById("backButton").disabled = false;
338   }
339
340   gColorPalette = getColorPalette(document.getElementById("palette").value);
341
342   var Cr_vals = getAdjustVal("last_image.Cr_*");
343   var Cr_min = Cr_vals.Cr_min;
344   var Cr_max = Cr_vals.Cr_max;
345
346   var Ci_vals = getAdjustVal("last_image.Ci_*");
347   var Ci_min = Ci_vals.Ci_min;
348   var Ci_max = Ci_vals.Ci_max;
349
350   var iterMax = getAdjustVal("iteration_max");
351   var algorithm = getAdjustVal("use_algorithm");
352
353   var iWidth = getAdjustVal("image.width");
354   var iHeight = getAdjustVal("image.height");
355
356   gCurrentImageData = {
357     C_min: new complex(Cr_min, Ci_min),
358     C_max: new complex(Cr_max, Ci_max),
359     iWidth: iWidth,
360     iHeight: iHeight,
361     iterMax: iterMax
362   };
363
364   canvas.width = iWidth;
365   canvas.height = iHeight;
366
367   context.fillStyle = "rgba(255, 255, 255, 127)";
368   context.fillRect(0, 0, canvas.width, canvas.height);
369
370   gStartTime = new Date();
371
372   drawLine(0, [Cr_min, Cr_max, Ci_min, Ci_max],
373               canvas, context, iterMax, algorithm);
374 }
375
376 function drawLine(line, dimensions, canvas, context, iterMax, algorithm) {
377     var Cr_min = dimensions[0];
378     var Cr_max = dimensions[1];
379     var Cr_scale = Cr_max - Cr_min;
380
381     var Ci_min = dimensions[2];
382     var Ci_max = dimensions[3];
383     var Ci_scale = Ci_max - Ci_min;
384
385     var lines = Math.min(canvas.height - line, 8);
386     var imageData = context.createImageData(canvas.width, lines);
387     var pixels = imageData.data;
388     var idx = 0;
389     for (var img_y = line; img_y < canvas.height && img_y < line+8; img_y++)
390       for (var img_x = 0; img_x < canvas.width; img_x++) {
391         var C = new complex(Cr_min + (img_x / canvas.width) * Cr_scale,
392                             Ci_min + (img_y / canvas.height) * Ci_scale);
393         var colors = drawPoint(context, img_x, img_y, C, iterMax, algorithm);
394         pixels[idx++] = colors[0];
395         pixels[idx++] = colors[1];
396         pixels[idx++] = colors[2];
397         pixels[idx++] = colors[3];
398       }
399     context.putImageData(imageData, 0, line);
400
401     if (img_y < canvas.height)
402       setTimeout(drawLine, 0, img_y, dimensions, canvas, context, iterMax, algorithm);
403     else if (gStartTime)
404       EndCalc();
405 }
406
407 function EndCalc() {
408   var endTime = new Date();
409   var timeUsed = (endTime.getTime() - gStartTime.getTime()) / 1000;
410   document.getElementById("calcTime").textContent = timeUsed.toFixed(3) + " seconds";
411 }
412
413 function complex(aReal, aImag) {
414   this.r = aReal;
415   this.i = aImag;
416 }
417 complex.prototype = {
418   square: function() {
419     return new complex(this.r * this.r - this.i * this.i,
420                        2 * this.r * this.i);
421   },
422   dist: function() {
423     return Math.sqrt(this.r * this.r + this.i * this.i);
424   },
425   add: function(aComplex) {
426     return new complex(this.r + aComplex.r, this.i + aComplex.i);
427   }
428 }
429
430 function mandelbrotValueOO (aC, aIterMax) {
431   // this would be nice code in general but it looks like JS objects are too heavy for normal use.
432   var Z = new complex(0.0, 0.0);
433   for (var iter = 0; iter < aIterMax; iter++) {
434     Z = Z.square().add(aC);
435     if (Z.r * Z.r + Z.i * Z.i > 256) { break; }
436   }
437   return iter;
438 }
439
440 function mandelbrotValueNumeric (aC, aIterMax) {
441   // optimized numeric code for fast calculation
442   var Cr = aC.r, Ci = aC.i;
443   var Zr = 0.0, Zi = 0.0;
444   var Zr2 = Zr * Zr, Zi2 = Zi * Zi;
445   for (var iter = 0; iter < aIterMax; iter++) {
446     Zi = 2 * Zr * Zi + Ci;
447     Zr = Zr2 - Zi2 + Cr;
448
449     Zr2 = Zr * Zr; Zi2 = Zi * Zi;
450     if (Zr2 + Zi2 > 256) { break; }
451   }
452   return iter;
453 }
454
455 function getColor(aIterValue, aIterMax) {
456   var standardizedValue = Math.round(aIterValue * 1024 / aIterMax);
457   if (gColorPalette && gColorPalette.length)
458     return gColorPalette[standardizedValue];
459
460   // fallback to simple b/w if for some reason we don't have a palette
461   if (aIterValue == aIterMax)
462     return [0, 0, 0, 255];
463   else
464     return [255, 255, 255, 255];
465 }
466
467 function getColorPalette(palName) {
468   var palette = [];
469   switch (palName) {
470     case 'bw':
471       for (var i = 0; i < 1024; i++) {
472         palette[i] = [255, 255, 255, 255];
473       }
474       palette[1024] = [0, 0, 0, 255];
475       break;
476     case 'kairo':
477       // outer areas
478       for (var i = 0; i < 32; i++) {
479         var cc1 = Math.floor(i * 127 / 31);
480         var cc2 = 170 - Math.floor(i * 43 / 31);
481         palette[i] = [cc1, cc2, cc1, 255];
482       }
483       // inner areas
484       for (var i = 0; i < 51; i++) {
485         var cc = Math.floor(i * 170 / 50);
486         palette[32 + i] = [cc, 0, (170-cc), 255];
487       }
488       // corona
489       for (var i = 0; i < 101; i++) {
490         var cc = Math.floor(i * 200 / 100);
491         palette[83 + i] = [255, cc, 0, 255];
492       }
493       // inner corona
494       for (var i = 0; i < 201; i++) {
495         var cc1 = 255 - Math.floor(i * 85 / 200);
496         var cc2 = 200 - Math.floor(i * 30 / 200);
497         var cc3 = Math.floor(i * 170 / 200);
498         palette[184 + i] = [cc1, cc2, cc3, 255];
499       }
500       for (var i = 0; i < 301; i++) {
501         var cc1 = 170 - Math.floor(i * 43 / 300);
502         var cc2 = 170 + Math.floor(i * 85 / 300);
503         palette[385 + i] = [cc1, cc1, cc2, 255];
504       }
505       for (var i = 0; i < 338; i++) {
506         var cc = 127 + Math.floor(i * 128 / 337);
507         palette[686 + i] = [cc, cc, 255, 255];
508       }
509       palette[1024] = [0, 0, 0, 255];
510       break;
511     case 'rainbow-linear1':
512       for (var i = 0; i < 256; i++) {
513         palette[i] = [i, 0, 0, 255];
514         palette[256 + i] = [255, i, 0, 255];
515         palette[512 + i] = [255 - i, 255, i, 255];
516         palette[768 + i] = [i, 255-i, 255, 255];
517       }
518       palette[1024] = [0, 0, 0, 255];
519       break;
520     case 'rainbow-squared1':
521       for (var i = 0; i < 34; i++) {
522         var cc = Math.floor(i * 255 / 33);
523         palette[i] = [cc, 0, 0, 255];
524       }
525       for (var i = 0; i < 137; i++) {
526         var cc = Math.floor(i * 255 / 136);
527         palette[34 + i] = [255, cc, 0, 255];
528       }
529       for (var i = 0; i < 307; i++) {
530         var cc = Math.floor(i * 255 / 306);
531         palette[171 + i] = [255 - cc, 255, cc, 255];
532       }
533       for (var i = 0; i < 546; i++) {
534         var cc = Math.floor(i * 255 / 545);
535         palette[478 + i] = [cc, 255 - cc, 255, 255];
536       }
537       palette[1024] = [0, 0, 0, 255];
538       break;
539     case 'rainbow-linear2':
540       for (var i = 0; i < 205; i++) {
541         var cc = Math.floor(i * 255 / 204);
542         palette[i] = [255, cc, 0, 255];
543         palette[204 + i] = [255 - cc, 255, 0, 255];
544         palette[409 + i] = [0, 255, cc, 255];
545         palette[614 + i] = [0, 255 - cc, 255, 255];
546         palette[819 + i] = [cc, 0, 255, 255];
547       }
548       palette[1024] = [0, 0, 0, 255];
549       break;
550     case 'rainbow-squared2':
551       for (var i = 0; i < 19; i++) {
552         var cc = Math.floor(i * 255 / 18);
553         palette[i] = [255, cc, 0, 255];
554       }
555       for (var i = 0; i < 74; i++) {
556         var cc = Math.floor(i * 255 / 73);
557         palette[19 + i] = [255 - cc, 255, 0, 255];
558       }
559       for (var i = 0; i < 168; i++) {
560         var cc = Math.floor(i * 255 / 167);
561         palette[93 + i] = [0, 255, cc, 255];
562       }
563       for (var i = 0; i < 298; i++) {
564         var cc = Math.floor(i * 255 / 297);
565         palette[261 + i] = [0, 255 - cc, 255, 255];
566       }
567       for (var i = 0; i < 465; i++) {
568         var cc = Math.floor(i * 255 / 464);
569         palette[559 + i] = [cc, 0, 255, 255];
570       }
571       palette[1024] = [0, 0, 0, 255];
572       break;
573   }
574   return palette;
575 }
576
577 function drawPoint(context, img_x, img_y, C, iterMax, algorithm) {
578   var itVal;
579   switch (algorithm) {
580     case 'oo':
581       itVal = mandelbrotValueOO(C, iterMax);
582       break;
583     case 'numeric':
584     default:
585       itVal = mandelbrotValueNumeric(C, iterMax);
586       break;
587   }
588   return getColor(itVal, iterMax);
589 }
590
591 // ########## UI functions ##########
592
593 var zoomstart;
594 var imgBackup;
595 var zoomTouchID;
596
597 var imgEvHandler = {
598   handleEvent: function(aEvent) {
599     var canvas = document.getElementById("mbrotImage");
600     var context = canvas.getContext("2d");
601     var touchEvent = aEvent.type.indexOf('touch') != -1;
602
603     // Bail out if this is neither a touch nor left-click.
604     if (!touchEvent && aEvent.button != 0)
605       return;
606
607     // Bail out if the started touch can't be found.
608     if (touchEvent && zoomstart &&
609         !aEvent.changedTouches.identifiedTouch(zoomTouchID))
610       return;
611
612     var coordObj = touchEvent ?
613                    aEvent.changedTouches.identifiedTouch(zoomTouchID) :
614                    aEvent;
615
616     switch (aEvent.type) {
617       case 'mousedown':
618       case 'touchstart':
619         if (touchEvent) {
620           zoomTouchID = aEvent.changedTouches.item(0).identifier;
621           coordObj = aEvent.changedTouches.identifiedTouch(zoomTouchID);
622         }
623         // left button - start dragzoom
624         zoomstart = {x: coordObj.clientX - canvas.offsetLeft,
625                      y: coordObj.clientY - canvas.offsetTop};
626         imgBackup = context.getImageData(0, 0, canvas.width, canvas.height);
627         break;
628       case 'mouseup':
629       case 'touchend':
630         if (zoomstart) {
631           context.putImageData(imgBackup, 0, 0);
632           var zoomend = {x: coordObj.clientX - canvas.offsetLeft,
633                          y: coordObj.clientY - canvas.offsetTop};
634
635           // make sure zoomend is bigger than zoomstart
636           if ((zoomend.x == zoomstart.x) || (zoomend.y == zoomstart.y)) {
637             // cannot zoom what has no area, discard it
638             zoomstart = undefined;
639             return;
640           }
641           if (zoomend.x < zoomstart.x)
642             [zoomend.x, zoomstart.x] = [zoomstart.x, zoomend.x];
643           if (zoomend.y < zoomstart.y)
644             [zoomend.y, zoomstart.y] = [zoomstart.y, zoomend.y];
645
646           if (gCurrentImageData) {
647             // determine new "coordinates"
648             var CWidth = gCurrentImageData.C_max.r - gCurrentImageData.C_min.r;
649             var CHeight = gCurrentImageData.C_max.i - gCurrentImageData.C_min.i;
650             var newC_min = new complex(
651                 gCurrentImageData.C_min.r + zoomstart.x / gCurrentImageData.iWidth * CWidth,
652                 gCurrentImageData.C_min.i + zoomstart.y / gCurrentImageData.iHeight * CHeight);
653             var newC_max = new complex(
654                 gCurrentImageData.C_min.r + zoomend.x / gCurrentImageData.iWidth * CWidth,
655                 gCurrentImageData.C_min.i + zoomend.y / gCurrentImageData.iHeight * CHeight);
656           }
657           else {
658             var newC_min = new complex(-2, -1.5);
659             var newC_max = new complex(1, 1.5);
660           }
661
662           adjustCoordsAndDraw(newC_min, newC_max);
663         }
664         zoomstart = undefined;
665         break;
666       case 'mousemove':
667       case 'touchmove':
668         if (zoomstart) {
669           context.putImageData(imgBackup, 0, 0);
670           context.strokeStyle = "rgb(255,255,31)";
671           context.strokeRect(zoomstart.x, zoomstart.y,
672                              coordObj.clientX - canvas.offsetLeft - zoomstart.x,
673                              coordObj.clientY - canvas.offsetTop - zoomstart.y);
674         }
675         break;
676     }
677   }
678 };
679
680 function drawIfEmpty() {
681   if (!gCurrentImageData) {
682     drawImage();
683   }
684 }
685
686 function toggleSettings() {
687   var fs = document.getElementById("settings");
688   if (fs.style.display != "block") {
689     fs.style.display = "block";
690   }
691   else {
692     fs.style.display = "none";
693   }
694 }
695
696 function goBack() {
697   if (gLastImageData) {
698     document.getElementById("iterMax").value = gLastImageData.iterMax;
699     // use gLastImageData.iWidth, gLastImageData.iHeight ???
700     adjustCoordsAndDraw(gLastImageData.C_min, gLastImageData.C_max);
701     gLastImageData = undefined;
702     document.getElementById("backButton").disabled = true;
703   }
704 }
705
706 function setIter(aIter) {
707   if (aIter)
708     document.getElementById("iterMax").value = aIter;
709   else
710     aIter = document.getElementById("iterMax").value;
711   gSyncPrefs.set("iteration_max", aIter);
712 }
713
714 function setPalette(aPaletteID) {
715   if (aPaletteID)
716     document.getElementById("palette").value = aPaletteID;
717   else
718     aPaletteID = document.getElementById("palette").value;
719   gSyncPrefs.set("color_palette", aPaletteID);
720   gColorPalette = getColorPalette(aPaletteID);
721 }
722
723 function setAlgorithm(aAlgoID) {
724   if (aAlgoID)
725     document.getElementById("algorithm").value = aAlgoID;
726   else
727     aAlgoID = document.getElementById("algorithm").value;
728   gSyncPrefs.set("use_algorithm", aAlgoID);
729 }
730
731 function callBookmark(evtarget) {
732   if (evtarget.id == "bookmarkSave" || evtarget.id == "bookmarkSeparator")
733     return;
734   if (evtarget.id == "bookmarkOverview") {
735     adjustCoordsAndDraw(new complex(0,0), new complex(0,0));
736     return;
737   }
738
739   if (evtarget.getAttribute('bmRowID')) {
740     var iterMax = 0;
741     var C_min = null;
742     var C_max = null;
743
744     // Get coordinates for this row ID.
745     /*
746     while (statement.executeStep()) {
747       iterMax = ;
748       C_min = new complex(, );
749       C_max = new complex(, );
750     }
751
752     if (iterMax && C_min && C_max) {
753       setIter(iterMax)
754       adjustCoordsAndDraw(C_min, C_max);
755     }
756     */
757   }
758 }
759
760 var gSyncPrefs = {
761   objStore: "prefs",
762   shadow: {},
763
764   init: function(aCallback) {
765     // Fill the shadow from the DB.
766     if (!mainDB)
767       return;
768     var transaction = mainDB.transaction([this.objStore]);
769     var objStore = transaction.objectStore(this.objStore);
770     if (objStore.getAll) { // currently Mozilla-specific
771       objStore.getAll().onsuccess = function(event) {
772         gSyncPrefs.shadow = event.target.result;
773         aCallback();
774       };
775     }
776     else { // Use cursor (standard method).
777       objStore.openCursor().onsuccess = function(event) {
778         var cursor = event.target.result;
779         if (cursor) {
780           gSyncPrefs.shadow[cursor.key] = cursor.value;
781           cursor.continue();
782         }
783         else {
784           aCallback();
785         }
786       };
787     }
788   },
789
790   get: function(aKey) {
791     // Only use the shadow.
792     return this.shadow[aKey];
793   },
794
795   set: function(aKey, aValue) {
796     // First update the shadow.
797     this.shadow[aKey] = aValue;
798     // Now sync the DB with this.
799     if (!mainDB)
800       return;
801     var success = false;
802     var transaction = mainDB.transaction([this.objStore], "readwrite");
803     var objStore = transaction.objectStore(this.objStore);
804     var request = objStore.put(aValue, aKey);
805     request.onsuccess = function(event) {
806       success = true;
807       // Nothing else to be done!
808     };
809     request.onerror = function(event) {
810       // Errors could be handled here (but are ignored).
811     };
812   },
813
814   unset: function(aKey) {
815     // First update the shadow.
816     delete this.shadow[aKey];
817     // Now sync the DB with this.
818     if (!mainDB)
819       return;
820     var success = false;
821     var transaction = mainDB.transaction([this.objStore], "readwrite");
822     var request = transaction.objectStore(this.objStore).delete(aKey);
823     request.onsuccess = function(event) {
824       success = true;
825       // Nothing else to be done!
826     };
827     request.onerror = function(event) {
828       // Errors could be handled here (but are ignored).
829     }
830   }
831 };
832
833 var gBMStore = {
834   objStore: "bookmarks",
835
836   getList: function(aCallback) {
837     if (!mainDB)
838       return;
839     var transaction = mainDB.transaction([this.objStore]);
840     var objStore = transaction.objectStore(this.objStore);
841     if (objStore.getAll) { // currently Mozilla-specific
842       objStore.getAll().onsuccess = function(event) {
843         aCallback(event.target.result);
844       };
845     }
846     else { // Use cursor (standard method).
847       var BMs = {};
848       objStore.openCursor().onsuccess = function(event) {
849         var cursor = event.target.result;
850         if (cursor) {
851           BMs[cursor.key] = cursor.value;
852           cursor.continue();
853         }
854         else {
855           aCallback(BMs);
856         }
857       };
858     }
859   },
860
861   get: function(aKey, aCallback) {
862     if (!mainDB)
863       return;
864     var transaction = mainDB.transaction([this.objStore]);
865     var request = transaction.objectStore(this.objStore).get(aKey);
866     request.onsuccess = function(event) {
867       aCallback(request.result, event);
868     };
869     request.onerror = function(event) {
870       // Errors can be handled here.
871       aCallback(undefined, event);
872     };
873   },
874
875   set: function(aKey, aValue, aCallback) {
876     if (!mainDB)
877       return;
878     var success = false;
879     var transaction = mainDB.transaction([this.objStore], "readwrite");
880     var objStore = transaction.objectStore(this.objStore);
881     var request = objStore.put(aValue, aKey);
882     request.onsuccess = function(event) {
883       success = true;
884       if (aCallback)
885         aCallback(success, event);
886     };
887     request.onerror = function(event) {
888       // Errors can be handled here.
889       if (aCallback)
890         aCallback(success, event);
891     };
892   },
893
894   unset: function(aKey, aCallback) {
895     if (!mainDB)
896       return;
897     var success = false;
898     var transaction = mainDB.transaction([this.objStore], "readwrite");
899     var request = transaction.objectStore(this.objStore).delete(aKey);
900     request.onsuccess = function(event) {
901       success = true;
902       if (aCallback)
903         aCallback(success, event);
904     };
905     request.onerror = function(event) {
906       // Errors can be handled here.
907       if (aCallback)
908         aCallback(success, event);
909     }
910   },
911
912   clear: function(aCallback) {
913     if (!mainDB)
914       return;
915     var success = false;
916     var transaction = mainDB.transaction([this.objStore], "readwrite");
917     var request = transaction.objectStore(this.objStore).clear();
918     request.onsuccess = function(event) {
919       success = true;
920       if (aCallback)
921         aCallback(success, event);
922     };
923     request.onerror = function(event) {
924       // Errors can be handled here.
925       if (aCallback)
926         aCallback(success, event);
927     }
928   }
929 };