1 /*
  2  * Copyright (C) 2008, 2009 Mihai Şucan
  3  *
  4  * This file is part of PaintWeb.
  5  *
  6  * PaintWeb is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation, either version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * PaintWeb is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
 18  *
 19  * $URL: http://code.google.com/p/paintweb $
 20  * $Date: 2009-07-09 14:26:21 +0300 $
 21  */
 22 
 23 /**
 24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 25  * @fileOverview Holds the implementation of the Color Mixer dialog.
 26  */
 27 
 28 // For the implementation of this extension I used the following references:
 29 // - Wikipedia articles on each subject.
 30 // - the great brucelindbloom.com Web site - lots of information.
 31 
 32 /**
 33  * @class The Color Mixer extension.
 34  *
 35  * @param {PaintWeb} app Reference to the main paint application object.
 36  */
 37 pwlib.extensions.colormixer = function (app) {
 38   var _self     = this,
 39       config    = app.config.colormixer,
 40       doc       = app.doc,
 41       gui       = app.gui,
 42       lang      = app.lang.colormixer,
 43       MathFloor = Math.floor,
 44       MathMax   = Math.max,
 45       MathMin   = Math.min,
 46       MathPow   = Math.pow,
 47       MathRound = Math.round,
 48       resScale  = app.resolution.scale;
 49 
 50   /**
 51    * Holds references to various DOM elements.
 52    *
 53    * @private
 54    * @type Object
 55    */
 56   this.elems = {
 57     /**
 58      * Reference to the element which holds Canvas controls (the dot on the 
 59      * Canvas, and the slider).
 60      * @type Element
 61      */
 62     'controls': null,
 63 
 64     /**
 65      * Reference to the dot element that is rendered on top of the color space 
 66      * visualisation.
 67      * @type Element
 68      */
 69     'chartDot': null,
 70 
 71     /**
 72      * Reference to the slider element.
 73      * @type Element
 74      */
 75     'slider': null,
 76 
 77     /**
 78      * Reference to the input element that allows the user to pick the color 
 79      * palette to be displayed.
 80      * @type Element
 81      */
 82     'cpaletteInput': null,
 83 
 84     /**
 85      * The container element which holds the colors of the currently selected 
 86      * palette.
 87      * @type Element
 88      */
 89     'cpaletteOutput': null,
 90 
 91     /**
 92      * Reference to the element which displays the current color.
 93      * @type Element
 94      */
 95     "colorActive": null,
 96 
 97     /**
 98      * Reference to the element which displays the old color.
 99      * @type Element
100      */
101     "colorOld": null
102   };
103 
104   /**
105    * Reference to the Color Mixer floating panel GUI component object.
106    *
107    * @private
108    * @type pwlib.guiFloatingPanel
109    */
110   this.panel = null;
111 
112   /**
113    * Reference to the Color Mixer tab panel GUI component object which holds the 
114    * inputs.
115    *
116    * @private
117    * @type pwlib.guiTabPanel
118    */
119   this.panelInputs = null;
120 
121   /**
122    * Reference to the Color Mixer tab panel GUI component object which holds the 
123    * Canvas used for color space visualisation and the color palettes selector.
124    *
125    * @private
126    * @type pwlib.guiTabPanel
127    */
128   this.panelSelector = null;
129 
130   /**
131    * Holds a reference to the 2D context of the color mixer Canvas element. This 
132    * is where the color chart and the slider are both drawn.
133    *
134    * @private
135    * @type CanvasRenderingContext2D
136    */
137   this.context2d = false;
138 
139   /**
140    * Target input hooks. This object must hold two methods:
141    *
142    * <ul>
143    *   <li><code>show()</code> which is invoked by this extension when the Color 
144    *   Mixer panel shows up on screen.
145    *
146    *   <li><code>hide()</code> which is invoked when the Color Mixer panel is 
147    *   hidden from the screen.
148    * </ul>
149    *
150    * <p>The object must also hold information about the associated configuration 
151    * property: <var>configProperty</var>, <var>configGroup</var> and 
152    * <var>configGroupRef</var>.
153    *
154    * @type Object
155    */
156   this.targetInput = null;
157 
158   /**
159    * Holds the current color in several formats: RGB, HEX, HSV, CIE Lab, and 
160    * CMYK. Except for 'hex', all the values should be from 0 to 1.
161    *
162    * @type Object
163    */
164   this.color = {
165     // RGB
166     red  : 0,
167     green: 0,
168     blue : 0,
169 
170     alpha : 0,
171     hex   : 0,
172 
173     // HSV
174     hue : 0,
175     sat : 0,
176     val : 0,
177 
178     // CMYK
179     cyan    : 0,
180     magenta : 0,
181     yellow  : 0,
182     black   : 0,
183 
184     // CIE Lab
185     cie_l : 0,
186     cie_a : 0,
187     cie_b : 0
188   };
189 
190   /**
191    * Holds references to all the DOM input fields, for each color channel.
192    *
193    * @private
194    * @type Object
195    */
196   this.inputs = {
197     red   : null,
198     green : null,
199     blue  : null,
200 
201     alpha : null,
202     hex   : null,
203 
204     hue : null,
205     sat : null,
206     val : null,
207 
208     cyan    : null,
209     magenta : null,
210     yellow  : null,
211     black   : null,
212 
213     cie_l : null,
214     cie_a : null,
215     cie_b : null
216   };
217 
218   /**
219    * The "absolute maximum" value is determined based on the min/max values.  
220    * For min -100 and max 100, the abs_max is 200.
221    * @private
222    *
223    */
224   this.abs_max  = {};
225 
226   // The hue spectrum used by the HSV charts.
227   var hueSpectrum = [
228     [255,   0,   0], // 0, Red,       0°
229     [255, 255,   0], // 1, Yellow,   60°
230     [  0, 255,   0], // 2, Green,   120°
231     [  0, 255, 255], // 3, Cyan,    180°
232     [  0,   0, 255], // 4, Blue,    240°
233     [255,   0, 255], // 5, Magenta, 300°
234     [255,   0,   0]  // 6, Red,     360°
235   ];
236 
237   // The active color key (input) determines how the color chart works.
238   this.ckey_active = 'red';
239 
240   // Given a group of the inputs: red, green and blue, when one of them is active, the ckey_adjoint is set to an array of the other two input IDs.
241   this.ckey_adjoint = false;
242   this.ckey_active_group = false;
243 
244   this.ckey_grouping = {
245     'red'   : 'rgb',
246     'green' : 'rgb',
247     'blue'  : 'rgb',
248 
249     'hue' : 'hsv',
250     'sat' : 'hsv',
251     'val' : 'hsv',
252 
253     'cyan'    : 'cmyk',
254     'magenta' : 'cmyk',
255     'yellow'  : 'cmyk',
256     'black'   : 'cmyk',
257 
258     'cie_l' : 'lab',
259     'cie_a' : 'lab',
260     'cie_b' : 'lab'
261   };
262 
263   // These values are automatically calculated when the color mixer is 
264   // initialized.
265   this.sliderX = 0;
266   this.sliderWidth = 0;
267   this.sliderHeight = 0;
268   this.sliderSpacing = 0;
269   this.chartWidth = 0;
270   this.chartHeight = 0;
271 
272   /**
273    * Register the Color Mixer extension.
274    *
275    * @returns {Boolean} True if the extension can be registered properly, or 
276    * false if not.
277    */
278   this.extensionRegister = function (ev) {
279     if (!gui.elems || !gui.elems.colormixer_canvas || !gui.floatingPanels || 
280         !gui.floatingPanels.colormixer || !gui.tabPanels || 
281         !gui.tabPanels.colormixer_inputs || !gui.tabPanels.colormixer_selector 
282         || !_self.init_lab()) {
283       return false;
284     }
285 
286     _self.panel = gui.floatingPanels.colormixer;
287     _self.panelSelector = gui.tabPanels.colormixer_selector;
288     _self.panelInputs = gui.tabPanels.colormixer_inputs;
289 
290     // Initialize the color mixer Canvas element.
291     _self.context2d = gui.elems.colormixer_canvas.getContext('2d');
292     if (!_self.context2d) {
293       return false;
294     }
295 
296     // Setup the color mixer inputs.
297     var elem, label, labelElem,
298         inputValues = config.inputValues,
299         form = _self.panelInputs.container;
300     if (!form) {
301       return false;
302     }
303 
304     for (var i in _self.inputs) {
305       elem = form.elements.namedItem('ckey_' + i) || gui.inputs['ckey_' + i];
306       if (!elem) {
307         return false;
308       }
309 
310       if (i === 'hex' || i === 'alpha') {
311         label = lang.inputs[i];
312       } else {
313         label = lang.inputs[_self.ckey_grouping[i] + '_' + i];
314       }
315 
316       labelElem = elem.parentNode;
317       labelElem.replaceChild(doc.createTextNode(label), labelElem.firstChild);
318 
319       elem.addEventListener('input',  _self.ev_input_change, false);
320       elem.addEventListener('change', _self.ev_input_change, false);
321 
322       if (i !== 'hex') {
323         elem.setAttribute('step', inputValues[i][2]);
324 
325         elem.setAttribute('max', MathRound(inputValues[i][1]));
326         elem.setAttribute('min', MathRound(inputValues[i][0]));
327         _self.abs_max[i] = inputValues[i][1] - inputValues[i][0];
328       }
329 
330       // Store the color key, which is used by the event handler.
331       elem._ckey = i;
332       _self.inputs[i] = elem;
333     }
334 
335     // Setup the ckey inputs of type=radio.
336     var ckey = form.ckey;
337     if (!ckey) {
338       return false;
339     }
340     for (var i = 0, n = ckey.length; i < n; i++) {
341       elem = ckey[i];
342       if (_self.ckey_grouping[elem.value] === 'lab' && 
343           !_self.context2d.putImageData) {
344         elem.disabled = true;
345         continue;
346       }
347 
348       elem.addEventListener('change', _self.ev_change_ckey_active, false);
349 
350       if (elem.value === _self.ckey_active) {
351         elem.checked = true;
352         _self.update_ckey_active(_self.ckey_active, true);
353       }
354     }
355 
356     // Prepare the color preview elements.
357     _self.elems.colorActive = gui.elems.colormixer_colorActive.firstChild;
358     _self.elems.colorOld = gui.elems.colormixer_colorOld.firstChild;
359     _self.elems.colorOld.addEventListener('click', _self.ev_click_color, false);
360 
361     // Make sure the buttons work properly.
362     var anchor, btn, buttons = ['accept', 'cancel', 'saveColor', 'pickColor'];
363     for (var i = 0, n = buttons.length; i < n; i++) {
364       btn = gui.elems['colormixer_btn_' + buttons[i]];
365       if (!btn) {
366         continue;
367       }
368 
369       anchor = doc.createElement('a');
370       anchor.href = '#';
371       anchor.appendChild(doc.createTextNode(lang.buttons[buttons[i]]));
372       anchor.addEventListener('click', _self['ev_click_' + buttons[i]], false);
373 
374       btn.replaceChild(anchor, btn.firstChild);
375     }
376 
377     // Prepare the canvas "controls" (the chart "dot" and the slider).
378     var id, elems = ['controls', 'chartDot', 'slider'];
379     for (var i = 0, n = elems.length; i < n; i++) {
380       id = elems[i];
381       elem = gui.elems['colormixer_' + id];
382       if (!elem) {
383         return false;
384       }
385 
386       elem.addEventListener('mousedown', _self.ev_canvas, false);
387       elem.addEventListener('mousemove', _self.ev_canvas, false);
388       elem.addEventListener('mouseup',   _self.ev_canvas, false);
389 
390       _self.elems[id] = elem;
391     }
392 
393     // The color palette <select>.
394     _self.elems.cpaletteInput = gui.inputs.colormixer_cpaletteInput;
395     _self.elems.cpaletteInput.addEventListener('change', 
396         _self.ev_change_cpalette, false);
397 
398     // Add the list of color palettes into the <select>.
399     var palette;
400     for (var i in config.colorPalettes) {
401       palette = config.colorPalettes[i];
402       elem = doc.createElement('option');
403       elem.value = i;
404       if (i === config.paletteDefault) {
405         elem.selected = true;
406       }
407 
408       elem.appendChild( doc.createTextNode(lang.colorPalettes[i]) );
409       _self.elems.cpaletteInput.appendChild(elem);
410     }
411 
412     // This is the ordered list where we add each color (list item).
413     _self.elems.cpaletteOutput = gui.elems.colormixer_cpaletteOutput;
414     _self.elems.cpaletteOutput.addEventListener('click', _self.ev_click_color, 
415         false);
416 
417     _self.cpalette_load(config.paletteDefault);
418 
419     // Make sure the Canvas element scale is in sync with the application.
420     app.events.add('canvasSizeChange', _self.update_dimensions);
421 
422     _self.panelSelector.events.add('guiTabActivate', _self.ev_tabActivate);
423 
424     // Make sure the Color Mixer is properly closed when the floating panel is 
425     // closed.
426     _self.panel.events.add('guiFloatingPanelStateChange', 
427         _self.ev_panel_stateChange);
428 
429     return true;
430   };
431 
432   /**
433    * This function calculates lots of values used by the other CIE Lab-related 
434    * functions.
435    *
436    * @private
437    * @returns {Boolean} True if the initialization was successful, or false if 
438    * not.
439    */
440   this.init_lab = function () {
441     var cfg = config.lab;
442     if (!cfg) {
443       return false;
444     }
445 
446     // Chromaticity coordinates for the RGB primaries.
447     var x0_r = cfg.x_r,
448         y0_r = cfg.y_r,
449         x0_g = cfg.x_g,
450         y0_g = cfg.y_g,
451         x0_b = cfg.x_b,
452         y0_b = cfg.y_b,
453 
454         // The reference white point (xyY to XYZ).
455         w_x = cfg.ref_x / cfg.ref_y,
456         w_y = 1,
457         w_z = (1 - cfg.ref_x - cfg.ref_y) / cfg.ref_y;
458 
459     cfg.w_x = w_x;
460     cfg.w_y = w_y;
461     cfg.w_z = w_z;
462 
463     // Again, xyY to XYZ for each RGB primary. Y=1.
464     var x_r = x0_r / y0_r,
465         y_r = 1,
466         z_r = (1 - x0_r - y0_r) / y0_r,
467         x_g = x0_g / y0_g,
468         y_g = 1,
469         z_g = (1 - x0_g - y0_g) / y0_g,
470         x_b = x0_b / y0_b,
471         y_b = 1,
472         z_b = (1 - x0_b - y0_b) / y0_b,
473         m   = [x_r, y_r, z_r,
474                x_g, y_g, z_g,
475                x_b, y_b, z_b],
476         m_i = _self.calc_m3inv(m),
477         s   = _self.calc_m1x3([w_x, w_y, w_z], m_i);
478 
479     // The 3x3 matrix used by rgb2xyz().
480     m = [s[0] * m[0], s[0] * m[1], s[0] * m[2],
481          s[1] * m[3], s[1] * m[4], s[1] * m[5],
482          s[2] * m[6], s[2] * m[7], s[2] * m[8]];
483 
484     // The matrix inverse, used by xyz2rgb();
485     cfg.m_i = _self.calc_m3inv(m);
486     cfg.m   = m;
487 
488     // Now determine the min/max values for a and b.
489 
490     var xyz = _self.rgb2xyz([0, 1, 0]), // green gives the minimum value for a
491         lab = _self.xyz2lab(xyz),
492         values = config.inputValues;
493     values.cie_a[0] = lab[1];
494 
495     xyz = _self.rgb2xyz([1, 0, 1]);     // magenta gives the maximum value for a
496     lab = _self.xyz2lab(xyz);
497     values.cie_a[1] = lab[1];
498 
499     xyz = _self.rgb2xyz([0, 0, 1]);     // blue gives the minimum value for b
500     lab = _self.xyz2lab(xyz);
501     values.cie_b[0] = lab[2];
502 
503     xyz = _self.rgb2xyz([1, 1, 0]);     // yellow gives the maximum value for b
504     lab = _self.xyz2lab(xyz);
505     values.cie_b[1] = lab[2];
506 
507     return true;
508   };
509 
510   /**
511    * The <code>guiTabActivate</code> event handler for the tab panel which holds 
512    * the color mixer and the color palettes. When switching back to the color 
513    * mixer, this method updates the Canvas.
514    *
515    * @private
516    * @param {pwlib.appEvent.guiTabActivate} ev The application event object.
517    */
518   this.ev_tabActivate = function (ev) {
519     if (ev.tabId === 'mixer' && _self.update_canvas_needed) {
520       _self.update_canvas(null, true);
521     }
522   };
523 
524   /**
525    * The <code>click</code> event handler for the Accept button. This method 
526    * dispatches the {@link pwlib.appEvent.configChange} application event for 
527    * the configuration property associated to the target input, and hides the 
528    * Color Mixer floating panel.
529    *
530    * @private
531    * @param {Event} ev The DOM Event object.
532    */
533   this.ev_click_accept = function (ev) {
534     ev.preventDefault();
535 
536     var configProperty = _self.targetInput.configProperty,
537         configGroup    = _self.targetInput.configGroup,
538         configGroupRef = _self.targetInput.configGroupRef,
539         prevVal = configGroupRef[configProperty],
540         newVal  = 'rgba(' + MathRound(_self.color.red   * 255) + ',' +
541                             MathRound(_self.color.green * 255) + ',' +
542                             MathRound(_self.color.blue  * 255) + ',' +
543                             _self.color.alpha + ')';
544 
545     _self.hide();
546 
547     if (prevVal !== newVal) {
548       configGroupRef[configProperty] = newVal;
549       app.events.dispatch(new pwlib.appEvent.configChange(newVal, prevVal, 
550           configProperty, configGroup, configGroupRef));
551     }
552   };
553 
554   /**
555    * The <code>click</code> event handler for the Cancel button. This method 
556    * hides the Color Mixer floating panel.
557    *
558    * @private
559    * @param {Event} ev The DOM Event object.
560    */
561   this.ev_click_cancel = function (ev) {
562     ev.preventDefault();
563     _self.hide();
564   };
565 
566   /**
567    * The <code>click</code> event handler for the "Save color" button. This 
568    * method adds the current color into the "_saved" color palette.
569    *
570    * @private
571    * @param {Event} ev The DOM Event object.
572    */
573   // TODO: provide a way to save the color palette permanently. This should use 
574   // some application event.
575   this.ev_click_saveColor = function (ev) {
576     ev.preventDefault();
577 
578     var color = [_self.color.red, _self.color.green, _self.color.blue],
579         saved = config.colorPalettes._saved;
580 
581     saved.colors.push(color);
582 
583     _self.elems.cpaletteInput.value = '_saved';
584     _self.cpalette_load('_saved');
585     _self.panelSelector.tabActivate('cpalettes');
586 
587     return true;
588   };
589 
590   /**
591    * The <code>click</code> event handler for the "Pick color" button. This 
592    * method activates the color picker tool.
593    *
594    * @private
595    * @param {Event} ev The DOM Event object.
596    */
597   this.ev_click_pickColor = function (ev) {
598     ev.preventDefault();
599     app.toolActivate('cpicker', ev);
600   };
601 
602   /**
603    * The <code>change</code> event handler for the color palette input element.  
604    * This loads the color palette the user selected.
605    *
606    * @private
607    * @param {Event} ev The DOM Event object.
608    */
609   this.ev_change_cpalette = function (ev) {
610     _self.cpalette_load(this.value);
611   };
612 
613   /**
614    * Load a color palette. Loading is performed asynchronously.
615    *
616    * @param {String} id The color palette ID.
617    *
618    * @returns {Boolean} True if the load was successful, or false if not.
619    */
620   this.cpalette_load = function (id) {
621     if (!id || !(id in config.colorPalettes)) {
622       return false;
623     }
624 
625     var palette = config.colorPalettes[id];
626 
627     if (palette.file) {
628       pwlib.xhrLoad(PaintWeb.baseFolder + palette.file, this.cpalette_loaded);
629 
630       return true;
631 
632     } else if (palette.colors) {
633       return this.cpalette_show(palette.colors);
634 
635     } else {
636       return false;
637     }
638   };
639 
640   /**
641    * The <code>onreadystatechange</code> event handler for the color palette 
642    * XMLHttpRequest object.
643    *
644    * @private
645    * @param {XMLHttpRequest} xhr The XMLHttpRequest object.
646    */
647   this.cpalette_loaded = function (xhr) {
648     if (!xhr || xhr.readyState !== 4) {
649       return;
650     }
651 
652     if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
653       alert(lang.failedColorPaletteLoad);
654       return;
655     }
656 
657     var colors = JSON.parse(xhr.responseText);
658     xhr = null;
659     _self.cpalette_show(colors);
660   };
661 
662   /**
663    * Show a color palette. This method adds all the colors in the DOM as 
664    * individual anchor elements which users can click on.
665    *
666    * @private
667    *
668    * @param {Array} colors The array which holds each color in the palette.
669    *
670    * @returns {Boolean} True if the operation was successful, or false if not.
671    */
672   this.cpalette_show = function (colors) {
673     if (!colors || !(colors instanceof Array)) {
674       return false;
675     }
676 
677     var color, anchor, rgbValue,
678         frag = doc.createDocumentFragment(),
679         dest = this.elems.cpaletteOutput;
680 
681     dest.style.display = 'none';
682     while (dest.hasChildNodes()) {
683       dest.removeChild(dest.firstChild);
684     }
685 
686     for (var i = 0, n = colors.length; i < n; i++) {
687       color = colors[i];
688 
689       // Do not allow values higher than 1.
690       color[0] = MathMin(1, color[0]);
691       color[1] = MathMin(1, color[1]);
692       color[2] = MathMin(1, color[2]);
693 
694       rgbValue = 'rgb(' + MathRound(color[0] * 255) + ',' +
695           MathRound(color[1] * 255) + ',' +
696           MathRound(color[2] * 255) + ')';
697 
698       anchor = doc.createElement('a');
699       anchor.href = '#';
700       anchor._color = color;
701       anchor.style.backgroundColor = rgbValue;
702       anchor.appendChild(doc.createTextNode(rgbValue));
703 
704       frag.appendChild(anchor);
705     }
706 
707     dest.appendChild(frag);
708     dest.style.display = 'block';
709 
710     colors = frag = null;
711 
712     return true;
713   };
714 
715   /**
716    * The <code>click</code> event handler for colors in the color palette list.  
717    * This event handler is also used for the "old color" element. This method 
718    * updates the color mixer to use the color the user picked.
719    *
720    * @private
721    * @param {Event} ev The DOM Event object.
722    */
723   this.ev_click_color = function (ev) {
724     var color = ev.target._color;
725     if (!color) {
726       return;
727     }
728 
729     ev.preventDefault();
730 
731     _self.color.red   = color[0];
732     _self.color.green = color[1];
733     _self.color.blue  = color[2];
734 
735     if (typeof(color[3]) !== 'undefined') {
736       _self.color.alpha = color[3];
737     }
738 
739     _self.update_color('rgb');
740   };
741 
742   /**
743    * Recalculate the dimensions and coordinates for the slider and for the color 
744    * space visualisation within the Canvas element.
745    *
746    * <p>This method is an event handler for the {@link 
747    * pwlib.appEvent.canvasSizeChange} application event.
748    *
749    * @private
750    */
751   this.update_dimensions = function () {
752     if (resScale === app.resolution.scale) {
753       return;
754     }
755 
756     resScale = app.resolution.scale;
757 
758     var canvas  = _self.context2d.canvas,
759         width   = canvas.width,
760         height  = canvas.height,
761         sWidth  = width  / resScale,
762         sHeight = height / resScale,
763         style;
764 
765     _self.sliderWidth   = MathRound(width * config.sliderWidth);
766     _self.sliderHeight  = height - 1;
767     _self.sliderSpacing = MathRound(width * config.sliderSpacing);
768     _self.sliderX       = width - _self.sliderWidth - 2;
769     _self.chartWidth    = _self.sliderX - _self.sliderSpacing;
770     _self.chartHeight   = height;
771 
772     style = _self.elems.controls.style;
773     style.width  = sWidth  + 'px';
774     style.height = sHeight + 'px';
775 
776     style = _self.elems.slider.style;
777     style.width = (_self.sliderWidth / resScale) + 'px';
778     style.left  = (_self.sliderX     / resScale) + 'px';
779 
780     style = canvas.style;
781     style.width  = sWidth  + 'px';
782     style.height = sHeight + 'px';
783 
784     if (_self.panel.state !== _self.panel.STATE_HIDDEN) {
785       _self.update_canvas();
786     }
787   };
788 
789   /**
790    * Calculate the product of two matrices.
791    *
792    * <p>Matrices are one-dimensional arrays of the form <code>[a0, a1, a2, ..., 
793    * b0, b1, b2, ...]</code> where each element from the matrix is given in 
794    * order, from the left to the right, row by row from the top to the bottom.
795    *
796    * @param {Array} a The first matrix must be one row and three columns.
797    * @param {Array} b The second matrix must be three rows and three columns.
798    *
799    * @returns {Array} The matrix product, one row and three columns.
800    */
801   // Note: for obvious reasons, this method is not a full-fledged matrix product 
802   // calculator. It's as simple as possible - fitting only the very specific 
803   // needs of the color mixer.
804   this.calc_m1x3 = function (a, b) {
805     if (!(a instanceof Array) || !(b instanceof Array)) {
806       return false;
807     } else {
808       return [
809         a[0] * b[0] + a[1] * b[3] + a[2] * b[6], // x
810         a[0] * b[1] + a[1] * b[4] + a[2] * b[7], // y
811         a[0] * b[2] + a[1] * b[5] + a[2] * b[8]  // z
812       ];
813     }
814   };
815 
816   /**
817    * Calculate the matrix inverse.
818    *
819    * <p>Matrices are one-dimensional arrays of the form <code>[a0, a1, a2, ..., 
820    * b0, b1, b2, ...]</code> where each element from the matrix is given in 
821    * order, from the left to the right, row by row from the top to the bottom.
822    *
823    * @private
824    *
825    * @param {Array} m The square matrix which must have three rows and three 
826    * columns.
827    *
828    * @returns {Array|false} The computed matrix inverse, or false if the matrix 
829    * determinant was 0 - the given matrix is not invertible.
830    */
831   // Note: for obvious reasons, this method is not a full-fledged matrix inverse 
832   // calculator. It's as simple as possible - fitting only the very specific 
833   // needs of the color mixer.
834   this.calc_m3inv = function (m) {
835     if (!(m instanceof Array)) {
836       return false;
837     }
838 
839     var d = (m[0]*m[4]*m[8] + m[1]*m[5]*m[6] + m[2]*m[3]*m[7]) -
840             (m[2]*m[4]*m[6] + m[5]*m[7]*m[0] + m[8]*m[1]*m[3]);
841 
842     // Matrix determinant is 0: the matrix is not invertible.
843     if (d === 0) {
844       return false;
845     }
846 
847     var i = [
848        m[4]*m[8] - m[5]*m[7], -m[3]*m[8] + m[5]*m[6],  m[3]*m[7] - m[4]*m[6],
849       -m[1]*m[8] + m[2]*m[7],  m[0]*m[8] - m[2]*m[6], -m[0]*m[7] + m[1]*m[6],
850        m[1]*m[5] - m[2]*m[4], -m[0]*m[5] + m[2]*m[3],  m[0]*m[4] - m[1]*m[3]
851     ];
852 
853     i = [1/d * i[0], 1/d * i[3], 1/d * i[6],
854          1/d * i[1], 1/d * i[4], 1/d * i[7],
855          1/d * i[2], 1/d * i[5], 1/d * i[8]];
856 
857     return i;
858   };
859 
860   /**
861    * The <code>change</code> event handler for the Color Mixer inputs of 
862    * type=radio. This method allows users to change the active color key - used 
863    * for the color space visualisation.
864    * @private
865    */
866   this.ev_change_ckey_active = function () {
867     if (this.value && this.value !== _self.ckey_active) {
868       _self.update_ckey_active(this.value);
869     }
870   };
871 
872   /**
873    * Update the active color key. This method updates the Canvas accordingly.
874    *
875    * @private
876    *
877    * @param {String} ckey The color key you want to be active.
878    * @param {Boolean} [only_vars] Tells if you want only the variables to be 
879    * updated - no Canvas updates. This is true only during the Color Mixer 
880    * initialization.
881    *
882    * @return {Boolean} True if the operation was successful, or false if not.
883    */
884   this.update_ckey_active = function (ckey, only_vars) {
885     if (!_self.inputs[ckey]) {
886       return false;
887     }
888 
889     _self.ckey_active = ckey;
890 
891     var  adjoint = [], group = _self.ckey_grouping[ckey];
892 
893     // Determine the adjoint color keys. For example, if red is active, then adjoint = ['green', 'blue'].
894     for (var i in _self.ckey_grouping) {
895       if (_self.ckey_grouping[i] === group && i !== ckey) {
896         adjoint.push(i);
897       }
898     }
899 
900     _self.ckey_active_group  = group;
901     _self.ckey_adjoint       = adjoint;
902 
903     if (!only_vars) {
904       if (_self.panelSelector.tabId !== 'mixer') {
905         _self.update_canvas_needed = true;
906         _self.panelSelector.tabActivate('mixer');
907       } else {
908         _self.update_canvas();
909       }
910 
911       if (_self.panelInputs.tabId !== group) {
912         _self.panelInputs.tabActivate(group);
913       }
914     }
915 
916     return true;
917   };
918 
919   /**
920    * Show the Color Mixer.
921    *
922    * @param {Object} target The target input object.
923    *
924    * @param {Object} color The color you want to set before the Color Mixer is 
925    * shown. The object must have four properties: <var>red</var>, 
926    * <var>green</var>, <var>blue</var> and <var>alpha</var>. All the values must 
927    * be between 0 and 1. This color becomes the "active color" and the "old 
928    * color".
929    *
930    * @see this.targetInput for more information about the <var>target</var> 
931    * object.
932    */
933   this.show = function (target, color) {
934       var styleActive = _self.elems.colorActive.style,
935           colorOld    = _self.elems.colorOld,
936           styleOld    = colorOld.style;
937 
938     if (target) {
939       if (_self.targetInput) {
940         _self.targetInput.hide();
941       }
942 
943       _self.targetInput = target;
944       _self.targetInput.show();
945     }
946 
947     if (color) {
948       _self.color.red   = color.red;
949       _self.color.green = color.green;
950       _self.color.blue  = color.blue;
951       _self.color.alpha = color.alpha;
952 
953       _self.update_color('rgb');
954 
955       styleOld.backgroundColor = styleActive.backgroundColor;
956       styleOld.opacity = styleActive.opacity;
957       colorOld._color = [color.red, color.green, color.blue, color.alpha];
958     }
959 
960     _self.panel.show();
961   };
962 
963   /**
964    * Hide the Color Mixer floating panel. This method invokes the 
965    * <code>hide()</code> method provided by the target input.
966    */
967   this.hide = function () {
968     _self.panel.hide();
969     _self.ev_canvas_mode = false;
970   };
971 
972   /**
973    * The <code>guiFloatingPanelStateChange</code> event handler for the Color 
974    * Mixer panel. This method ensures the Color Mixer is properly closed.
975    *
976    * @param {pwlib.appEvent.guiFloatingPanelStateChange} ev The application 
977    * event object.
978    */
979   this.ev_panel_stateChange = function (ev) {
980     if (ev.state === ev.STATE_HIDDEN) {
981       if (_self.targetInput) {
982         _self.targetInput.hide();
983         _self.targetInput = null;
984       }
985       _self.ev_canvas_mode = false;
986     }
987   };
988 
989   /**
990    * The <code>input</code> and <code>change</code> event handler for all the 
991    * Color Mixer inputs.
992    * @private
993    */
994   this.ev_input_change = function () {
995     if (!this._ckey) {
996       return;
997     }
998 
999     // Validate and restrict the possible values.
1000     // If the input is unchanged, or if the new value is invalid, the function 
1001     // stops.
1002     // The hexadecimal input is checked with a simple regular expression.
1003 
1004     if ((this._ckey === 'hex' && !/^\#[a-f0-9]{6}$/i.test(this.value))) {
1005       return;
1006     }
1007 
1008     if (this.getAttribute('type') === 'number') {
1009       var val = parseInt(this.value),
1010           min = this.getAttribute('min'),
1011           max = this.getAttribute('max');
1012 
1013       if (isNaN(val)) {
1014         val = min;
1015       }
1016 
1017       if (val < min) {
1018         val = min;
1019       } else if (val > max) {
1020         val = max;
1021       }
1022 
1023       if (val != this.value) {
1024         this.value = val;
1025       }
1026     }
1027 
1028     // Update the internal color value.
1029     if (this._ckey === 'hex') {
1030       _self.color[this._ckey] = this.value;
1031     } else if (_self.ckey_grouping[this._ckey] === 'lab') {
1032       _self.color[this._ckey] = parseInt(this.value);
1033     } else {
1034       _self.color[this._ckey] = parseInt(this.value) 
1035         / config.inputValues[this._ckey][1];
1036     }
1037 
1038     _self.update_color(this._ckey);
1039   };
1040 
1041   /**
1042    * Update the current color. Once a color value is updated, this method is 
1043    * called to keep the rest of the color mixer in sync: for example, when a RGB 
1044    * value is updated, it needs to be converted to HSV, CMYK and all of the 
1045    * other formats. Additionally, this method updates the color preview, the 
1046    * controls on the Canvas and the input values.
1047    *
1048    * <p>You need to call this function whenever you update the color manually.
1049    *
1050    * @param {String} ckey The color key that was updated.
1051    */
1052   this.update_color = function (ckey) {
1053     var group = _self.ckey_grouping[ckey] || ckey;
1054 
1055     switch (group) {
1056       case 'rgb':
1057         _self.rgb2hsv();
1058         _self.rgb2hex();
1059         _self.rgb2lab();
1060         _self.rgb2cmyk();
1061         break;
1062 
1063       case 'hsv':
1064         _self.hsv2rgb();
1065         _self.rgb2hex();
1066         _self.rgb2lab();
1067         _self.rgb2cmyk();
1068         break;
1069 
1070       case 'hex':
1071         _self.hex2rgb();
1072         _self.rgb2hsv();
1073         _self.rgb2lab();
1074         _self.rgb2cmyk();
1075         break;
1076 
1077       case 'lab':
1078         _self.lab2rgb();
1079         _self.rgb2hsv();
1080         _self.rgb2hex();
1081         _self.rgb2cmyk();
1082         break;
1083 
1084       case 'cmyk':
1085         _self.cmyk2rgb();
1086         _self.rgb2lab();
1087         _self.rgb2hsv();
1088         _self.rgb2hex();
1089     }
1090 
1091     _self.update_preview();
1092     _self.update_inputs();
1093 
1094     if (ckey !== 'alpha') {
1095       _self.update_canvas(ckey);
1096     }
1097   };
1098 
1099   /**
1100    * Update the color preview.
1101    * @private
1102    */
1103   this.update_preview = function () {
1104     var red   = MathRound(_self.color.red   * 255),
1105         green = MathRound(_self.color.green * 255),
1106         blue  = MathRound(_self.color.blue  * 255),
1107         style = _self.elems.colorActive.style;
1108 
1109     style.backgroundColor = 'rgb(' + red + ',' + green + ',' + blue + ')';
1110     style.opacity = _self.color.alpha;
1111   };
1112 
1113   /**
1114    * Update the color inputs. This method takes the internal color values and 
1115    * shows them in the DOM input elements.
1116    * @private
1117    */
1118   this.update_inputs = function () {
1119     var input;
1120     for (var i in _self.inputs) {
1121       input = _self.inputs[i];
1122       input._old_value = input.value;
1123       if (input._ckey === 'hex') {
1124         input.value = _self.color[i];
1125       } else if (_self.ckey_grouping[input._ckey] === 'lab') {
1126         input.value = MathRound(_self.color[i]);
1127       } else {
1128         input.value = MathRound(_self.color[i] * config.inputValues[i][1]);
1129       }
1130     }
1131   };
1132 
1133   /**
1134    * Convert RGB to CMYK. This uses the current color RGB values and updates the 
1135    * CMYK values accordingly.
1136    * @private
1137    */
1138   // Quote from Wikipedia:
1139   // "Since RGB and CMYK spaces are both device-dependent spaces, there is no 
1140   // simple or general conversion formula that converts between them.  
1141   // Conversions are generally done through color management systems, using 
1142   // color profiles that describe the spaces being converted. Nevertheless, the 
1143   // conversions cannot be exact, since these spaces have very different 
1144   // gamuts."
1145   // Translation: this is just a simple RGB to CMYK conversion function.
1146   this.rgb2cmyk = function () {
1147     var color = _self.color,
1148         cyan, magenta, yellow, black,
1149         red   = color.red,
1150         green = color.green,
1151         blue  = color.blue;
1152 
1153     cyan    = 1 - red;
1154     magenta = 1 - green;
1155     yellow  = 1 - blue;
1156 
1157     black = MathMin(cyan, magenta, yellow, 1);
1158 
1159     if (black === 1) {
1160       cyan = magenta = yellow = 0;
1161     } else {
1162       var w = 1 - black;
1163       cyan    = (cyan    - black) / w;
1164       magenta = (magenta - black) / w;
1165       yellow  = (yellow  - black) / w;
1166     }
1167 
1168     color.cyan    = cyan;
1169     color.magenta = magenta;
1170     color.yellow  = yellow;
1171     color.black   = black;
1172   };
1173 
1174   /**
1175    * Convert CMYK to RGB (internally).
1176    * @private
1177    */
1178   this.cmyk2rgb = function () {
1179     var color = _self.color,
1180         w = 1 - color.black;
1181 
1182     color.red   = 1 - color.cyan    * w - color.black;
1183     color.green = 1 - color.magenta * w - color.black;
1184     color.blue  = 1 - color.yellow  * w - color.black;
1185   };
1186 
1187   /**
1188    * Convert RGB to HSV (internally).
1189    * @private
1190    */
1191   this.rgb2hsv = function () {
1192     var hue, sat, val, // HSV
1193         red   = _self.color.red,
1194         green = _self.color.green,
1195         blue  = _self.color.blue,
1196         min   = MathMin(red, green, blue),
1197         max   = MathMax(red, green, blue),
1198         delta = max - min,
1199         val   = max;
1200 
1201     // This is gray (red==green==blue)
1202     if (delta === 0) {
1203       hue = sat = 0;
1204     } else {
1205       sat = delta / max;
1206 
1207       if (max === red) {
1208         hue = (green -  blue) / delta;
1209       } else if (max === green) {
1210         hue = (blue  -   red) / delta + 2;
1211       } else if (max ===  blue) {
1212         hue = (red   - green) / delta + 4;
1213       }
1214 
1215       hue /= 6;
1216       if (hue < 0) {
1217         hue += 1;
1218       }
1219     }
1220 
1221     _self.color.hue = hue;
1222     _self.color.sat = sat;
1223     _self.color.val = val;
1224   };
1225 
1226   /**
1227    * Convert HSV to RGB.
1228    *
1229    * @private
1230    *
1231    * @param {Boolean} [no_update] Tells the function to not update the internal 
1232    * RGB color values.
1233    * @param {Array} [hsv] The array holding the HSV values you want to convert 
1234    * to RGB. This array must have three elements ordered as: <var>hue</var>, 
1235    * <var>saturation</var> and <var>value</var> - all between 0 and 1. If you do 
1236    * not provide the array, then the internal HSV color values are used.
1237    *
1238    * @returns {Array} The RGB values converted from HSV. The array has three 
1239    * elements ordered as: <var>red</var>, <var>green</var> and <var>blue</var> 
1240    * - all with values between 0 and 1.
1241    */
1242   this.hsv2rgb = function (no_update, hsv) {
1243     var color = _self.color,
1244         red, green, blue, hue, sat, val;
1245 
1246     // Use custom HSV values or the current color.
1247     if (hsv) {
1248       hue = hsv[0];
1249       sat = hsv[1];
1250       val = hsv[2];
1251     } else {
1252       hue = color.hue,
1253       sat = color.sat,
1254       val = color.val;
1255     }
1256 
1257     // achromatic (grey)
1258     if (sat === 0) {
1259       red = green = blue = val;
1260     } else {
1261       var h = hue * 6;
1262       var i = MathFloor(h);
1263       var t1 = val * ( 1 - sat ),
1264           t2 = val * ( 1 - sat * ( h - i ) ),
1265           t3 = val * ( 1 - sat * ( 1 - (h - i) ) );
1266 
1267       if (i === 0 || i === 6) { //   0° Red
1268         red = val;  green =  t3;  blue =  t1;
1269       } else if (i === 1) {    //  60° Yellow
1270         red =  t2;  green = val;  blue =  t1;
1271       } else if (i === 2) {    // 120° Green
1272         red =  t1;  green = val;  blue =  t3;
1273       } else if (i === 3) {    // 180° Cyan
1274         red =  t1;  green =  t2;  blue = val;
1275       } else if (i === 4) {    // 240° Blue
1276         red =  t3;  green =  t1;  blue = val;
1277       } else if (i === 5) {    // 300° Magenta
1278         red = val;  green =  t1;  blue =  t2;
1279       }
1280     }
1281 
1282     if (!no_update) {
1283       color.red   = red;
1284       color.green = green;
1285       color.blue  = blue;
1286     }
1287 
1288     return [red, green, blue];
1289   };
1290 
1291   /**
1292    * Convert RGB to hexadecimal representation (internally).
1293    * @private
1294    */
1295   this.rgb2hex = function () {
1296     var hex = '#', rgb = ['red', 'green', 'blue'], i, val,
1297         color = _self.color;
1298 
1299     for (i = 0; i < 3; i++) {
1300       val = MathRound(color[rgb[i]] * 255).toString(16);
1301       if (val.length === 1) {
1302         val = '0' + val;
1303       }
1304       hex += val;
1305     }
1306 
1307     color.hex = hex;
1308   };
1309 
1310   /**
1311    * Convert the hexadecimal representation of color to RGB values (internally).
1312    * @private
1313    */
1314   this.hex2rgb = function () {
1315     var rgb = ['red', 'green', 'blue'], i, val,
1316         color = _self.color,
1317         hex   = color.hex;
1318 
1319     hex = hex.substr(1);
1320     if (hex.length !== 6) {
1321       return;
1322     }
1323 
1324     for (i = 0; i < 3; i++) {
1325       val = hex.substr(i*2, 2);
1326       color[rgb[i]] = parseInt(val, 16)/255;
1327     }
1328   };
1329 
1330   /**
1331    * Convert RGB to CIE Lab (internally).
1332    * @private
1333    */
1334   this.rgb2lab = function () {
1335     var color = _self.color,
1336         lab   = _self.xyz2lab(_self.rgb2xyz([color.red, color.green, 
1337               color.blue]));
1338 
1339     color.cie_l = lab[0];
1340     color.cie_a = lab[1];
1341     color.cie_b = lab[2];
1342   };
1343 
1344   /**
1345    * Convert CIE Lab values to RGB values (internally).
1346    * @private
1347    */
1348   this.lab2rgb = function () {
1349     var color = _self.color,
1350         rgb   = _self.xyz2rgb(_self.lab2xyz(color.cie_l, color.cie_a, 
1351               color.cie_b));
1352 
1353     color.red   = rgb[0];
1354     color.green = rgb[1];
1355     color.blue  = rgb[2];
1356   };
1357 
1358   /**
1359    * Convert XYZ color values into CIE Lab values.
1360    *
1361    * @private
1362    *
1363    * @param {Array} xyz The array holding the XYZ color values in order: 
1364    * <var>X</var>, <var>Y</var> and <var>Z</var>.
1365    *
1366    * @returns {Array} An array holding the CIE Lab values in order: 
1367    * <var>L</var>, <var>a</var> and <var>b</var>.
1368    */
1369   this.xyz2lab = function (xyz) {
1370     var cfg = config.lab,
1371 
1372         // 216/24389 or (6/29)^3 (both = 0.008856...)
1373         e = 216/24389,
1374 
1375         // 903.296296...
1376         k = 24389/27;
1377 
1378     xyz[0] /= cfg.w_x;
1379     xyz[1] /= cfg.w_y;
1380     xyz[2] /= cfg.w_z;
1381 
1382     if (xyz[0] > e) {
1383       xyz[0] = MathPow(xyz[0], 1/3);
1384     } else {
1385       xyz[0] = (k*xyz[0] + 16)/116;
1386     }
1387 
1388     if (xyz[1] > e) {
1389       xyz[1] = MathPow(xyz[1], 1/3);
1390     } else {
1391       xyz[1] = (k*xyz[1] + 16)/116;
1392     }
1393 
1394     if (xyz[2] > e) {
1395       xyz[2] = MathPow(xyz[2], 1/3);
1396     } else {
1397       xyz[2] = (k*xyz[2] + 16)/116;
1398     }
1399 
1400     var cie_l = 116 *  xyz[1] - 16,
1401         cie_a = 500 * (xyz[0] -  xyz[1]),
1402         cie_b = 200 * (xyz[1] -  xyz[2]);
1403 
1404     return [cie_l, cie_a, cie_b];
1405   };
1406 
1407   /**
1408    * Convert CIE Lab values to XYZ color values.
1409    *
1410    * @private
1411    *
1412    * @param {Number} cie_l The color lightness value.
1413    * @param {Number} cie_a The a* color opponent.
1414    * @param {Number} cie_b The b* color opponent.
1415    *
1416    * @returns {Array} An array holding the XYZ color values in order: 
1417    * <var>X</var>, <var>Y</var> and <var>Z</var>.
1418    */
1419   this.lab2xyz = function (cie_l, cie_a, cie_b) {
1420     var y = (cie_l + 16) / 116,
1421         x = y + cie_a / 500,
1422         z = y - cie_b / 200,
1423 
1424         // 0.206896551...
1425         e = 6/29,
1426 
1427         // 7.787037...
1428         k = 1/3 * MathPow(29/6, 2),
1429 
1430         // 0.137931...
1431         t = 16/116,
1432         cfg = config.lab;
1433 
1434     if (x > e) {
1435       x = MathPow(x, 3);
1436     } else {
1437       x = (x - t) / k;
1438     }
1439 
1440     if (y > e) {
1441       y = MathPow(y, 3);
1442     } else {
1443       y = (y - t) / k;
1444     }
1445 
1446     if (z > e) {
1447       z = MathPow(z, 3);
1448     } else {
1449       z = (z - t) / k;
1450     }
1451 
1452     x *= cfg.w_x;
1453     y *= cfg.w_y;
1454     z *= cfg.w_z;
1455 
1456     return [x, y, z];
1457   };
1458 
1459   /**
1460    * Convert XYZ color values to RGB.
1461    *
1462    * @private
1463    *
1464    * @param {Array} xyz The array holding the XYZ color values in order: 
1465    * <var>X</var>, <var>Y</var> and <var>Z</var>
1466    *
1467    * @returns {Array} An array holding the RGB values in order: <var>red</var>, 
1468    * <var>green</var> and <var>blue</var>.
1469    */
1470   this.xyz2rgb = function (xyz) {
1471     var rgb = _self.calc_m1x3(xyz, config.lab.m_i);
1472 
1473     if (rgb[0] > 0.0031308) {
1474       rgb[0] = 1.055 * MathPow(rgb[0], 1 / 2.4) - 0.055;
1475     } else {
1476       rgb[0] *= 12.9232;
1477     }
1478 
1479     if (rgb[1] > 0.0031308) {
1480       rgb[1] = 1.055 * MathPow(rgb[1], 1 / 2.4) - 0.055;
1481     } else {
1482       rgb[1] *= 12.9232;
1483     }
1484 
1485     if (rgb[2] > 0.0031308) {
1486       rgb[2] = 1.055 * MathPow(rgb[2], 1 / 2.4) - 0.055;
1487     } else {
1488       rgb[2] *= 12.9232;
1489     }
1490 
1491     if (rgb[0] < 0) {
1492       rgb[0] = 0;
1493     } else if (rgb[0] > 1) {
1494       rgb[0] = 1;
1495     }
1496 
1497     if (rgb[1] < 0) {
1498       rgb[1] = 0;
1499     } else if (rgb[1] > 1) {
1500       rgb[1] = 1;
1501     }
1502 
1503     if (rgb[2] < 0) {
1504       rgb[2] = 0;
1505     } else if (rgb[2] > 1) {
1506       rgb[2] = 1;
1507     }
1508 
1509     return rgb;
1510   };
1511 
1512   /**
1513    * Convert RGB values to XYZ color values.
1514    *
1515    * @private
1516    *
1517    * @param {Array} rgb The array holding the RGB values in order: 
1518    * <var>red</var>, <var>green</var> and <var>blue</var>.
1519    *
1520    * @returns {Array} An array holding the XYZ color values in order: 
1521    * <var>X</var>, <var>Y</var> and <var>Z</var>.
1522    */
1523   this.rgb2xyz = function (rgb) {
1524     if (rgb[0] > 0.04045) {
1525       rgb[0] = MathPow(( rgb[0] + 0.055 ) / 1.055, 2.4);
1526     } else {
1527       rgb[0] /= 12.9232;
1528     }
1529 
1530     if (rgb[1] > 0.04045) {
1531       rgb[1] = MathPow(( rgb[1] + 0.055 ) / 1.055, 2.4);
1532     } else {
1533       rgb[1] /= 12.9232;
1534     }
1535 
1536     if (rgb[2] > 0.04045) {
1537       rgb[2] = MathPow(( rgb[2] + 0.055 ) / 1.055, 2.4);
1538     } else {
1539       rgb[2] /= 12.9232;
1540     }
1541 
1542     return _self.calc_m1x3(rgb, config.lab.m);
1543   };
1544 
1545   /**
1546    * Update the color space visualisation. This method updates the color chart 
1547    * and/or the color slider, and the associated controls, each as needed when 
1548    * a color key is updated.
1549    *
1550    * @private
1551    *
1552    * @param {String} updated_ckey The color key that was updated.
1553    * @param {Boolean} [force=false] Tells the function to force an update. The 
1554    * Canvas is not updated when the color mixer panel is not visible.
1555    *
1556    * @returns {Boolean} If the operation was successful, or false if not.
1557    */
1558   this.update_canvas = function (updated_ckey, force) {
1559     if (_self.panelSelector.tabId !== 'mixer' && !force) {
1560       _self.update_canvas_needed = true;
1561       return true;
1562     }
1563 
1564     _self.update_canvas_needed = false;
1565 
1566     var slider  = _self.elems.slider.style,
1567         chart   = _self.elems.chartDot.style,
1568         color   = _self.color,
1569         ckey    = _self.ckey_active,
1570         group   = _self.ckey_active_group,
1571         adjoint = _self.ckey_adjoint,
1572         width   = _self.chartWidth  / resScale,
1573         height  = _self.chartHeight / resScale,
1574         mx, my, sy;
1575 
1576     // Update the slider which shows the position of the active ckey.
1577     if (updated_ckey !== adjoint[0] && updated_ckey !== adjoint[1] && 
1578         _self.ev_canvas_mode !== 'chart') {
1579       if (group === 'lab') {
1580         sy = (color[ckey] - config.inputValues[ckey][0]) / _self.abs_max[ckey];
1581       } else {
1582         sy = color[ckey];
1583       }
1584 
1585       if (ckey !== 'hue' && group !== 'lab') {
1586         sy = 1 - sy;
1587       }
1588 
1589       slider.top = MathRound(sy * height) + 'px';
1590     }
1591 
1592     // Update the chart dot.
1593     if (updated_ckey !== ckey) {
1594       if (group === 'lab') {
1595         mx = (color[adjoint[0]] - config.inputValues[adjoint[0]][0]) 
1596           / _self.abs_max[adjoint[0]];
1597         my = (color[adjoint[1]] - config.inputValues[adjoint[1]][0]) 
1598           / _self.abs_max[adjoint[1]];
1599       } else {
1600         mx = color[adjoint[0]];
1601         my = 1 - color[adjoint[1]];
1602       }
1603 
1604       chart.top  = MathRound(my * height) + 'px';
1605       chart.left = MathRound(mx *  width) + 'px';
1606     }
1607 
1608     if (!_self.draw_chart(updated_ckey) || !_self.draw_slider(updated_ckey)) {
1609       return false;
1610     } else {
1611       return true;
1612     }
1613   };
1614 
1615   /**
1616    * The mouse events handler for the Canvas controls. This method determines 
1617    * the region the user is using, and it also updates the color values for the 
1618    * active color key. The Canvas and all the inputs in the color mixer are 
1619    * updated as needed.
1620    *
1621    * @private
1622    * @param {Event} ev The DOM Event object.
1623    */
1624   this.ev_canvas = function (ev) {
1625     ev.preventDefault();
1626 
1627     // Initialize color picking only on mousedown.
1628     if (ev.type === 'mousedown' && !_self.ev_canvas_mode) {
1629       _self.ev_canvas_mode = true;
1630       doc.addEventListener('mouseup', _self.ev_canvas, false);
1631     }
1632 
1633     if (!_self.ev_canvas_mode) {
1634       return false;
1635     }
1636 
1637     // The mouseup event stops the effect of any further mousemove events.
1638     if (ev.type === 'mouseup') {
1639       _self.ev_canvas_mode = false;
1640       doc.removeEventListener('mouseup', _self.ev_canvas, false);
1641     }
1642 
1643     var elems = _self.elems;
1644 
1645     // If the user is on top of the 'controls' element, determine the mouse coordinates and the 'mode' for this function: the user is either working with the slider, or he/she is working with the color chart itself.
1646     if (ev.target === elems.controls) {
1647       var mx, my,
1648           width  = _self.context2d.canvas.width,
1649           height = _self.context2d.canvas.height;
1650 
1651       // Get the mouse position, relative to the event target.
1652       if (ev.layerX || ev.layerX === 0) { // Firefox
1653         mx = ev.layerX * resScale;
1654         my = ev.layerY * resScale;
1655       } else if (ev.offsetX || ev.offsetX === 0) { // Opera
1656         mx = ev.offsetX * resScale;
1657         my = ev.offsetY * resScale;
1658       }
1659 
1660       if (mx >= 0 && mx <= _self.chartWidth) {
1661         mode = 'chart';
1662       } else if (mx >= _self.sliderX && mx <= width) {
1663         mode = 'slider';
1664       }
1665     } else {
1666       // The user might have clicked on the chart dot, or on the slider graphic 
1667       // itself.
1668       // If yes, then determine the mode based on this.
1669       if (ev.target === elems.chartDot) {
1670         mode = 'chart';
1671       } else if (ev.target === elems.slider) {
1672         mode = 'slider';
1673       }
1674     }
1675 
1676     // Update the ev_canvas_mode value to include the mode name, if it's simply 
1677     // the true boolean.
1678     // This ensures that the continuous mouse movements do not go from one mode 
1679     // to another when the user moves out from the slider to the chart (and 
1680     // vice-versa).
1681     if (mode && _self.ev_canvas_mode === true) {
1682       _self.ev_canvas_mode = mode;
1683     }
1684 
1685     // Do not continue if the mode wasn't determined (the mouse is not on the 
1686     // slider, nor on the chart).
1687     // Also don't continue if the mouse is not in the same place (different 
1688     // mode).
1689     if (!mode || _self.ev_canvas_mode !== mode || ev.target !== elems.controls) 
1690     {
1691       return false;
1692     }
1693 
1694     var color = _self.color,
1695         val_x = mx / _self.chartWidth,
1696         val_y = my / height;
1697 
1698     if (mode === 'slider') {
1699       if (_self.ckey_active === 'hue') {
1700         color[_self.ckey_active] = val_y;
1701       } else if (_self.ckey_active_group === 'lab') {
1702         color[_self.ckey_active] = _self.abs_max[_self.ckey_active] * val_y 
1703           + config.inputValues[_self.ckey_active][0];
1704       } else {
1705         color[_self.ckey_active] = 1 - val_y;
1706       }
1707 
1708       return _self.update_color(_self.ckey_active);
1709 
1710     } else if (mode === 'chart') {
1711       if (val_x > 1) {
1712         return false;
1713       }
1714 
1715       if (_self.ckey_active_group === 'lab') {
1716         val_x = _self.abs_max[_self.ckey_adjoint[0]] * val_x 
1717           + config.inputValues[_self.ckey_adjoint[0]][0];
1718         val_y = _self.abs_max[_self.ckey_adjoint[1]] * val_y 
1719           + config.inputValues[_self.ckey_adjoint[1]][0];
1720       } else {
1721         val_y = 1 - val_y;
1722       }
1723 
1724       color[_self.ckey_adjoint[0]] = val_x;
1725       color[_self.ckey_adjoint[1]] = val_y;
1726 
1727       return _self.update_color(_self.ckey_active_group);
1728     }
1729 
1730     return false;
1731   };
1732 
1733   /**
1734    * Draw the color space visualisation.
1735    *
1736    * @private
1737    *
1738    * @param {String} updated_ckey The color key that was updated. This is used 
1739    * to determine if the Canvas needs to be updated or not.
1740    */
1741   this.draw_chart = function (updated_ckey) {
1742     var context = _self.context2d,
1743         gradient, color, opacity, i;
1744 
1745     if (updated_ckey === _self.ckey_adjoint[0] || updated_ckey === 
1746         _self.ckey_adjoint[1] || (_self.ev_canvas_mode === 'chart' && 
1747           updated_ckey === _self.ckey_active_group)) {
1748       return true;
1749     }
1750 
1751     var w = _self.chartWidth,
1752         h = _self.chartHeight;
1753 
1754     context.clearRect(0, 0, w, h);
1755 
1756     if (_self.ckey_active === 'sat') {
1757       // In saturation mode the user has the slider which allows him/her to 
1758       // change the saturation (hSv) of the current color.
1759       // The chart shows the hue spectrum on the X axis, while the Y axis gives 
1760       // the Value (hsV).
1761 
1762       if (_self.color.sat > 0) {
1763         // Draw the hue spectrum gradient on the X axis.
1764         gradient = context.createLinearGradient(0, 0, w, 0);
1765         for (i = 0; i <= 6; i++) {
1766           color = 'rgb(' + hueSpectrum[i][0] + ', ' +
1767               hueSpectrum[i][1] + ', ' +
1768               hueSpectrum[i][2] + ')';
1769           gradient.addColorStop(i * 1/6, color);
1770         }
1771         context.fillStyle = gradient;
1772         context.fillRect(0, 0, w, h);
1773 
1774         // Draw the gradient which darkens the hue spectrum on the Y axis.
1775         gradient = context.createLinearGradient(0, 0, 0, h);
1776         gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
1777         gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
1778         context.fillStyle = gradient;
1779         context.fillRect(0, 0, w, h);
1780       }
1781 
1782       if (_self.color.sat < 1) {
1783         // Draw the white to black gradient. This is used for creating the 
1784         // saturation effect. Lowering the saturation value makes the gradient 
1785         // more visible, hence the hue colors desaturate.
1786         opacity = 1 - _self.color.sat;
1787         gradient = context.createLinearGradient(0, 0, 0, h);
1788         gradient.addColorStop(0, 'rgba(255, 255, 255, ' + opacity + ')');
1789         gradient.addColorStop(1, 'rgba(  0,   0,   0, ' + opacity + ')');
1790         context.fillStyle = gradient;
1791         context.fillRect(0, 0, w, h);
1792       }
1793 
1794     } else if (_self.ckey_active === 'val') {
1795       // In value mode the user has the slider which allows him/her to change the value (hsV) of the current color.
1796       // The chart shows the hue spectrum on the X axis, while the Y axis gives the saturation (hSv).
1797 
1798       if (_self.color.val > 0) {
1799         // Draw the hue spectrum gradient on the X axis.
1800         gradient = context.createLinearGradient(0, 0, w, 0);
1801         for (i = 0; i <= 6; i++) {
1802           color = 'rgb(' + hueSpectrum[i][0] + ', ' +
1803             hueSpectrum[i][1] + ', ' +
1804             hueSpectrum[i][2] + ')';
1805           gradient.addColorStop(i * 1/6, color);
1806         }
1807         context.fillStyle = gradient;
1808         context.fillRect(0, 0, w, h);
1809 
1810         // Draw the gradient which lightens the hue spectrum on the Y axis.
1811         gradient = context.createLinearGradient(0, 0, 0, h);
1812         gradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
1813         gradient.addColorStop(1, 'rgba(255, 255, 255, 1)');
1814         context.fillStyle = gradient;
1815         context.fillRect(0, 0, w, h);
1816       }
1817 
1818       if (_self.color.val < 1) {
1819         // Draw a solid black color on top. This is used for darkening the hue colors gradient when the user reduces the Value (hsV).
1820         context.fillStyle = 'rgba(0, 0, 0, ' + (1 - _self.color.val) +')';
1821         context.fillRect(0, 0, w, h);
1822       }
1823 
1824     } else if (_self.ckey_active === 'hue') {
1825       // In hue mode the user has the slider which allows him/her to change the hue (Hsv) of the current color.
1826       // The chart shows the current color in the background. The X axis gives the saturation (hSv), and the Y axis gives the value (hsV).
1827 
1828       if (_self.color.sat === 1 && _self.color.val === 1) {
1829         color = [_self.color.red, _self.color.green, _self.color.blue];
1830       } else {
1831         // Determine the RGB values for the current color which has the same hue, but maximum saturation and value (hSV).
1832         color = _self.hsv2rgb(true, [_self.color.hue, 1, 1]);
1833       }
1834       for (i = 0; i < 3; i++) {
1835         color[i] = MathRound(color[i] * 255);
1836       }
1837 
1838       context.fillStyle = 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')';
1839       context.fillRect(0, 0, w, h);
1840 
1841       // Draw the white gradient for saturation (X axis, hSv).
1842       gradient = context.createLinearGradient(0, 0, w, 0);
1843       gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
1844       gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
1845       context.fillStyle = gradient;
1846       context.fillRect(0, 0, w, h);
1847 
1848       // Draw the black gradient for value (Y axis, hsV).
1849       gradient = context.createLinearGradient(0, 0, 0, h);
1850       gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
1851       gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
1852       context.fillStyle = gradient;
1853       context.fillRect(0, 0, w, h);
1854 
1855     } else if (_self.ckey_active_group === 'rgb') {
1856       // In any red/green/blue mode the background color becomes the one of the ckey_active. Say, for ckey_active=red the background color would be the current red value (green and blue are both set to 0).
1857       // On the X/Y axes the other two colors are shown. E.g. for red the X axis gives the green gradient, and the Y axis gives the blue gradient. The two gradients are drawn on top of the red background using a global composite operation (lighter) - to create the color addition effect.
1858       var color2, color3;
1859 
1860       color = {'red' : 0, 'green' : 0, 'blue' : 0};
1861       color[_self.ckey_active] = MathRound(_self.color[_self.ckey_active] 
1862           * 255);
1863 
1864       color2 = {'red' : 0, 'green' : 0, 'blue' : 0};
1865       color2[_self.ckey_adjoint[1]] = 255;
1866 
1867       color3 = {'red' : 0, 'green' : 0, 'blue' : 0};
1868       color3[_self.ckey_adjoint[0]] = 255;
1869 
1870       // The background.
1871       context.fillStyle = 'rgb(' + color.red + ',' + color.green + ',' + color.blue + ')';
1872       context.fillRect(0, 0, w, h);
1873 
1874       // This doesn't work in Opera 9.2 and older versions.
1875       var op = context.globalCompositeOperation;
1876       context.globalCompositeOperation = 'lighter';
1877 
1878       // The Y axis gradient.
1879       gradient = context.createLinearGradient(0, 0, 0, h);
1880       gradient.addColorStop(0, 'rgba(' + color2.red + ',' + color2.green + ',' + color2.blue + ', 1)');
1881       gradient.addColorStop(1, 'rgba(' + color2.red + ',' + color2.green + ',' + color2.blue + ', 0)');
1882       context.fillStyle = gradient;
1883       context.fillRect(0, 0, w, h);
1884 
1885       // The X axis gradient.
1886       gradient = context.createLinearGradient(0, 0, w, 0);
1887       gradient.addColorStop(0, 'rgba(' + color3.red + ',' + color3.green + ',' + color3.blue + ', 0)');
1888       gradient.addColorStop(1, 'rgba(' + color3.red + ',' + color3.green + ',' + color3.blue + ', 1)');
1889       context.fillStyle = gradient;
1890       context.fillRect(0, 0, w, h);
1891 
1892       context.globalCompositeOperation = op;
1893 
1894     } else if (_self.ckey_active_group === 'lab') {
1895       // The chart plots the CIE Lab colors. The non-active color keys give the X/Y axes. For example, if cie_l (lightness) is active, then the cie_a values give the X axis, and the Y axis is given by the values of cie_b.
1896       // The chart is drawn manually, pixel-by-pixel, due to the special way CIE Lab works. This is very slow in today's UAs.
1897 
1898       var imgd = false;
1899 
1900       if (context.createImageData) {
1901         imgd = context.createImageData(w, h);
1902       } else if (context.getImageData) {
1903         imgd = context.getImageData(0, 0, w, h);
1904       } else {
1905         imgd = {
1906           'width'  : w,
1907           'height' : h,
1908           'data'   : new Array(w*h*4)
1909         };
1910       }
1911 
1912       var pix = imgd.data,
1913           n = imgd.data.length - 1,
1914           i = -1, p = 0, inc_x, inc_y, xyz = [], rgb = [], cie_x, cie_y;
1915 
1916       cie_x = _self.ckey_adjoint[0];
1917       cie_y = _self.ckey_adjoint[1];
1918 
1919       color = {
1920         'cie_l' : _self.color.cie_l,
1921         'cie_a' : _self.color.cie_a,
1922         'cie_b' : _self.color.cie_b
1923       };
1924 
1925       inc_x = _self.abs_max[cie_x] / w;
1926       inc_y = _self.abs_max[cie_y] / h;
1927 
1928       color[cie_x] = config.inputValues[cie_x][0];
1929       color[cie_y] = config.inputValues[cie_y][0];
1930 
1931       while (i < n) {
1932         xyz = _self.lab2xyz(color.cie_l, color.cie_a, color.cie_b);
1933         rgb = _self.xyz2rgb(xyz);
1934 
1935         pix[++i] = MathRound(rgb[0]*255);
1936         pix[++i] = MathRound(rgb[1]*255);
1937         pix[++i] = MathRound(rgb[2]*255);
1938         pix[++i] = 255;
1939 
1940         p++;
1941         color[cie_x] += inc_x;
1942 
1943         if ((p % w) === 0) {
1944           color[cie_x] = config.inputValues[cie_x][0];
1945           color[cie_y] += inc_y;
1946         }
1947       }
1948 
1949       context.putImageData(imgd, 0, 0);
1950     }
1951 
1952     return true;
1953   };
1954 
1955   /**
1956    * Draw the color slider on the Canvas element.
1957    *
1958    * @private
1959    *
1960    * @param {String} updated_ckey The color key that was updated. This is used 
1961    * to determine if the Canvas needs to be updated or not.
1962    */
1963   this.draw_slider = function (updated_ckey) {
1964     if (_self.ckey_active === updated_ckey) {
1965       return true;
1966     }
1967 
1968     var context  = _self.context2d,
1969         slider_w = _self.sliderWidth,
1970         slider_h = _self.sliderHeight,
1971         slider_x = _self.sliderX,
1972         slider_y = 0,
1973         gradient, color, i;
1974 
1975     gradient = context.createLinearGradient(slider_x, slider_y, slider_x, slider_h);
1976 
1977     if (_self.ckey_active === 'hue') {
1978       // Draw the hue spectrum gradient.
1979       for (i = 0; i <= 6; i++) {
1980         color = 'rgb(' + hueSpectrum[i][0] + ', ' +
1981             hueSpectrum[i][1] + ', ' +
1982             hueSpectrum[i][2] + ')';
1983         gradient.addColorStop(i * 1/6, color);
1984       }
1985       context.fillStyle = gradient;
1986       context.fillRect(slider_x, slider_y, slider_w, slider_h);
1987 
1988       if (_self.color.sat < 1) {
1989         context.fillStyle = 'rgba(255, 255, 255, ' +
1990           (1 - _self.color.sat) + ')';
1991         context.fillRect(slider_x, slider_y, slider_w, slider_h);
1992       }
1993       if (_self.color.val < 1) {
1994         context.fillStyle = 'rgba(0, 0, 0, ' + (1 - _self.color.val) + ')';
1995         context.fillRect(slider_x, slider_y, slider_w, slider_h);
1996       }
1997 
1998     } else if (_self.ckey_active === 'sat') {
1999       // Draw the saturation gradient for the slider.
2000       // The start color is the current color with maximum saturation. The bottom gradient color is the same "color" without saturation.
2001       // The slider allows you to desaturate the current color.
2002 
2003       // Determine the RGB values for the current color which has the same hue and value (HsV), but maximum saturation (hSv).
2004       if (_self.color.sat === 1) {
2005         color = [_self.color.red, _self.color.green, _self.color.blue];
2006       } else {
2007         color = _self.hsv2rgb(true, [_self.color.hue, 1, _self.color.val]);
2008       }
2009 
2010       for (i = 0; i < 3; i++) {
2011         color[i] = MathRound(color[i] * 255);
2012       }
2013 
2014       var gray = MathRound(_self.color.val * 255);
2015       gradient.addColorStop(0, 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')');
2016       gradient.addColorStop(1, 'rgb(' + gray     + ', ' + gray     + ', ' + gray     + ')');
2017       context.fillStyle = gradient;
2018       context.fillRect(slider_x, slider_y, slider_w, slider_h);
2019 
2020     } else if (_self.ckey_active === 'val') {
2021       // Determine the RGB values for the current color which has the same hue and saturation, but maximum value (hsV).
2022       if (_self.color.val === 1) {
2023         color = [_self.color.red, _self.color.green, _self.color.blue];
2024       } else {
2025         color = _self.hsv2rgb(true, [_self.color.hue, _self.color.sat, 1]);
2026       }
2027 
2028       for (i = 0; i < 3; i++) {
2029         color[i] = MathRound(color[i] * 255);
2030       }
2031 
2032       gradient.addColorStop(0, 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')');
2033       gradient.addColorStop(1, 'rgb(0, 0, 0)');
2034       context.fillStyle = gradient;
2035       context.fillRect(slider_x, slider_y, slider_w, slider_h);
2036 
2037     } else if (_self.ckey_active_group === 'rgb') {
2038       var red   = MathRound(_self.color.red   * 255),
2039           green = MathRound(_self.color.green * 255),
2040           blue  = MathRound(_self.color.blue  * 255);
2041 
2042       color = {
2043         'red'   : red,
2044         'green' : green,
2045         'blue'  : blue
2046       };
2047       color[_self.ckey_active] = 255;
2048 
2049       var color2 = {
2050         'red'   : red,
2051         'green' : green,
2052         'blue'  : blue
2053       };
2054       color2[_self.ckey_active] = 0;
2055 
2056       gradient.addColorStop(0, 'rgb(' + color.red  + ',' + color.green  + ',' + color.blue  + ')');
2057       gradient.addColorStop(1, 'rgb(' + color2.red + ',' + color2.green + ',' + color2.blue + ')');
2058       context.fillStyle = gradient;
2059       context.fillRect(slider_x, slider_y, slider_w, slider_h);
2060 
2061     } else if (_self.ckey_active_group === 'lab') {
2062       // The slider shows a gradient with the current color key going from the minimum to the maximum value. The gradient is calculated pixel by pixel, due to the special way CIE Lab is defined.
2063 
2064       var imgd = false;
2065 
2066       if (context.createImageData) {
2067         imgd = context.createImageData(1, slider_h);
2068       } else if (context.getImageData) {
2069         imgd = context.getImageData(0, 0, 1, slider_h);
2070       } else {
2071         imgd = {
2072           'width'  : 1,
2073           'height' : slider_h,
2074           'data'   : new Array(slider_h*4)
2075         };
2076       }
2077 
2078       var pix = imgd.data,
2079           n = imgd.data.length - 1,
2080           ckey = _self.ckey_active,
2081           i = -1, inc, xyz, rgb;
2082 
2083       color = {
2084         'cie_l' : _self.color.cie_l,
2085         'cie_a' : _self.color.cie_a,
2086         'cie_b' : _self.color.cie_b
2087       };
2088 
2089       color[ckey] = config.inputValues[ckey][0];
2090       inc = _self.abs_max[ckey] / slider_h;
2091 
2092       while (i < n) {
2093         xyz = _self.lab2xyz(color.cie_l, color.cie_a, color.cie_b);
2094         rgb = _self.xyz2rgb(xyz);
2095         pix[++i] = MathRound(rgb[0]*255);
2096         pix[++i] = MathRound(rgb[1]*255);
2097         pix[++i] = MathRound(rgb[2]*255);
2098         pix[++i] = 255;
2099 
2100         color[ckey] += inc;
2101       }
2102 
2103       for (i = 0; i <= slider_w; i++) {
2104         context.putImageData(imgd, slider_x+i, slider_y);
2105       }
2106     }
2107 
2108     context.strokeStyle = '#6d6d6d';
2109     context.strokeRect(slider_x, slider_y, slider_w, slider_h);
2110 
2111     return true;
2112   };
2113 };
2114 
2115 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
2116 
2117