// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ==========================================================================

/** @class

 Represents a color, and provides methods for manipulating it. Maintains underlying
 rgba values, and includes support for colorspace conversions between rgb and hsl.

 For instructions on using SC.Color to color a view, see "SC.Color and SC.View"
 below.

 ### Basic Use

 You can create a color from red, green, blue and alpha values, with:

     SC.Color.create({
       r: 255,
       g: 255,
       b: 255,
       a: 0.5
     });

 All values are optional; the default is black. You can also create a color from any
 valid CSS string, with:

     SC.Color.from('rgba(255, 255, 255, 0.5)');

 The best CSS value for the given color in the current browser is available at the
 bindable property `cssText`. (This will provide deprecated ARGB values for older
 versions of IE; see "Edge Case: Supporting Alpha in IE" below.) (Calling
 `SC.Color.from` with an undefined or null value is equivalent to calling it with
 `'transparent'`.)

 Once created, you can manipulate a color by settings its a, r, g or b values,
 or setting its cssText value (though be careful of invalid values; see "Error
 State" below).

 ### Math

 `SC.Color` provides three methods for performing basic math: `sub` for subtraction,
 `add` for addition, and `mult` for scaling a number via multiplication.

 Note that these methods do not perform any validation to ensure that the underlying
 rgba values stay within the device's gamut (0 to 255 on a normal screen, and 0 to 1
 for alpha). For example, adding white to white will result in r, g and b values of
 510, a nonsensical value. (The `cssText` property will, however, correctly clamp the
 component values, and the color will output #FFFFFF.) This behavior is required
 for operations such as interpolating between colors (see "SC.Color and SC.View"
 below); it also gives SC.Color more predictable math, where A + B - B = A, even if
 the intermediate (A + B) operation results in underlying values outside of the normal
 gamut.

 (The class method `SC.Color.clampToDeviceGamut` is used to clamp r, g and b values to the
 standard 0 - 255 range. If your application is displaying on a screen with non-standard
 ranges, you may need to override this method.)

 ### SC.Color and SC.View

 Hooking up an instance of SC.Color to SC.View#backgroundColor is simple, but like all
 uses of backgroundColor, it comes with moderate performance implications, and should
 be avoided in cases where regular CSS is sufficient, or where bindings are unduly
 expensive, such as in rapidly-scrolling ListViews.

 Use the following code to tie a view's background color to an instance of SC.Color. Note
 that you must add backgroundColor to displayProperties in order for your view to update
 when the it changes; for performance reasons it is not included by default.

     SC.View.extend({
       color: SC.Color.from({'burlywood'}),
       backgroundColorBinding: SC.Binding.oneWay('*color.cssText'),
       displayProperties: ['backgroundColor']
     })

 You can use this to implement a simple cross-fade between two colors. Here's a basic
 example (again, note that when possible, pure CSS transitions will be substantially
 more performant):

     SC.View.extend({
       displayProperties: ['backgroundColor'],
       backgroundColor: 'cadetblue',
       fromColor: SC.Color.from('cadetblue'),
       toColor: SC.Color.from('springgreen'),
       click: function() {
         // Cancel previous timer.
         if (this._timer) this._timer.invalidate();
         // Figure out whether we're coming or going.
         this._forward = !this._forward;
         // Calculate the difference between the two colors.
         var fromColor = this._forward ? this.fromColor : this.toColor,
             toColor = this._forward ? this.toColor : this.fromColor;
         this._deltaColor = toColor.sub(fromColor);
         // Set the timer.
         this._timer = SC.Timer.schedule({
           target: this,
           action: '_tick',
           interval: 15,
           repeats: YES,
           until: Date.now() + 500
         });
       },
       _tick: function() {
         // Calculate percent of time elapsed.
         var started = this._timer.startTime,
             now = Date.now(),
             until = this._timer.until,
             pct = (now - started) / (until - started);
         // Constrain pct.
         pct = Math.min(pct, 1);
         // Calculate color.
         var fromColor = this._forward ? this.fromColor : this.toColor,
             toColor = this._forward ? this.toColor : this.fromColor,
             deltaColor = this._deltaColor,
             currentColor = fromColor.add(deltaColor.mult(pct));
         // Set.
         this.set('backgroundColor', currentColor.get('cssText'));
       }
     })

 ### Error State

 If you call `SC.Color.from` with an invalid value, or set `cssText` to an invalid
 value, the color object will go into error mode, with `isError` set to YES and
 `errorValue` containing the invalid value that triggered it. A color in error mode
 will become transparent, and you will be unable to modify its r, g, b or a values.

 To reset a color to its last-good values (or, if none, to black), call its `reset`
 method. Setting `cssText` to a valid value will also recover the color object to a
 non-error state.

 ### Edge Case: Supporting Alpha in IE

 Supporting the alpha channel in older versions of IE requires a little extra work.
 The bindable `cssText` property will return valid ARGB (e.g. #99FFFFFF) when it
 detects that it's in an older version of IE which requires it, but unfortunately you
 can't simply plug that value into `background-color`. The following code will detect
 this case and provide the correct CSS snippet:

     // This hack disables ClearType on IE!
     var color = SC.Color.from('rgba(0, 0, 0, .5)').get('cssText'),
         css;
     if (SC.Color.supportsARGB) {
       var gradient = "progid:DXImageTransform.Microsoft.gradient";
       css = ("-ms-filter:" + gradient + "(startColorstr=%@1,endColorstr=%@1);" +
              "filter:" + gradient + "(startColorstr=%@1,endColorstr=%@1)" +
              "zoom: 1").fmt(color);
     } else {
       css = "background-color:" + color;
     }

 @extends SC.Object
 @extends SC.Copyable
 @extends SC.Error
*/

