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