/* Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Motorola Mobility LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ //////////////////////////////////////////////////////////////////////// // var Montage = require("montage/core/core").Montage, Component = require("montage/ui/component").Component; //////////////////////////////////////////////////////////////////////// // exports.ColorModel = Montage.create(Component, { //////////////////////////////////////////////////////////////////// // hasTemplate: { enumerable: false, value: false }, //////////////////////////////////////////////////////////////////// //HSV Value of current color selected _gradient: { enumerable: false, value: null }, //////////////////////////////////////////////////////////////////// //HSV Value of current color selected gradient: { enumerable: true, get: function() { return this._gradient; }, set: function(value) { this._gradient = value; //Updating color selected (converting to all modes) this.updateColorSelected('gradient', value); } }, //////////////////////////////////////////////////////////////////// //HSV Value of current color selected _hsv: { enumerable: false, value: null }, //////////////////////////////////////////////////////////////////// //HSV Value of current color selected hsv: { enumerable: true, get: function() { return this._hsv; }, set: function(value) { this._hsv = value; //Updating color selected (converting to all modes) this.updateColorSelected('hsv', value); } }, //////////////////////////////////////////////////////////////////// //RGB Value of current color selected _rgb: { enumerable: false, value: null }, //////////////////////////////////////////////////////////////////// //RGB Value of current color selected rgb: { enumerable: true, get: function() { return this._rgb; }, set: function(value) { this._rgb = value; //Updating color selected (converting to all modes) this.updateColorSelected('rgb', value); } }, //////////////////////////////////////////////////////////////////// //HSL Value of current color selected _hsl: { enumerable: false, value: null }, //////////////////////////////////////////////////////////////////// //HSL Value of current color selected hsl: { enumerable: true, get: function() { return this._hsl; }, set: function(value) { this._hsl = value; //Updating color selected (converting to all modes) this.updateColorSelected('hsl', value); } }, //////////////////////////////////////////////////////////////////// //HEX Value of current color selected _hex: { numerable: false, value: null }, //////////////////////////////////////////////////////////////////// //HEX Value of current color selected hex: { enumerable: true, get: function() { return this._hex; }, set: function(value) { this._hex = value; //Updating color selected (converting to all modes) this.updateColorSelected('hex', value); } }, //////////////////////////////////////////////////////////////////// //ALPHA Value of current color selected _alpha: { enumerable: false, value: {value: 1, type: 'change', wasSetByCode: true} }, //////////////////////////////////////////////////////////////////// //ALPHA Value of current color selected alpha: { enumerable: true, get: function() { return this._alpha; }, set: function(value) { value.value = Math.ceil(value.value*100)/100; this._alpha = value; // if (this.rgb || this.hsl) { this._dispatchChangeEvent('alpha', value); } } }, //////////////////////////////////////////////////////////////////// //Input (fill or stroke) Value of current color selected _input: { enumerable: false, value: 'fill' }, //////////////////////////////////////////////////////////////////// //Input Value of current color selected input: { enumerable: true, get: function() { return this._input; }, set: function(value) { this._input = value; //Dispatching change event this._dispatchChangeEvent('input', value); } }, //////////////////////////////////////////////////////////////////// //Color mode of current color selected _mode: { enumerable: false, value: null }, //////////////////////////////////////////////////////////////////// //Color mode of current color selected mode: { enumerable: true, get: function() { return this._mode; }, set: function(value) { this._mode = value; //Dispatching change event this._dispatchChangeEvent('mode', value); } }, //////////////////////////////////////////////////////////////////// //Stroke Color Value of current color selected _stroke: { enumerable: false, value: null }, //////////////////////////////////////////////////////////////////// //Stroke Color Value of current color selected stroke: { enumerable: true, get: function() { return this._stroke; }, set: function(value) { this._stroke = value; } }, //////////////////////////////////////////////////////////////////// //Fill Color Value of current color selected _fill: { enumerable: false, value: null }, //////////////////////////////////////////////////////////////////// //Fill Color Value of current color selected fill: { enumerable: true, get: function() { return this._fill; }, set: function(value) { this._fill = value; } }, //////////////////////////////////////////////////////////////////// //History Value array of current color selected colorHistory: { enumerable: false, value: {stroke: [{m: 'rgb', c: {r: 0, g: 0, b: 0}, a: 1}, {m: 'rgb', c: {r: 0, g: 0, b: 0}, a: 1}], fill: [{m: 'rgb', c: {r: 0, g: 0, b: 0}, a: 1}, {m: 'rgb', c: {r: 0, g: 0, b: 0}, a: 1}]} }, //////////////////////////////////////////////////////////////////// //History Value array of current color selected _addColorHistory: { enumerable: true, value: function(input, mode, color, alpha) { //TODO: Add limit if (this.colorHistory[input.toLowerCase()].length > 1) { if (this.colorHistory[input.toLowerCase()][this.colorHistory[input.toLowerCase()].length-1].c !== color || this.colorHistory[input.toLowerCase()][this.colorHistory[input.toLowerCase()].length-1].a !== alpha.value) { this.colorHistory[input.toLowerCase()].push({m: mode, c: color, a: alpha.value}); } } else { this.colorHistory[input.toLowerCase()].push({m: mode, c: color, a: alpha.value}); } } }, //////////////////////////////////////////////////////////////////// // applyNoColor: { enumerable: true, value: function (code) { // var nocolor = {}; nocolor.wasSetByCode = code; nocolor.type = 'change'; this.updateColorSelected('nocolor', nocolor); } }, //////////////////////////////////////////////////////////////////// //Method to update color selected and convert to other modes updateColorSelected: { enumerable: true, //value: function (input, mode, color, alpha) { value: function (mode, color) { //////////////////////////////////////////////////////////// //Checking for color mode to convert colors switch (mode.toLocaleLowerCase()) { //////////////////////////////////////////////////////// case 'gradient': //Checking for match of previous gradient if (color !== this.gradient) { //Setting value and breaking out of function this.gradient = color; return; } // this._hex = '------'; this._rgb = null; this._hsv = null; this._hsl = null; break; //////////////////////////////////////////////////////// case 'rgb': //Checking for match of previous (RGB) if (color.r !== this.rgb.r && color.g !== this.rgb.g && color.b !== this.rgb.b) { //Setting value and breaking out of function this.rgb = color; return; } //Setting other color mode values this._hsv = this.rgbToHsv(color.r, color.g, color.b); this._hsl = this.rgbToHsl(color.r, color.g, color.b); this._hex = this.rgbToHex(color.r, color.g, color.b); this._gradient = null; break; //////////////////////////////////////////////////////// case 'hsv': //Checking for match of previous (HSV) if (color.h !== this.hsv.h && color.s !== this.hsv.s && color.v !== this.hsv.v) { //Setting value and breaking out of function this.hsv = color; return; } //Setting other color mode values this._rgb = this.hsvToRgb(color.h/(2*Math.PI), color.s, color.v); this._hsl = this.rgbToHsl(this.rgb.r, this.rgb.g, this.rgb.b); this._hex = this.rgbToHex(this.rgb.r, this.rgb.g, this.rgb.b); this._gradient = null; break; //////////////////////////////////////////////////////// case 'hsl': //Checking for match of previous (HSV) if (color.h !== this.hsl.h && color.s !== this.hsl.s && color.l !== this.hsl.l) { //Setting value and breaking out of function this.hsl = color; return; } //Setting other color mode values this._rgb = this.hslToRgb(color.h/360, color.s/100, color.l/100); //This is a hack to keep the values of color spectrum the same on limits (B/W) var hsvTemp = this.rgbToHsv(this.rgb.r, this.rgb.g, this.rgb.b); this._hsv = {h: (this._hsl.h/360)*(2*Math.PI), s: hsvTemp.s, v: hsvTemp.v}; this._hex = this.rgbToHex(this.rgb.r, this.rgb.g, this.rgb.b); this._gradient = null; break; //////////////////////////////////////////////////////// case 'hex': switch (color.length) { case 1: this.applyNoColor(false); return; break; case 2: this.applyNoColor(false); return; break; case 3: color = color[0]+color[0]+color[1]+color[1]+color[2]+color[2]; break; case 4: this.applyNoColor(false); return; break; case 5: this.applyNoColor(false); return; break; case 6: //Nothing break; default: this.applyNoColor(false); return; break; } //Checking for match of previous (HEX) if (color !== this.hex) { //Setting value and breaking out of function this.hex = color; return; } //Setting other color mode values this._rgb = this.hexToRgb(color); this._hsv = this.rgbToHsv(this.rgb.r, this.rgb.g, this.rgb.b); this._hsl = this.rgbToHsl(this.rgb.r, this.rgb.g, this.rgb.b); this._gradient = null; break; //////////////////////////////////////////////////////// case 'nocolor': // this._hex = '------'; this._rgb = null; this._hsv = null; this._hsl = null; this._gradient = null; break; //////////////////////////////////////////////////////// default: console.log('ERROR: An error occurred in ColorManager. '+mode+' was not identified in updateColorSelected'); break; //////////////////////////////////////////////////////// } //Dispatching change event this._dispatchChangeEvent(mode, color); } }, //////////////////////////////////////////////////////////////////// //Dispatching "Change" event _dispatchChangeEvent: { //Modes: rgb, hsl, hsv, hex, alpha, input, mode value: function(mode, value) { //Creating custom event to dispatch and include values var colorEvent = document.createEvent("CustomEvent"); // if (value && value.type) { if (value.type === "change") { // colorEvent.initEvent("change", true, true); //Storing the selected color in the history array if ((mode === 'rgb' || mode === 'hsv' || mode === 'hsl' || mode === 'hex' || mode === 'nocolor' || mode === 'gradient') && this.input !== 'chip') { this._addColorHistory(this.input, mode, value, this.alpha); } else if (mode === 'alpha' && this.input !== 'chip') { var c = this.colorHistory[this.input][this.colorHistory[this.input].length-1]; this._addColorHistory(this.input, c.m, c.c, this.alpha); } } else if (value.type === "changing") { // colorEvent.initEvent("changing", true, true); } else { // colorEvent.initEvent("error", true, true); } } else { // colorEvent.initEvent("error", true, true); } //Checking for event to be color, if so, adding color data if (mode === 'rgb' || mode === 'hsv' || mode === 'hsl' || mode === 'hex' || mode === 'alpha' || mode === 'nocolor' || mode === 'gradient') { //Returning color history of input if (this.input === 'stroke') { colorEvent.history = this.colorHistory.stroke; } else if (this.input === 'fill') { colorEvent.history = this.colorHistory.fill; } else { colorEvent.history = null; } } //Also returning other color mode values if (this.input) colorEvent.input = this.input; if (this.alpha) colorEvent.alpha = this.alpha.value; if (this.hsl) colorEvent.hsla = {h: this.hsl.h, s: this.hsl.s, l: this.hsl.l, a: this.alpha.value, css: 'hsla('+this.hsl.h+', '+this.hsl.s+'%, '+this.hsl.l+'%, '+this.alpha.value+')'}; if (this.rgb) { colorEvent.rgba = {r: this.rgb.r, g: this.rgb.g, b: this.rgb.b, a: this.alpha.value, css: 'rgba('+ this.rgb.r +', '+this.rgb.g+', '+this.rgb.b+', '+this.alpha.value+')'}; colorEvent.webGlColor = [this.rgb.r/255, this.rgb.g/255, this.rgb.b/255, this.alpha.value]; } if (this.hex) colorEvent.hex = this.hex; //Standard values that apply to any event colorEvent.value = value; colorEvent.mode = mode; if (value && value.wasSetByCode) { colorEvent.wasSetByCode = value.wasSetByCode; } this.dispatchEvent(colorEvent); } }, //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// // COLOR CONVERTION METHODS (public) //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// //Convert RGB to HEX (RGB = 0 to 255) rgbToHex: { enumerable: true, value: function (r, g, b) { //Coverting color channel to Hex function getHex (c) { var filter = "0123456789ABCDEF"; c = parseInt (c, 10); if (isNaN(c)) return "00"; c = Math.max(0,Math.min(c,255)); return filter.charAt((c-c%16)/16) + filter.charAt(c%16); } //Returning HEX string return (getHex(r)+getHex(g)+getHex(b)); } }, //////////////////////////////////////////////////////////////////// //Convert HEX to RGB (no # symbol) hexToRgb: { enumerable: true, value: function (value) { //Spliting string converting to values var r = parseInt(value.substring(0,2), 16), g = parseInt(value.substring(2,4), 16), b = parseInt(value.substring(4,6), 16); //Checking for valid values if (isNaN(r)) return null; if (isNaN(g)) return null; if (isNaN(b)) return null; return {r: r, g: g, b: b}; } }, //////////////////////////////////////////////////////////////////// //Convert HSV to RGB (HSV = 0 to 1) hsvToRgb: { enumerable: true, value: function (h, s, v) { var r, g, b, i = Math.floor(h * 6), f = h * 6 - i, p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); // switch (i % 6){ case 0: r = v, g = t, b = p; break; case 1: r = q, g = v, b = p; break; case 2: r = p, g = v, b = t; break; case 3: r = p, g = q, b = v; break; case 4: r = t, g = p, b = v; break; case 5: r = v, g = p, b = q; break; } //Returning RGB object return {r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255)}; } }, //////////////////////////////////////////////////////////////////// //Convert RGB to HSV (RGB = 0 to 255) rgbToHsv: { enumerable: true, value: function (r, g, b) { //RGB covertion to percentage r = r/255, g = g/255, b = b/255; var max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, v, d; h = s = v = max; d = max - min; s = max == 0 ? 0 : d / max; //TODO: Check for Hue not to change if S or V = 0 if (max == min) { h = 0; } else { switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } //Returing HSV object return {h: h*Math.PI*2, s: s, v: v}; } }, //////////////////////////////////////////////////////////////////// //Convert RGB TO HSL (RGB = 0 to 255) rgbToHsl: { enumerable: true, value: function (r, g, b) { //RGB covertion to percentage r /= 255, g /= 255, b /= 255; var max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l, d; h = s = l = (max + min) / 2; //TODO: Check for Hue not to change if S or L = 0 if (max == min) { h = s = 0; } else { d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } //Returing HSL object return {h: h*360, s: s*100, l: l*100}; } }, //////////////////////////////////////////////////////////////////// //Convert HSL to RGB (HSL = 0 to 1) hslToRgb: { enumerable: true, value: function (h, s, l) { var r, g, b; if (s == 0) { r = g = b = l; } else { function getRgb (p, q, t) { if(t < 0) t += 1; if(t > 1) t -= 1; if(t < 1/6) return p + (q - p) * 6 * t; if(t < 1/2) return q; if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = getRgb(p, q, h + 1/3); g = getRgb(p, q, h); b = getRgb(p, q, h - 1/3); } //Returning RGB object return {r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255)}; } }, //////////////////////////////////////////////////////////////////// //Returns WebGL color array in [r, g, b, a] format where the values are [0,1] given a color object colorToWebGl: { enumerable: true, value: function (color) { var temp; if (color) { if(color.l !== undefined) { temp = this.hslToRgb(color.h/360, color.s/100, color.l/100); } else if (color.r !== undefined) { temp = color; } else if (color.gradientMode) { // TODO - Need to handle gradients at some point return null; } temp.a = color.a; } //WebGL uses array if (temp) { return [temp.r/255, temp.g/255, temp.b/255, temp.a]; } else { return null; } } }, //////////////////////////////////////////////////////////////////// //Returns a color object given a WebGL color array/object with gradient stops webGlToColor: { enumerable: true, value: function (c) { if(c) { if(c.gradientMode) { // Gradient var i = 0, len, css, stops = c.color, gradient; // Create the CSS string if (c.gradientMode === 'radial') { css = '-webkit-radial-gradient(center, ellipse cover'; } else { css = '-webkit-gradient(linear, left top, right top'; } //Sorting array (must be sorted for radial gradients, at least in Chrome stops.sort(function(a,b){return a.position - b.position}); //Looping through stops in gradient to create CSS len = stops.length; for (i=0; i < len; i++) { //Adding to CSS String if (c.gradientMode === 'radial' && stops[i].value) { css += ', '+stops[i].value.css+' '+stops[i].position+'% '; } else if (stops[i].value){ css += ', color-stop('+stops[i].position+'%,'+stops[i].value.css+')'; } } //Closing the CSS strings css += ')'; gradient = {stops: c.color, mode: c.gradientMode, gradientMode: c.gradientMode, css: css}; return {mode: 'gradient', value: gradient, color: gradient}; } else if(c.length === 4) { // CSS return this.application.ninja.colorController.getColorObjFromCss('rgba(' + c[0]*255 + ', ' + c[1]*255 + ', ' + c[2]*255 + ', ' + c[3] +')'); } } return null; } } //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// });