SC.Color = SC.Object.extend(

SC.Copyable,
/** @scope SC.Color.prototype */{

/**
  The original color string from which this object was created.

  For example, if your color was created via `SC.Color.from("burlywood")`,
  then this would be set to `"burlywood"`.

  @type String
  @default null
 */
original: null,

/**
  Whether the color is valid. Attempting to set `cssText` or call `from` with invalid input
  will put the color into an error state until updated with a valid string or reset.

  @type Boolean
  @default NO
  @see SC.Error
*/
isError: NO,

/**
  In the case of an invalid color, this contains the invalid string that was used to create or
  update it.

  @type String
  @default null
  @see SC.Error
*/
errorValue: null,

/**
  The alpha channel (opacity).
  `a` is a decimal value between 0 and 1.

  @type Number
  @default 1
 */
a: function (key, value) {
  // Getter.
  if (value === undefined) {
    return this._a;
  }
  // Setter.
  else {
    if (this.get('isError')) value = this._a;
    this._a = value;
    return value;
  }
}.property().cacheable(),

/** @private */
_a: 1,

/**
  The red value.
  `r` is an integer between 0 and 255.

  @type Number
  @default 0
 */
r: function (key, value) {
  // Getter.
  if (value === undefined) {
    return this._r;
  }
  // Setter.
  else {
    if (this.get('isError')) value = this._r;
    this._r = value;
    return value;
  }
}.property().cacheable(),

/** @private */
_r: 0,

/**
  The green value.
  `g` is an integer between 0 and 255.

  @type Number
  @default 0
 */
g: function (key, value) {
  // Getter.
  if (value === undefined) {
    return this._g;
  }
  // Setter.
  else {
    if (this.get('isError')) value = this._g;
    this._g = value;
    return value;
  }
}.property().cacheable(),

/** @private */
_g: 0,

/**
  The blue value.
  `b` is an integer between 0 and 255.

  @type Number
  @default 0
 */
b: function (key, value) {
  // Getter.
  if (value === undefined) {
    return this._b;
  }
  // Setter.
  else {
    if (this.get('isError')) value = this._b;
    this._b = value;
    return value;
  }
}.property().cacheable(),

/** @private */
_b: 0,

/**
  The current hue of this color.
  Hue is a float in degrees between 0° and 360°.

  @field
  @type Number
 */
hue: function (key, deg) {
  var clamp = SC.Color.clampToDeviceGamut,
      hsl = SC.Color.rgbToHsl(clamp(this.get('r')),
                              clamp(this.get('g')),
                              clamp(this.get('b'))),
      rgb;

  if (deg !== undefined) {
    // Normalize the hue to be between 0 and 360
    hsl[0] = (deg % 360 + 360) % 360;

    rgb = SC.Color.hslToRgb(hsl[0], hsl[1], hsl[2]);
    this.beginPropertyChanges();
    this.set('r', rgb[0]);
    this.set('g', rgb[1]);
    this.set('b', rgb[2]);
    this.endPropertyChanges();
  }
  return hsl[0];
}.property('r', 'g', 'b').cacheable(),

/**
  The current saturation of this color.
  Saturation is a percent between 0 and 1.

  @field
  @type Number
 */
saturation: function (key, value) {
  var clamp = SC.Color.clampToDeviceGamut,
      hsl = SC.Color.rgbToHsl(clamp(this.get('r')),
                              clamp(this.get('g')),
                              clamp(this.get('b'))),
      rgb;

  if (value !== undefined) {
    // Clamp the saturation between 0 and 100
    hsl[1] = SC.Color.clamp(value, 0, 1);

    rgb = SC.Color.hslToRgb(hsl[0], hsl[1], hsl[2]);
    this.beginPropertyChanges();
    this.set('r', rgb[0]);
    this.set('g', rgb[1]);
    this.set('b', rgb[2]);
    this.endPropertyChanges();
  }

  return hsl[1];
}.property('r', 'g', 'b').cacheable(),

/**
  The current lightness of this color.
  Saturation is a percent between 0 and 1.

  @field
  @type Number
 */
luminosity: function (key, value) {
  var clamp = SC.Color.clampToDeviceGamut,
      hsl = SC.Color.rgbToHsl(clamp(this.get('r')),
                              clamp(this.get('g')),
                              clamp(this.get('b'))),
      rgb;

  if (value !== undefined) {
    // Clamp the lightness between 0 and 1
    hsl[2] = SC.Color.clamp(value, 0, 1);

    rgb = SC.Color.hslToRgb(hsl[0], hsl[1], hsl[2]);
    this.beginPropertyChanges();
    this.set('r', rgb[0]);
    this.set('g', rgb[1]);
    this.set('b', rgb[2]);
    this.endPropertyChanges();
  }
  return hsl[2];
}.property('r', 'g', 'b').cacheable(),

/**
  Whether two colors are equivalent.
  @param {SC.Color} color The color to compare this one to.
  @returns {Boolean} YES if the two colors are equivalent
 */
isEqualTo: function (color) {
  return this.get('r') === color.get('r') &&
         this.get('g') === color.get('g') &&
         this.get('b') === color.get('b') &&
         this.get('a') === color.get('a');
},

/**
  Returns a CSS string of the color
  under the #aarrggbb scheme.

  This color is only valid for IE
  filters. This is here as a hack
  to support animating rgba values
  in older versions of IE by using
  filter gradients with no change in
  the actual gradient.

  @returns {String} The color in the rgba color space as an argb value.
 */
toArgb: function () {
  var clamp = SC.Color.clampToDeviceGamut;

  return '#' + [clamp(255 * this.get('a')),
                clamp(this.get('r')),
                clamp(this.get('g')),
                clamp(this.get('b'))].map(function (v) {
    v = v.toString(16);
    return v.length === 1 ? '0' + v : v;
  }).join('');
},

/**
  Returns a CSS string of the color
  under the #rrggbb scheme.

  @returns {String} The color in the rgb color space as a hex value.
 */
toHex: function () {
  var clamp = SC.Color.clampToDeviceGamut;
  return '#' + [clamp(this.get('r')),
                clamp(this.get('g')),
                clamp(this.get('b'))].map(function (v) {
    v = v.toString(16);
    return v.length === 1 ? '0' + v : v;
  }).join('');
},

/**
  Returns a CSS string of the color
  under the rgb() scheme.

  @returns {String} The color in the rgb color space.
 */
toRgb: function () {
  var clamp = SC.Color.clampToDeviceGamut;
  return 'rgb(' + clamp(this.get('r')) + ','
                + clamp(this.get('g')) + ','
                + clamp(this.get('b')) + ')';
},

/**
  Returns a CSS string of the color
  under the rgba() scheme.

  @returns {String} The color in the rgba color space.
 */
toRgba: function () {
  var clamp = SC.Color.clampToDeviceGamut;
  return 'rgba(' + clamp(this.get('r')) + ','
                 + clamp(this.get('g')) + ','
                 + clamp(this.get('b')) + ','
                 + this.get('a') + ')';
},

/**
  Returns a CSS string of the color
  under the hsl() scheme.

  @returns {String} The color in the hsl color space.
 */
toHsl: function () {
  var round = Math.round;
  return 'hsl(' + round(this.get('hue')) + ','
                + round(this.get('saturation') * 100) + '%,'
                + round(this.get('luminosity') * 100) + '%)';
},

/**
  Returns a CSS string of the color
  under the hsla() scheme.

  @returns {String} The color in the hsla color space.
 */
toHsla: function () {
  var round = Math.round;
  return 'hsla(' + round(this.get('hue')) + ','
                 + round(this.get('saturation') * 100) + '%,'
                 + round(this.get('luminosity') * 100) + '%,'
                 + this.get('a') + ')';
},

/**
  The CSS string representation that will be
  best displayed by the browser.

  @field
  @type String
 */
cssText: function (key, value) {
  // Getter.
  if (value === undefined) {
    // FAST PATH: Error.
    if (this.get('isError')) return this.get('errorValue');

    // FAST PATH: transparent.
    if (this.get('a') === 0) return 'transparent';

    var supportsAlphaChannel = SC.Color.supportsRgba ||
                               SC.Color.supportsArgb;
    return (this.get('a') === 1 || !supportsAlphaChannel)
           ? this.toHex()
           : SC.Color.supportsRgba
           ? this.toRgba()
           : this.toArgb();
  }
  // Setter.
  else {
    var hash = SC.Color._parse(value);
    this.beginPropertyChanges();
    // Error state
    if (!hash) {
      // Cache current value for recovery.
      this._lastValidHash = { r: this._r, g: this._g, b: this._b, a: this._a };
      this.set('r', 0);
      this.set('g', 0);
      this.set('b', 0);
      this.set('a', 0);
      this.set('errorValue', value);
      this.set('isError', YES);
    }
    // Happy state
    else {
      this.setIfChanged('isError', NO);
      this.setIfChanged('errorValue', null);
      this.set('r', hash.r);
      this.set('g', hash.g);
      this.set('b', hash.b);
      this.set('a', hash.a);
    }
    this.endPropertyChanges();
    return value;
  }
}.property('r', 'g', 'b', 'a').cacheable(),

/**
  A read-only property which always returns a valid CSS property. If the color is in
  an error state, it returns 'transparent'.

  @field
  @type String
 */
validCssText: function() {
  if (this.get('isError')) return 'transparent';
  else return this.get('cssText');
}.property('cssText', 'isError').cacheable(),

/**
  Resets an errored color to its last valid color. If the color has never been valid,
  it resets to black.

  @returns {SC.Color} receiver
 */
reset: function() {
  // Gatekeep: not in error mode.
  if (!this.get('isError')) return this;
  // Reset the value to the last valid hash, or default black.
  var lastValidHash = this._lastValidHash || { r: 0, g: 0, b: 0, a: 1 };
  this.beginPropertyChanges();
  this.set('isError', NO);
  this.set('errorValue', null);
  this.set('r', lastValidHash.r);
  this.set('g', lastValidHash.g);
  this.set('b', lastValidHash.b);
  this.set('a', lastValidHash.a);
  this.endPropertyChanges();
  return this;
},

/**
  Returns a clone of this color.
  This will always a deep clone.

  @returns {SC.Color} The clone color.
 */
copy: function () {
  return SC.Color.create({
    original: this.get('original'),
    r: this.get('r'),
    g: this.get('g'),
    b: this.get('b'),
    a: this.get('a'),
    isError: this.get('isError'),
    errorValue: this.get('errorValue')
  });
},

/**
  Returns a color that's the difference between two colors.

  Note that the result might not be a valid CSS color.

  @param {SC.Color} color The color to subtract from this one.
  @returns {SC.Color} The difference between the two colors.
 */
sub: function (color) {
  return SC.Color.create({
    r: this.get('r') - color.get('r'),
    g: this.get('g') - color.get('g'),
    b: this.get('b') - color.get('b'),
    a: this.get('a') - color.get('a'),
    isError: this.get('isError') || color.get('isError')
  });
},

/**
  Returns a new color that's the addition of two colors.

  Note that the resulting a, r, g and b values are not clamped to within valid
  ranges.

  @param {SC.Color} color The color to add to this one.
  @returns {SC.Color} The addition of the two colors.
 */
add: function (color) {
  return SC.Color.create({
    r: this.get('r') + color.get('r'),
    g: this.get('g') + color.get('g'),
    b: this.get('b') + color.get('b'),
    a: this.get('a') + color.get('a'),
    isError: this.get('isError')
  });
},

/**
  Returns a color that has it's units uniformly multiplied
  by a given multiplier.

  Note that the result might not be a valid CSS color.

  @param {Number} multipler How much to multiply rgba by.
  @returns {SC.Color} The adjusted color.
 */
mult: function (multiplier) {
  var round = Math.round;
  return SC.Color.create({
    r: round(this.get('r') * multiplier),
    g: round(this.get('g') * multiplier),
    b: round(this.get('b') * multiplier),
    a: this.get('a') * multiplier,
    isError: this.get('isError')
  });
}

});

