1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /** @class
  9   Represents a color, and provides methods for manipulating it. Maintains underlying
 10   rgba values, and includes support for colorspace conversions between rgb and hsl.
 11 
 12   For instructions on using SC.Color to color a view, see "SC.Color and SC.View"
 13   below.
 14 
 15   ### Basic Use
 16 
 17   You can create a color from red, green, blue and alpha values, with:
 18 
 19       SC.Color.create({
 20         r: 255,
 21         g: 255,
 22         b: 255,
 23         a: 0.5
 24       });
 25 
 26   All values are optional; the default is black. You can also create a color from any
 27   valid CSS string, with:
 28 
 29       SC.Color.from('rgba(255, 255, 255, 0.5)');
 30 
 31   The best CSS value for the given color in the current browser is available at the
 32   bindable property `cssText`. (This will provide deprecated ARGB values for older
 33   versions of IE; see "Edge Case: Supporting Alpha in IE" below.) (Calling
 34   `SC.Color.from` with an undefined or null value is equivalent to calling it with
 35   `'transparent'`.)
 36 
 37   Once created, you can manipulate a color by settings its a, r, g or b values,
 38   or setting its cssText value (though be careful of invalid values; see "Error
 39   State" below).
 40 
 41   ### Math
 42 
 43   `SC.Color` provides three methods for performing basic math: `sub` for subtraction,
 44   `add` for addition, and `mult` for scaling a number via multiplication.
 45 
 46   Note that these methods do not perform any validation to ensure that the underlying
 47   rgba values stay within the device's gamut (0 to 255 on a normal screen, and 0 to 1
 48   for alpha). For example, adding white to white will result in r, g and b values of
 49   510, a nonsensical value. (The `cssText` property will, however, correctly clamp the
 50   component values, and the color will output #FFFFFF.) This behavior is required
 51   for operations such as interpolating between colors (see "SC.Color and SC.View"
 52   below); it also gives SC.Color more predictable math, where A + B - B = A, even if
 53   the intermediate (A + B) operation results in underlying values outside of the normal
 54   gamut.
 55 
 56   (The class method `SC.Color.clampToDeviceGamut` is used to clamp r, g and b values to the
 57   standard 0 - 255 range. If your application is displaying on a screen with non-standard
 58   ranges, you may need to override this method.)
 59 
 60   ### SC.Color and SC.View
 61 
 62   Hooking up an instance of SC.Color to SC.View#backgroundColor is simple, but like all
 63   uses of backgroundColor, it comes with moderate performance implications, and should
 64   be avoided in cases where regular CSS is sufficient, or where bindings are unduly
 65   expensive, such as in rapidly-scrolling ListViews.
 66 
 67   Use the following code to tie a view's background color to an instance of SC.Color. Note
 68   that you must add backgroundColor to displayProperties in order for your view to update
 69   when the it changes; for performance reasons it is not included by default.
 70 
 71       SC.View.extend({
 72         color: SC.Color.from({'burlywood'}),
 73         backgroundColorBinding: SC.Binding.oneWay('*color.cssText'),
 74         displayProperties: ['backgroundColor']
 75       })
 76 
 77   You can use this to implement a simple cross-fade between two colors. Here's a basic
 78   example (again, note that when possible, pure CSS transitions will be substantially
 79   more performant):
 80 
 81       SC.View.extend({
 82         displayProperties: ['backgroundColor'],
 83         backgroundColor: 'cadetblue',
 84         fromColor: SC.Color.from('cadetblue'),
 85         toColor: SC.Color.from('springgreen'),
 86         click: function() {
 87           // Cancel previous timer.
 88           if (this._timer) this._timer.invalidate();
 89           // Figure out whether we're coming or going.
 90           this._forward = !this._forward;
 91           // Calculate the difference between the two colors.
 92           var fromColor = this._forward ? this.fromColor : this.toColor,
 93               toColor = this._forward ? this.toColor : this.fromColor;
 94           this._deltaColor = toColor.sub(fromColor);
 95           // Set the timer.
 96           this._timer = SC.Timer.schedule({
 97             target: this,
 98             action: '_tick',
 99             interval: 15,
100             repeats: YES,
101             until: Date.now() + 500
102           });
103         },
104         _tick: function() {
105           // Calculate percent of time elapsed.
106           var started = this._timer.startTime,
107               now = Date.now(),
108               until = this._timer.until,
109               pct = (now - started) / (until - started);
110           // Constrain pct.
111           pct = Math.min(pct, 1);
112           // Calculate color.
113           var fromColor = this._forward ? this.fromColor : this.toColor,
114               toColor = this._forward ? this.toColor : this.fromColor,
115               deltaColor = this._deltaColor,
116               currentColor = fromColor.add(deltaColor.mult(pct));
117           // Set.
118           this.set('backgroundColor', currentColor.get('cssText'));
119         }
120       })
121 
122   ### Error State
123 
124   If you call `SC.Color.from` with an invalid value, or set `cssText` to an invalid
125   value, the color object will go into error mode, with `isError` set to YES and
126   `errorValue` containing the invalid value that triggered it. A color in error mode
127   will become transparent, and you will be unable to modify its r, g, b or a values.
128 
129   To reset a color to its last-good values (or, if none, to black), call its `reset`
130   method. Setting `cssText` to a valid value will also recover the color object to a
131   non-error state.
132 
133   ### Edge Case: Supporting Alpha in IE
134 
135   Supporting the alpha channel in older versions of IE requires a little extra work.
136   The bindable `cssText` property will return valid ARGB (e.g. #99FFFFFF) when it
137   detects that it's in an older version of IE which requires it, but unfortunately you
138   can't simply plug that value into `background-color`. The following code will detect
139   this case and provide the correct CSS snippet:
140 
141       // This hack disables ClearType on IE!
142       var color = SC.Color.from('rgba(0, 0, 0, .5)').get('cssText'),
143           css;
144       if (SC.Color.supportsARGB) {
145         var gradient = "progid:DXImageTransform.Microsoft.gradient";
146         css = ("-ms-filter:" + gradient + "(startColorstr=%@1,endColorstr=%@1);" +
147                "filter:" + gradient + "(startColorstr=%@1,endColorstr=%@1)" +
148                "zoom: 1").fmt(color);
149       } else {
150         css = "background-color:" + color;
151       }
152 
153   @extends SC.Object
154   @extends SC.Copyable
155   @extends SC.Error
156  */
157 SC.Color = SC.Object.extend(
158   SC.Copyable,
159   /** @scope SC.Color.prototype */{
160 
161   /**
162     The original color string from which this object was created.
163 
164     For example, if your color was created via `SC.Color.from("burlywood")`,
165     then this would be set to `"burlywood"`.
166 
167     @type String
168     @default null
169    */
170   original: null,
171 
172   /**
173     Whether the color is valid. Attempting to set `cssText` or call `from` with invalid input
174     will put the color into an error state until updated with a valid string or reset.
175 
176     @type Boolean
177     @default NO
178     @see SC.Error
179   */
180   isError: NO,
181 
182   /**
183     In the case of an invalid color, this contains the invalid string that was used to create or
184     update it.
185 
186     @type String
187     @default null
188     @see SC.Error
189   */
190   errorValue: null,
191 
192   /**
193     The alpha channel (opacity).
194     `a` is a decimal value between 0 and 1.
195 
196     @type Number
197     @default 1
198    */
199   a: function (key, value) {
200     // Getter.
201     if (value === undefined) {
202       return this._a;
203     }
204     // Setter.
205     else {
206       if (this.get('isError')) value = this._a;
207       this._a = value;
208       return value;
209     }
210   }.property().cacheable(),
211 
212   /** @private */
213   _a: 1,
214 
215   /**
216     The red value.
217     `r` is an integer between 0 and 255.
218 
219     @type Number
220     @default 0
221    */
222   r: function (key, value) {
223     // Getter.
224     if (value === undefined) {
225       return this._r;
226     }
227     // Setter.
228     else {
229       if (this.get('isError')) value = this._r;
230       this._r = value;
231       return value;
232     }
233   }.property().cacheable(),
234 
235   /** @private */
236   _r: 0,
237 
238   /**
239     The green value.
240     `g` is an integer between 0 and 255.
241 
242     @type Number
243     @default 0
244    */
245   g: function (key, value) {
246     // Getter.
247     if (value === undefined) {
248       return this._g;
249     }
250     // Setter.
251     else {
252       if (this.get('isError')) value = this._g;
253       this._g = value;
254       return value;
255     }
256   }.property().cacheable(),
257 
258   /** @private */
259   _g: 0,
260 
261   /**
262     The blue value.
263     `b` is an integer between 0 and 255.
264 
265     @type Number
266     @default 0
267    */
268   b: function (key, value) {
269     // Getter.
270     if (value === undefined) {
271       return this._b;
272     }
273     // Setter.
274     else {
275       if (this.get('isError')) value = this._b;
276       this._b = value;
277       return value;
278     }
279   }.property().cacheable(),
280 
281   /** @private */
282   _b: 0,
283 
284   /**
285     The current hue of this color.
286     Hue is a float in degrees between 0° and 360°.
287 
288     @field
289     @type Number
290    */
291   hue: function (key, deg) {
292     var clamp = SC.Color.clampToDeviceGamut,
293         hsl = SC.Color.rgbToHsl(clamp(this.get('r')),
294                                 clamp(this.get('g')),
295                                 clamp(this.get('b'))),
296         rgb;
297 
298     if (deg !== undefined) {
299       // Normalize the hue to be between 0 and 360
300       hsl[0] = (deg % 360 + 360) % 360;
301 
302       rgb = SC.Color.hslToRgb(hsl[0], hsl[1], hsl[2]);
303       this.beginPropertyChanges();
304       this.set('r', rgb[0]);
305       this.set('g', rgb[1]);
306       this.set('b', rgb[2]);
307       this.endPropertyChanges();
308     }
309     return hsl[0];
310   }.property('r', 'g', 'b').cacheable(),
311 
312   /**
313     The current saturation of this color.
314     Saturation is a percent between 0 and 1.
315 
316     @field
317     @type Number
318    */
319   saturation: function (key, value) {
320     var clamp = SC.Color.clampToDeviceGamut,
321         hsl = SC.Color.rgbToHsl(clamp(this.get('r')),
322                                 clamp(this.get('g')),
323                                 clamp(this.get('b'))),
324         rgb;
325 
326     if (value !== undefined) {
327       // Clamp the saturation between 0 and 100
328       hsl[1] = SC.Color.clamp(value, 0, 1);
329 
330       rgb = SC.Color.hslToRgb(hsl[0], hsl[1], hsl[2]);
331       this.beginPropertyChanges();
332       this.set('r', rgb[0]);
333       this.set('g', rgb[1]);
334       this.set('b', rgb[2]);
335       this.endPropertyChanges();
336     }
337 
338     return hsl[1];
339   }.property('r', 'g', 'b').cacheable(),
340 
341   /**
342     The current lightness of this color.
343     Saturation is a percent between 0 and 1.
344 
345     @field
346     @type Number
347    */
348   luminosity: function (key, value) {
349     var clamp = SC.Color.clampToDeviceGamut,
350         hsl = SC.Color.rgbToHsl(clamp(this.get('r')),
351                                 clamp(this.get('g')),
352                                 clamp(this.get('b'))),
353         rgb;
354 
355     if (value !== undefined) {
356       // Clamp the lightness between 0 and 1
357       hsl[2] = SC.Color.clamp(value, 0, 1);
358 
359       rgb = SC.Color.hslToRgb(hsl[0], hsl[1], hsl[2]);
360       this.beginPropertyChanges();
361       this.set('r', rgb[0]);
362       this.set('g', rgb[1]);
363       this.set('b', rgb[2]);
364       this.endPropertyChanges();
365     }
366     return hsl[2];
367   }.property('r', 'g', 'b').cacheable(),
368 
369   /**
370     Whether two colors are equivalent.
371     @param {SC.Color} color The color to compare this one to.
372     @returns {Boolean} YES if the two colors are equivalent
373    */
374   isEqualTo: function (color) {
375     return this.get('r') === color.get('r') &&
376            this.get('g') === color.get('g') &&
377            this.get('b') === color.get('b') &&
378            this.get('a') === color.get('a');
379   },
380 
381   /**
382     Returns a CSS string of the color
383     under the #aarrggbb scheme.
384 
385     This color is only valid for IE
386     filters. This is here as a hack
387     to support animating rgba values
388     in older versions of IE by using
389     filter gradients with no change in
390     the actual gradient.
391 
392     @returns {String} The color in the rgba color space as an argb value.
393    */
394   toArgb: function () {
395     var clamp = SC.Color.clampToDeviceGamut;
396 
397     return '#' + [clamp(255 * this.get('a')),
398                   clamp(this.get('r')),
399                   clamp(this.get('g')),
400                   clamp(this.get('b'))].map(function (v) {
401       v = v.toString(16);
402       return v.length === 1 ? '0' + v : v;
403     }).join('');
404   },
405 
406   /**
407     Returns a CSS string of the color
408     under the #rrggbb scheme.
409 
410     @returns {String} The color in the rgb color space as a hex value.
411    */
412   toHex: function () {
413     var clamp = SC.Color.clampToDeviceGamut;
414     return '#' + [clamp(this.get('r')),
415                   clamp(this.get('g')),
416                   clamp(this.get('b'))].map(function (v) {
417       v = v.toString(16);
418       return v.length === 1 ? '0' + v : v;
419     }).join('');
420   },
421 
422   /**
423     Returns a CSS string of the color
424     under the rgb() scheme.
425 
426     @returns {String} The color in the rgb color space.
427    */
428   toRgb: function () {
429     var clamp = SC.Color.clampToDeviceGamut;
430     return 'rgb(' + clamp(this.get('r')) + ','
431                   + clamp(this.get('g')) + ','
432                   + clamp(this.get('b')) + ')';
433   },
434 
435   /**
436     Returns a CSS string of the color
437     under the rgba() scheme.
438 
439     @returns {String} The color in the rgba color space.
440    */
441   toRgba: function () {
442     var clamp = SC.Color.clampToDeviceGamut;
443     return 'rgba(' + clamp(this.get('r')) + ','
444                    + clamp(this.get('g')) + ','
445                    + clamp(this.get('b')) + ','
446                    + this.get('a') + ')';
447   },
448 
449   /**
450     Returns a CSS string of the color
451     under the hsl() scheme.
452 
453     @returns {String} The color in the hsl color space.
454    */
455   toHsl: function () {
456     var round = Math.round;
457     return 'hsl(' + round(this.get('hue')) + ','
458                   + round(this.get('saturation') * 100) + '%,'
459                   + round(this.get('luminosity') * 100) + '%)';
460   },
461 
462   /**
463     Returns a CSS string of the color
464     under the hsla() scheme.
465 
466     @returns {String} The color in the hsla color space.
467    */
468   toHsla: function () {
469     var round = Math.round;
470     return 'hsla(' + round(this.get('hue')) + ','
471                    + round(this.get('saturation') * 100) + '%,'
472                    + round(this.get('luminosity') * 100) + '%,'
473                    + this.get('a') + ')';
474   },
475 
476   /**
477     The CSS string representation that will be
478     best displayed by the browser.
479 
480     @field
481     @type String
482    */
483   cssText: function (key, value) {
484     // Getter.
485     if (value === undefined) {
486       // FAST PATH: Error.
487       if (this.get('isError')) return this.get('errorValue');
488 
489       // FAST PATH: transparent.
490       if (this.get('a') === 0) return 'transparent';
491 
492       var supportsAlphaChannel = SC.Color.supportsRgba ||
493                                  SC.Color.supportsArgb;
494       return (this.get('a') === 1 || !supportsAlphaChannel)
495              ? this.toHex()
496              : SC.Color.supportsRgba
497              ? this.toRgba()
498              : this.toArgb();
499     }
500     // Setter.
501     else {
502       var hash = SC.Color._parse(value);
503       this.beginPropertyChanges();
504       // Error state
505       if (!hash) {
506         // Cache current value for recovery.
507         this._lastValidHash = { r: this._r, g: this._g, b: this._b, a: this._a };
508         this.set('r', 0);
509         this.set('g', 0);
510         this.set('b', 0);
511         this.set('a', 0);
512         this.set('errorValue', value);
513         this.set('isError', YES);
514       }
515       // Happy state
516       else {
517         this.setIfChanged('isError', NO);
518         this.setIfChanged('errorValue', null);
519         this.set('r', hash.r);
520         this.set('g', hash.g);
521         this.set('b', hash.b);
522         this.set('a', hash.a);
523       }
524       this.endPropertyChanges();
525       return value;
526     }
527   }.property('r', 'g', 'b', 'a').cacheable(),
528 
529   /**
530     A read-only property which always returns a valid CSS property. If the color is in
531     an error state, it returns 'transparent'.
532 
533     @field
534     @type String
535    */
536   validCssText: function() {
537     if (this.get('isError')) return 'transparent';
538     else return this.get('cssText');
539   }.property('cssText', 'isError').cacheable(),
540 
541   /**
542     Resets an errored color to its last valid color. If the color has never been valid,
543     it resets to black.
544 
545     @returns {SC.Color} receiver
546    */
547   reset: function() {
548     // Gatekeep: not in error mode.
549     if (!this.get('isError')) return this;
550     // Reset the value to the last valid hash, or default black.
551     var lastValidHash = this._lastValidHash || { r: 0, g: 0, b: 0, a: 1 };
552     this.beginPropertyChanges();
553     this.set('isError', NO);
554     this.set('errorValue', null);
555     this.set('r', lastValidHash.r);
556     this.set('g', lastValidHash.g);
557     this.set('b', lastValidHash.b);
558     this.set('a', lastValidHash.a);
559     this.endPropertyChanges();
560     return this;
561   },
562 
563   /**
564     Returns a clone of this color.
565     This will always a deep clone.
566 
567     @returns {SC.Color} The clone color.
568    */
569   copy: function () {
570     return SC.Color.create({
571       original: this.get('original'),
572       r: this.get('r'),
573       g: this.get('g'),
574       b: this.get('b'),
575       a: this.get('a'),
576       isError: this.get('isError'),
577       errorValue: this.get('errorValue')
578     });
579   },
580 
581   /**
582     Returns a color that's the difference between two colors.
583 
584     Note that the result might not be a valid CSS color.
585 
586     @param {SC.Color} color The color to subtract from this one.
587     @returns {SC.Color} The difference between the two colors.
588    */
589   sub: function (color) {
590     return SC.Color.create({
591       r: this.get('r') - color.get('r'),
592       g: this.get('g') - color.get('g'),
593       b: this.get('b') - color.get('b'),
594       a: this.get('a') - color.get('a'),
595       isError: this.get('isError') || color.get('isError')
596     });
597   },
598 
599   /**
600     Returns a new color that's the addition of two colors.
601 
602     Note that the resulting a, r, g and b values are not clamped to within valid
603     ranges.
604 
605     @param {SC.Color} color The color to add to this one.
606     @returns {SC.Color} The addition of the two colors.
607    */
608   add: function (color) {
609     return SC.Color.create({
610       r: this.get('r') + color.get('r'),
611       g: this.get('g') + color.get('g'),
612       b: this.get('b') + color.get('b'),
613       a: this.get('a') + color.get('a'),
614       isError: this.get('isError')
615     });
616   },
617 
618   /**
619     Returns a color that has it's units uniformly multiplied
620     by a given multiplier.
621 
622     Note that the result might not be a valid CSS color.
623 
624     @param {Number} multipler How much to multiply rgba by.
625     @returns {SC.Color} The adjusted color.
626    */
627   mult: function (multiplier) {
628     var round = Math.round;
629     return SC.Color.create({
630       r: round(this.get('r') * multiplier),
631       g: round(this.get('g') * multiplier),
632       b: round(this.get('b') * multiplier),
633       a: this.get('a') * multiplier,
634       isError: this.get('isError')
635     });
636   }
637 });
638 
639 SC.Color.mixin(
640   /** @scope SC.Color */{
641 
642   /** @private Overrides create to support creation with {a, r, g, b} hash. */
643   create: function() {
644     var vals = {},
645         hasVals = NO,
646         keys = ['a', 'r', 'g', 'b'],
647         args, len,
648         hash, i, k, key;
649 
650 
651     // Fast arguments access.
652     // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly.
653     args = new Array(arguments.length); // SC.A(arguments)
654     len = args.length;
655 
656     // Loop through all arguments. If any of them contain numeric a, r, g or b arguments,
657     // clone the hash and move the value from (e.g.) a to _a.
658     for (i = 0; i < len; i++) {
659       hash = arguments[i];
660       if (SC.typeOf(hash.a) === SC.T_NUMBER
661         || SC.typeOf(hash.r) === SC.T_NUMBER
662         || SC.typeOf(hash.g) === SC.T_NUMBER
663         || SC.typeOf(hash.b) === SC.T_NUMBER
664       ) {
665         hasVals = YES;
666         hash = args[i] = SC.clone(hash);
667         for (k = 0; k < 4; k++) {
668           key = keys[k];
669           if (SC.typeOf(hash[key]) === SC.T_NUMBER) {
670             vals['_' + key] = hash[key];
671             delete hash[key];
672           }
673         }
674       } else {
675         args[i] = hash;
676       }
677     }
678 
679     if (hasVals) args.push(vals);
680     return SC.Object.create.apply(this, args);
681   },
682 
683   /**
684     Whether this browser supports the rgba color model.
685     Check courtesy of Modernizr.
686     @type Boolean
687     @see https://github.com/Modernizr/Modernizr/blob/master/modernizr.js#L552
688    */
689   supportsRgba: (function () {
690     var style = document.getElementsByTagName('script')[0].style,
691         cssText = style.cssText,
692         supported;
693 
694     style.cssText = 'background-color:rgba(5,2,1,.5)';
695     supported = style.backgroundColor.indexOf('rgba') !== -1;
696     style.cssText = cssText;
697     return supported;
698   }()),
699 
700   /**
701     Whether this browser supports the argb color model.
702     @type Boolean
703    */
704   supportsArgb: (function () {
705     var style = document.getElementsByTagName('script')[0].style,
706         cssText = style.cssText,
707         supported;
708 
709     style.cssText = 'filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#55000000", endColorstr="#55000000");';
710     supported = style.backgroundColor.indexOf('#55000000') !== -1;
711     style.cssText = cssText;
712     return supported;
713   }()),
714 
715   /**
716     Used to clamp a value in between a minimum
717     value and a maximum value.
718 
719     @param {Number} value The value to clamp.
720     @param {Number} min The minimum number the value can be.
721     @param {Number} max The maximum number the value can be.
722     @returns {Number} The value clamped between min and max.
723    */
724   clamp: function (value, min, max) {
725     return Math.max(Math.min(value, max), min);
726   },
727 
728   /**
729     Clamps a number, then rounds it to the nearest integer.
730 
731     @param {Number} value The value to clamp.
732     @param {Number} min The minimum number the value can be.
733     @param {Number} max The maximum number the value can be.
734     @returns {Number} The value clamped between min and max as an integer.
735     @see SC.Color.clamp
736    */
737   clampInt: function (value, min, max) {
738     return Math.round(SC.Color.clamp(value, min, max));
739   },
740 
741   /**
742     Clamps a number so it lies in the device gamut.
743     For screens, this an integer between 0 and 255.
744 
745     @param {Number} value The value to clamp
746     @returns {Number} The value clamped to the device gamut.
747    */
748   clampToDeviceGamut: function (value) {
749     return SC.Color.clampInt(value, 0, 255);
750   },
751 
752   /**
753     Returns the RGB for a color defined in
754     the HSV color space.
755 
756     @param {Number} h The hue of the color as a degree between 0° and 360°
757     @param {Number} s The saturation of the color as a percent between 0 and 1.
758     @param {Number} v The value of the color as a percent between 0 and 1.
759     @returns {Number[]} A RGB triple in the form `(r, g, b)`
760       where each of the values are integers between 0 and 255.
761    */
762   hsvToRgb: function (h, s, v) {
763     h /= 360;
764     var r, g, b,
765         i = Math.floor(h * 6),
766         f = h * 6 - i,
767         p = v * (1 - s),
768         q = v * (1 - (s * f)),
769         t = v * (1 - (s * (1 - f))),
770         rgb = [[v, t, p],
771                [q, v, p],
772                [p, v, t],
773                [p, q, v],
774                [t, p, v],
775                [v, p, q]],
776         clamp = SC.Color.clampToDeviceGamut;
777 
778     i = i % 6;
779     r = clamp(rgb[i][0] * 255);
780     g = clamp(rgb[i][1] * 255);
781     b = clamp(rgb[i][2] * 255);
782 
783     return [r, g, b];
784   },
785 
786   /**
787     Returns an RGB color transformed into the
788     HSV colorspace as triple `(h, s, v)`.
789 
790     @param {Number} r The red component as an integer between 0 and 255.
791     @param {Number} g The green component as an integer between 0 and 255.
792     @param {Number} b The blue component as an integer between 0 and 255.
793     @returns {Number[]} A HSV triple in the form `(h, s, v)`
794       where `h` is in degrees (as a float) between 0° and 360° and
795             `s` and `v` are percents between 0 and 1.
796    */
797   rgbToHsv: function (r, g, b) {
798     r /= 255;
799     g /= 255;
800     b /= 255;
801 
802     var max = Math.max(r, g, b),
803         min = Math.min(r, g, b),
804         d = max - min,
805         h, s = max === 0 ? 0 : d / max, v = max;
806 
807     // achromatic
808     if (max === min) {
809       h = 0;
810     } else {
811       switch (max) {
812       case r:
813         h = (g - b) / d + (g < b ? 6 : 0);
814         break;
815       case g:
816         h = (b - r) / d + 2;
817         break;
818       case b:
819         h = (r - g) / d + 4;
820         break;
821       }
822       h /= 6;
823     }
824     h *= 360;
825 
826     return [h, s, v];
827   },
828 
829   /**
830     Returns the RGB for a color defined in
831     the HSL color space.
832 
833     (Notes are taken from the W3 spec, and are
834      written in ABC)
835 
836     @param {Number} h The hue of the color as a degree between 0° and 360°
837     @param {Number} s The saturation of the color as a percent between 0 and 1.
838     @param {Number} l The luminosity of the color as a percent between 0 and 1.
839     @returns {Number[]} A RGB triple in the form `(r, g, b)`
840       where each of the values are integers between 0 and 255.
841     @see http://www.w3.org/TR/css3-color/#hsl-color
842    */
843   hslToRgb: function (h, s, l) {
844     h /= 360;
845 
846   // HOW TO RETURN hsl.to.rgb(h, s, l):
847     var m1, m2, hueToRgb = SC.Color.hueToRgb,
848         clamp = SC.Color.clampToDeviceGamut;
849 
850     // SELECT:
851       // l<=0.5: PUT l*(s+1) IN m2
852       // ELSE: PUT l+s-l*s IN m2
853     m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
854     // PUT l*2-m2 IN m1
855     m1 = l * 2 - m2;
856     // PUT hue.to.rgb(m1, m2, h+1/3) IN r
857     // PUT hue.to.rgb(m1, m2, h    ) IN g
858     // PUT hue.to.rgb(m1, m2, h-1/3) IN b
859     // RETURN (r, g, b)
860     return [clamp(hueToRgb(m1, m2, h + 1/3) * 255),
861             clamp(hueToRgb(m1, m2, h)       * 255),
862             clamp(hueToRgb(m1, m2, h - 1/3) * 255)];
863   },
864 
865   /** @private
866     Returns the RGB value for a given hue.
867    */
868   hueToRgb: function (m1, m2, h) {
869   // HOW TO RETURN hue.to.rgb(m1, m2, h):
870     // IF h<0: PUT h+1 IN h
871     if (h < 0) h++;
872     // IF h>1: PUT h-1 IN h
873     if (h > 1) h--;
874     // IF h*6<1: RETURN m1+(m2-m1)*h*6
875     if (h < 1/6) return m1 + (m2 - m1) * h * 6;
876     // IF h*2<1: RETURN m2
877     if (h < 1/2) return m2;
878     // IF h*3<2: RETURN m1+(m2-m1)*(2/3-h)*6
879     if (h < 2/3) return m1 + (m2 - m1) * (2/3 - h) * 6;
880     // RETURN m1
881     return m1;
882   },
883 
884   /**
885     Returns an RGB color transformed into the
886     HSL colorspace as triple `(h, s, l)`.
887 
888     @param {Number} r The red component as an integer between 0 and 255.
889     @param {Number} g The green component as an integer between 0 and 255.
890     @param {Number} b The blue component as an integer between 0 and 255.
891     @returns {Number[]} A HSL triple in the form `(h, s, l)`
892       where `h` is in degrees (as a float) between 0° and 360° and
893             `s` and `l` are percents between 0 and 1.
894    */
895   rgbToHsl: function (r, g, b) {
896     r /= 255;
897     g /= 255;
898     b /= 255;
899 
900     var max = Math.max(r, g, b),
901         min = Math.min(r, g, b),
902         h, s, l = (max + min) / 2,
903         d = max - min;
904 
905     // achromatic
906     if (max === min) {
907       h = s = 0;
908     } else {
909       s = l > 0.5
910           ? d / (2 - max - min)
911           : d / (max + min);
912 
913       switch (max) {
914       case r:
915         h = (g - b) / d + (g < b ? 6 : 0);
916         break;
917       case g:
918         h = (b - r) / d + 2;
919         break;
920       case b:
921         h = (r - g) / d + 4;
922         break;
923       }
924       h /= 6;
925     }
926     h *= 360;
927 
928     return [h, s, l];
929   },
930 
931   // ..........................................................
932   // Regular expressions for accepted color types
933   //
934   PARSE_RGBA: /^rgba\(\s*([\d]+%?)\s*,\s*([\d]+%?)\s*,\s*([\d]+%?)\s*,\s*([.\d]+)\s*\)$/,
935   PARSE_RGB : /^rgb\(\s*([\d]+%?)\s*,\s*([\d]+%?)\s*,\s*([\d]+%?)\s*\)$/,
936   PARSE_HSLA: /^hsla\(\s*(-?[\d]+)\s*\s*,\s*([\d]+)%\s*,\s*([\d]+)%\s*,\s*([.\d]+)\s*\)$/,
937   PARSE_HSL : /^hsl\(\s*(-?[\d]+)\s*,\s*([\d]+)%\s*,\s*([\d]+)%\s*\)$/,
938   PARSE_HEX : /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/,
939   PARSE_ARGB: /^#[0-9a-fA-F]{8}$/,
940 
941   /**
942     A mapping of anglicized colors to their hexadecimal
943     representation.
944 
945     Computed by running the following code at http://www.w3.org/TR/css3-color
946 
947        var T = {}, color = null,
948            colors = document.querySelectorAll('.colortable')[1].querySelectorAll('.c');
949 
950        for (var i = 0; i < colors.length; i++) {
951          if (i % 4 === 0) {
952            color = colors[i].getAttribute('style').split(':')[1];
953          } else if (i % 4 === 1) {
954            T[color] = colors[i].getAttribute('style').split(':')[1].toUpperCase();
955          }
956        }
957        JSON.stringify(T);
958 
959     @see http://www.w3.org/TR/css3-color/#svg-color
960    */
961   KEYWORDS: {"aliceblue":"#F0F8FF","antiquewhite":"#FAEBD7","aqua":"#00FFFF","aquamarine":"#7FFFD4","azure":"#F0FFFF","beige":"#F5F5DC","bisque":"#FFE4C4","black":"#000000","blanchedalmond":"#FFEBCD","blue":"#0000FF","blueviolet":"#8A2BE2","brown":"#A52A2A","burlywood":"#DEB887","cadetblue":"#5F9EA0","chartreuse":"#7FFF00","chocolate":"#D2691E","coral":"#FF7F50","cornflowerblue":"#6495ED","cornsilk":"#FFF8DC","crimson":"#DC143C","cyan":"#00FFFF","darkblue":"#00008B","darkcyan":"#008B8B","darkgoldenrod":"#B8860B","darkgray":"#A9A9A9","darkgreen":"#006400","darkgrey":"#A9A9A9","darkkhaki":"#BDB76B","darkmagenta":"#8B008B","darkolivegreen":"#556B2F","darkorange":"#FF8C00","darkorchid":"#9932CC","darkred":"#8B0000","darksalmon":"#E9967A","darkseagreen":"#8FBC8F","darkslateblue":"#483D8B","darkslategray":"#2F4F4F","darkslategrey":"#2F4F4F","darkturquoise":"#00CED1","darkviolet":"#9400D3","deeppink":"#FF1493","deepskyblue":"#00BFFF","dimgray":"#696969","dimgrey":"#696969","dodgerblue":"#1E90FF","firebrick":"#B22222","floralwhite":"#FFFAF0","forestgreen":"#228B22","fuchsia":"#FF00FF","gainsboro":"#DCDCDC","ghostwhite":"#F8F8FF","gold":"#FFD700","goldenrod":"#DAA520","gray":"#808080","green":"#008000","greenyellow":"#ADFF2F","grey":"#808080","honeydew":"#F0FFF0","hotpink":"#FF69B4","indianred":"#CD5C5C","indigo":"#4B0082","ivory":"#FFFFF0","khaki":"#F0E68C","lavender":"#E6E6FA","lavenderblush":"#FFF0F5","lawngreen":"#7CFC00","lemonchiffon":"#FFFACD","lightblue":"#ADD8E6","lightcoral":"#F08080","lightcyan":"#E0FFFF","lightgoldenrodyellow":"#FAFAD2","lightgray":"#D3D3D3","lightgreen":"#90EE90","lightgrey":"#D3D3D3","lightpink":"#FFB6C1","lightsalmon":"#FFA07A","lightseagreen":"#20B2AA","lightskyblue":"#87CEFA","lightslategray":"#778899","lightslategrey":"#778899","lightsteelblue":"#B0C4DE","lightyellow":"#FFFFE0","lime":"#00FF00","limegreen":"#32CD32","linen":"#FAF0E6","magenta":"#FF00FF","maroon":"#800000","mediumaquamarine":"#66CDAA","mediumblue":"#0000CD","mediumorchid":"#BA55D3","mediumpurple":"#9370DB","mediumseagreen":"#3CB371","mediumslateblue":"#7B68EE","mediumspringgreen":"#00FA9A","mediumturquoise":"#48D1CC","mediumvioletred":"#C71585","midnightblue":"#191970","mintcream":"#F5FFFA","mistyrose":"#FFE4E1","moccasin":"#FFE4B5","navajowhite":"#FFDEAD","navy":"#000080","oldlace":"#FDF5E6","olive":"#808000","olivedrab":"#6B8E23","orange":"#FFA500","orangered":"#FF4500","orchid":"#DA70D6","palegoldenrod":"#EEE8AA","palegreen":"#98FB98","paleturquoise":"#AFEEEE","palevioletred":"#DB7093","papayawhip":"#FFEFD5","peachpuff":"#FFDAB9","peru":"#CD853F","pink":"#FFC0CB","plum":"#DDA0DD","powderblue":"#B0E0E6","purple":"#800080","red":"#FF0000","rosybrown":"#BC8F8F","royalblue":"#4169E1","saddlebrown":"#8B4513","salmon":"#FA8072","sandybrown":"#F4A460","seagreen":"#2E8B57","seashell":"#FFF5EE","sienna":"#A0522D","silver":"#C0C0C0","skyblue":"#87CEEB","slateblue":"#6A5ACD","slategray":"#708090","slategrey":"#708090","snow":"#FFFAFA","springgreen":"#00FF7F","steelblue":"#4682B4","tan":"#D2B48C","teal":"#008080","thistle":"#D8BFD8","tomato":"#FF6347","turquoise":"#40E0D0","violet":"#EE82EE","wheat":"#F5DEB3","white":"#FFFFFF","whitesmoke":"#F5F5F5","yellow":"#FFFF00","yellowgreen":"#9ACD32"},
962 
963   /**
964     Parses any valid CSS color into a `SC.Color` object. Given invalid input, will return a
965     `SC.Color` object in an error state (with isError: YES).
966 
967     @param {String} color The CSS color value to parse.
968     @returns {SC.Color} The color object representing the color passed in.
969    */
970   from: function (color) {
971     // Fast path: clone another color.
972     if (SC.kindOf(color, SC.Color)) {
973       return color.copy();
974     }
975 
976     // Slow path: string
977     var hash = SC.Color._parse(color),
978         C = SC.Color;
979 
980     // Gatekeep: bad input.
981     if (!hash) {
982       return SC.Color.create({
983         original: color,
984         isError: YES
985       });
986     }
987 
988     return C.create({
989       original: color,
990       r: C.clampInt(hash.r, 0, 255),
991       g: C.clampInt(hash.g, 0, 255),
992       b: C.clampInt(hash.b, 0, 255),
993       a: C.clamp(hash.a, 0, 1)
994     });
995   },
996 
997   /** @private
998     Parses any valid CSS color into r, g, b and a values. Returns null for invalid inputs.
999 
1000     For internal use only. External code should call `SC.Color.from` or `SC.Color#cssText`.
1001 
1002     @param {String} color The CSS color value to parse.
1003     @returns {Hash || null} A hash of r, g, b, and a values.
1004    */
1005   _parse: function (color) {
1006     var C = SC.Color,
1007         oColor = color,
1008         r, g, b, a = 1,
1009         percentOrDeviceGamut = function (value) {
1010           var v = parseInt(value, 10);
1011           return value.slice(-1) === "%"
1012                  ? C.clampInt(v * 2.55, 0, 255)
1013                  : C.clampInt(v, 0, 255);
1014         };
1015 
1016     if (C.KEYWORDS.hasOwnProperty(color)) {
1017       color = C.KEYWORDS[color];
1018     } else if (SC.none(color) || color === '') {
1019       color = 'transparent';
1020     }
1021 
1022     if (C.PARSE_RGB.test(color)) {
1023       color = color.match(C.PARSE_RGB);
1024 
1025       r = percentOrDeviceGamut(color[1]);
1026       g = percentOrDeviceGamut(color[2]);
1027       b = percentOrDeviceGamut(color[3]);
1028 
1029     } else if (C.PARSE_RGBA.test(color)) {
1030       color = color.match(C.PARSE_RGBA);
1031 
1032       r = percentOrDeviceGamut(color[1]);
1033       g = percentOrDeviceGamut(color[2]);
1034       b = percentOrDeviceGamut(color[3]);
1035 
1036       a = parseFloat(color[4], 10);
1037 
1038     } else if (C.PARSE_HEX.test(color)) {
1039       // The three-digit RGB notation (#rgb)
1040       // is converted into six-digit form (#rrggbb)
1041       // by replicating digits, not by adding zeros.
1042       if (color.length === 4) {
1043         color = '#' + color.charAt(1) + color.charAt(1)
1044                     + color.charAt(2) + color.charAt(2)
1045                     + color.charAt(3) + color.charAt(3);
1046       }
1047 
1048       r = parseInt(color.slice(1, 3), 16);
1049       g = parseInt(color.slice(3, 5), 16);
1050       b = parseInt(color.slice(5, 7), 16);
1051 
1052     } else if (C.PARSE_ARGB.test(color)) {
1053       r = parseInt(color.slice(3, 5), 16);
1054       g = parseInt(color.slice(5, 7), 16);
1055       b = parseInt(color.slice(7, 9), 16);
1056 
1057       a = parseInt(color.slice(1, 3), 16) / 255;
1058 
1059     } else if (C.PARSE_HSL.test(color)) {
1060       color = color.match(C.PARSE_HSL);
1061       color = C.hslToRgb(((parseInt(color[1], 10) % 360 + 360) % 360),
1062                          C.clamp(parseInt(color[2], 10) / 100, 0, 1),
1063                          C.clamp(parseInt(color[3], 10) / 100, 0, 1));
1064 
1065       r = color[0];
1066       g = color[1];
1067       b = color[2];
1068 
1069     } else if (C.PARSE_HSLA.test(color)) {
1070       color = color.match(C.PARSE_HSLA);
1071 
1072       a = parseFloat(color[4], 10);
1073 
1074       color = C.hslToRgb(((parseInt(color[1], 10) % 360 + 360) % 360),
1075                          C.clamp(parseInt(color[2], 10) / 100, 0, 1),
1076                          C.clamp(parseInt(color[3], 10) / 100, 0, 1));
1077 
1078       r = color[0];
1079       g = color[1];
1080       b = color[2];
1081 
1082     // See http://www.w3.org/TR/css3-color/#transparent-def
1083     } else if (color === "transparent") {
1084       r = g = b = 0;
1085       a = 0;
1086 
1087     } else {
1088       return null;
1089     }
1090 
1091     return {
1092       r: r,
1093       g: g,
1094       b: b,
1095       a: a
1096     };
1097   }
1098 });
1099