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