SC.Color.mixin(

/** @scope SC.Color */{

/** @private Overrides create to support creation with {a, r, g, b} hash. */
create: function() {
  var vals = {},
      hasVals = NO,
      keys = ['a', 'r', 'g', 'b'],
      args, len,
      hash, i, k, key;

  // Fast arguments access.
  // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly.
  args = new Array(arguments.length); // SC.A(arguments)
  len = args.length;

  // Loop through all arguments. If any of them contain numeric a, r, g or b arguments,
  // clone the hash and move the value from (e.g.) a to _a.
  for (i = 0; i < len; i++) {
    hash = arguments[i];
    if (SC.typeOf(hash.a) === SC.T_NUMBER
      || SC.typeOf(hash.r) === SC.T_NUMBER
      || SC.typeOf(hash.g) === SC.T_NUMBER
      || SC.typeOf(hash.b) === SC.T_NUMBER
    ) {
      hasVals = YES;
      hash = args[i] = SC.clone(hash);
      for (k = 0; k < 4; k++) {
        key = keys[k];
        if (SC.typeOf(hash[key]) === SC.T_NUMBER) {
          vals['_' + key] = hash[key];
          delete hash[key];
        }
      }
    } else {
      args[i] = hash;
    }
  }

  if (hasVals) args.push(vals);
  return SC.Object.create.apply(this, args);
},

/**
  Whether this browser supports the rgba color model.
  Check courtesy of Modernizr.
  @type Boolean
  @see https://github.com/Modernizr/Modernizr/blob/master/modernizr.js#L552
 */
supportsRgba: (function () {
  var style = document.getElementsByTagName('script')[0].style,
      cssText = style.cssText,
      supported;

  style.cssText = 'background-color:rgba(5,2,1,.5)';
  supported = style.backgroundColor.indexOf('rgba') !== -1;
  style.cssText = cssText;
  return supported;
}()),

/**
  Whether this browser supports the argb color model.
  @type Boolean
 */
supportsArgb: (function () {
  var style = document.getElementsByTagName('script')[0].style,
      cssText = style.cssText,
      supported;

  style.cssText = 'filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#55000000", endColorstr="#55000000");';
  supported = style.backgroundColor.indexOf('#55000000') !== -1;
  style.cssText = cssText;
  return supported;
}()),

/**
  Used to clamp a value in between a minimum
  value and a maximum value.

  @param {Number} value The value to clamp.
  @param {Number} min The minimum number the value can be.
  @param {Number} max The maximum number the value can be.
  @returns {Number} The value clamped between min and max.
 */
clamp: function (value, min, max) {
  return Math.max(Math.min(value, max), min);
},

/**
  Clamps a number, then rounds it to the nearest integer.

  @param {Number} value The value to clamp.
  @param {Number} min The minimum number the value can be.
  @param {Number} max The maximum number the value can be.
  @returns {Number} The value clamped between min and max as an integer.
  @see SC.Color.clamp
 */
clampInt: function (value, min, max) {
  return Math.round(SC.Color.clamp(value, min, max));
},

/**
  Clamps a number so it lies in the device gamut.
  For screens, this an integer between 0 and 255.

  @param {Number} value The value to clamp
  @returns {Number} The value clamped to the device gamut.
 */
clampToDeviceGamut: function (value) {
  return SC.Color.clampInt(value, 0, 255);
},

/**
  Returns the RGB for a color defined in
  the HSV color space.

  @param {Number} h The hue of the color as a degree between 0° and 360°
  @param {Number} s The saturation of the color as a percent between 0 and 1.
  @param {Number} v The value of the color as a percent between 0 and 1.
  @returns {Number[]} A RGB triple in the form `(r, g, b)`
    where each of the values are integers between 0 and 255.
 */
hsvToRgb: function (h, s, v) {
  h /= 360;
  var r, g, b,
      i = Math.floor(h * 6),
      f = h * 6 - i,
      p = v * (1 - s),
      q = v * (1 - (s * f)),
      t = v * (1 - (s * (1 - f))),
      rgb = [[v, t, p],
             [q, v, p],
             [p, v, t],
             [p, q, v],
             [t, p, v],
             [v, p, q]],
      clamp = SC.Color.clampToDeviceGamut;

  i = i % 6;
  r = clamp(rgb[i][0] * 255);
  g = clamp(rgb[i][1] * 255);
  b = clamp(rgb[i][2] * 255);

  return [r, g, b];
},

/**
  Returns an RGB color transformed into the
  HSV colorspace as triple `(h, s, v)`.

  @param {Number} r The red component as an integer between 0 and 255.
  @param {Number} g The green component as an integer between 0 and 255.
  @param {Number} b The blue component as an integer between 0 and 255.
  @returns {Number[]} A HSV triple in the form `(h, s, v)`
    where `h` is in degrees (as a float) between 0° and 360° and
          `s` and `v` are percents between 0 and 1.
 */
rgbToHsv: function (r, g, b) {
  r /= 255;
  g /= 255;
  b /= 255;

  var max = Math.max(r, g, b),
      min = Math.min(r, g, b),
      d = max - min,
      h, s = max === 0 ? 0 : d / max, v = max;

  // achromatic
  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;
  }
  h *= 360;

  return [h, s, v];
},

/**
  Returns the RGB for a color defined in
  the HSL color space.

  (Notes are taken from the W3 spec, and are
   written in ABC)

  @param {Number} h The hue of the color as a degree between 0° and 360°
  @param {Number} s The saturation of the color as a percent between 0 and 1.
  @param {Number} l The luminosity of the color as a percent between 0 and 1.
  @returns {Number[]} A RGB triple in the form `(r, g, b)`
    where each of the values are integers between 0 and 255.
  @see http://www.w3.org/TR/css3-color/#hsl-color
 */
hslToRgb: function (h, s, l) {
  h /= 360;

// HOW TO RETURN hsl.to.rgb(h, s, l):
  var m1, m2, hueToRgb = SC.Color.hueToRgb,
      clamp = SC.Color.clampToDeviceGamut;

  // SELECT:
    // l<=0.5: PUT l*(s+1) IN m2
    // ELSE: PUT l+s-l*s IN m2
  m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
  // PUT l*2-m2 IN m1
  m1 = l * 2 - m2;
  // PUT hue.to.rgb(m1, m2, h+1/3) IN r
  // PUT hue.to.rgb(m1, m2, h    ) IN g
  // PUT hue.to.rgb(m1, m2, h-1/3) IN b
  // RETURN (r, g, b)
  return [clamp(hueToRgb(m1, m2, h + 1/3) * 255),
          clamp(hueToRgb(m1, m2, h)       * 255),
          clamp(hueToRgb(m1, m2, h - 1/3) * 255)];
},

/** @private
  Returns the RGB value for a given hue.
 */
hueToRgb: function (m1, m2, h) {
// HOW TO RETURN hue.to.rgb(m1, m2, h):
  // IF h<0: PUT h+1 IN h
  if (h < 0) h++;
  // IF h>1: PUT h-1 IN h
  if (h > 1) h--;
  // IF h*6<1: RETURN m1+(m2-m1)*h*6
  if (h < 1/6) return m1 + (m2 - m1) * h * 6;
  // IF h*2<1: RETURN m2
  if (h < 1/2) return m2;
  // IF h*3<2: RETURN m1+(m2-m1)*(2/3-h)*6
  if (h < 2/3) return m1 + (m2 - m1) * (2/3 - h) * 6;
  // RETURN m1
  return m1;
},

/**
  Returns an RGB color transformed into the
  HSL colorspace as triple `(h, s, l)`.

  @param {Number} r The red component as an integer between 0 and 255.
  @param {Number} g The green component as an integer between 0 and 255.
  @param {Number} b The blue component as an integer between 0 and 255.
  @returns {Number[]} A HSL triple in the form `(h, s, l)`
    where `h` is in degrees (as a float) between 0° and 360° and
          `s` and `l` are percents between 0 and 1.
 */
rgbToHsl: function (r, g, b) {
  r /= 255;
  g /= 255;
  b /= 255;

  var max = Math.max(r, g, b),
      min = Math.min(r, g, b),
      h, s, l = (max + min) / 2,
      d = max - min;

  // achromatic
  if (max === min) {
    h = s = 0;
  } else {
    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;
  }
  h *= 360;

  return [h, s, l];
},

// ..........................................................
// Regular expressions for accepted color types
//
PARSE_RGBA: /^rgba\(\s*([\d]+%?)\s*,\s*([\d]+%?)\s*,\s*([\d]+%?)\s*,\s*([.\d]+)\s*\)$/,
PARSE_RGB : /^rgb\(\s*([\d]+%?)\s*,\s*([\d]+%?)\s*,\s*([\d]+%?)\s*\)$/,
PARSE_HSLA: /^hsla\(\s*(-?[\d]+)\s*\s*,\s*([\d]+)%\s*,\s*([\d]+)%\s*,\s*([.\d]+)\s*\)$/,
PARSE_HSL : /^hsl\(\s*(-?[\d]+)\s*,\s*([\d]+)%\s*,\s*([\d]+)%\s*\)$/,
PARSE_HEX : /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/,
PARSE_ARGB: /^#[0-9a-fA-F]{8}$/,

/**
  A mapping of anglicized colors to their hexadecimal
  representation.

  Computed by running the following code at http://www.w3.org/TR/css3-color

     var T = {}, color = null,
         colors = document.querySelectorAll('.colortable')[1].querySelectorAll('.c');

     for (var i = 0; i < colors.length; i++) {
       if (i % 4 === 0) {
         color = colors[i].getAttribute('style').split(':')[1];
       } else if (i % 4 === 1) {
         T[color] = colors[i].getAttribute('style').split(':')[1].toUpperCase();
       }
     }
     JSON.stringify(T);

  @see http://www.w3.org/TR/css3-color/#svg-color
 */
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"},

/**
  Parses any valid CSS color into a `SC.Color` object. Given invalid input, will return a
  `SC.Color` object in an error state (with isError: YES).

  @param {String} color The CSS color value to parse.
  @returns {SC.Color} The color object representing the color passed in.
 */
from: function (color) {
  // Fast path: clone another color.
  if (SC.kindOf(color, SC.Color)) {
    return color.copy();
  }

  // Slow path: string
  var hash = SC.Color._parse(color),
      C = SC.Color;

  // Gatekeep: bad input.
  if (!hash) {
    return SC.Color.create({
      original: color,
      isError: YES
    });
  }

  return C.create({
    original: color,
    r: C.clampInt(hash.r, 0, 255),
    g: C.clampInt(hash.g, 0, 255),
    b: C.clampInt(hash.b, 0, 255),
    a: C.clamp(hash.a, 0, 1)
  });
},

/** @private
  Parses any valid CSS color into r, g, b and a values. Returns null for invalid inputs.

  For internal use only. External code should call `SC.Color.from` or `SC.Color#cssText`.

  @param {String} color The CSS color value to parse.
  @returns {Hash || null} A hash of r, g, b, and a values.
 */
_parse: function (color) {
  var C = SC.Color,
      oColor = color,
      r, g, b, a = 1,
      percentOrDeviceGamut = function (value) {
        var v = parseInt(value, 10);
        return value.slice(-1) === "%"
               ? C.clampInt(v * 2.55, 0, 255)
               : C.clampInt(v, 0, 255);
      };

  if (C.KEYWORDS.hasOwnProperty(color)) {
    color = C.KEYWORDS[color];
  } else if (SC.none(color) || color === '') {
    color = 'transparent';
  }

  if (C.PARSE_RGB.test(color)) {
    color = color.match(C.PARSE_RGB);

    r = percentOrDeviceGamut(color[1]);
    g = percentOrDeviceGamut(color[2]);
    b = percentOrDeviceGamut(color[3]);

  } else if (C.PARSE_RGBA.test(color)) {
    color = color.match(C.PARSE_RGBA);

    r = percentOrDeviceGamut(color[1]);
    g = percentOrDeviceGamut(color[2]);
    b = percentOrDeviceGamut(color[3]);

    a = parseFloat(color[4], 10);

  } else if (C.PARSE_HEX.test(color)) {
    // The three-digit RGB notation (#rgb)
    // is converted into six-digit form (#rrggbb)
    // by replicating digits, not by adding zeros.
    if (color.length === 4) {
      color = '#' + color.charAt(1) + color.charAt(1)
                  + color.charAt(2) + color.charAt(2)
                  + color.charAt(3) + color.charAt(3);
    }

    r = parseInt(color.slice(1, 3), 16);
    g = parseInt(color.slice(3, 5), 16);
    b = parseInt(color.slice(5, 7), 16);

  } else if (C.PARSE_ARGB.test(color)) {
    r = parseInt(color.slice(3, 5), 16);
    g = parseInt(color.slice(5, 7), 16);
    b = parseInt(color.slice(7, 9), 16);

    a = parseInt(color.slice(1, 3), 16) / 255;

  } else if (C.PARSE_HSL.test(color)) {
    color = color.match(C.PARSE_HSL);
    color = C.hslToRgb(((parseInt(color[1], 10) % 360 + 360) % 360),
                       C.clamp(parseInt(color[2], 10) / 100, 0, 1),
                       C.clamp(parseInt(color[3], 10) / 100, 0, 1));

    r = color[0];
    g = color[1];
    b = color[2];

  } else if (C.PARSE_HSLA.test(color)) {
    color = color.match(C.PARSE_HSLA);

    a = parseFloat(color[4], 10);

    color = C.hslToRgb(((parseInt(color[1], 10) % 360 + 360) % 360),
                       C.clamp(parseInt(color[2], 10) / 100, 0, 1),
                       C.clamp(parseInt(color[3], 10) / 100, 0, 1));

    r = color[0];
    g = color[1];
    b = color[2];

  // See http://www.w3.org/TR/css3-color/#transparent-def
  } else if (color === "transparent") {
    r = g = b = 0;
    a = 0;

  } else {
    return null;
  }

  return {
    r: r,
    g: g,
    b: b,
    a: a
  };
}

});