[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/lib/plugins/styling/ -> iris.js (source)

   1  /*! Iris Color Picker - v1.0.7 - 2014-11-28
   2  * https://github.com/Automattic/Iris
   3  * Copyright (c) 2014 Matt Wiebe; Licensed GPLv2 */
   4  (function( $, undef ){
   5      var _html, nonGradientIE, gradientType, vendorPrefixes, _css, Iris, UA, isIE, IEVersion;
   6  
   7      _html = '<div class="iris-picker"><div class="iris-picker-inner"><div class="iris-square"><a class="iris-square-value" href="#"><span class="iris-square-handle ui-slider-handle"></span></a><div class="iris-square-inner iris-square-horiz"></div><div class="iris-square-inner iris-square-vert"></div></div><div class="iris-slider iris-strip"><div class="iris-slider-offset"></div></div></div></div>';
   8      _css = '.iris-picker{display:block;position:relative}.iris-picker,.iris-picker *{-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input+.iris-picker{margin-top:4px}.iris-error{background-color:#ffafaf}.iris-border{border-radius:3px;border:1px solid #aaa;width:200px;background-color:#fff}.iris-picker-inner{position:absolute;top:0;right:0;left:0;bottom:0}.iris-border .iris-picker-inner{top:10px;right:10px;left:10px;bottom:10px}.iris-picker .iris-square-inner{position:absolute;left:0;right:0;top:0;bottom:0}.iris-picker .iris-square,.iris-picker .iris-slider,.iris-picker .iris-square-inner,.iris-picker .iris-palette{border-radius:3px;box-shadow:inset 0 0 5px rgba(0,0,0,.4);height:100%;width:12.5%;float:left;margin-right:5%}.iris-picker .iris-square{width:76%;margin-right:10%;position:relative}.iris-picker .iris-square-inner{width:auto;margin:0}.iris-ie-9 .iris-square,.iris-ie-9 .iris-slider,.iris-ie-9 .iris-square-inner,.iris-ie-9 .iris-palette{box-shadow:none;border-radius:0}.iris-ie-9 .iris-square,.iris-ie-9 .iris-slider,.iris-ie-9 .iris-palette{outline:1px solid rgba(0,0,0,.1)}.iris-ie-lt9 .iris-square,.iris-ie-lt9 .iris-slider,.iris-ie-lt9 .iris-square-inner,.iris-ie-lt9 .iris-palette{outline:1px solid #aaa}.iris-ie-lt9 .iris-square .ui-slider-handle{outline:1px solid #aaa;background-color:#fff;-ms-filter:"alpha(Opacity=30)"}.iris-ie-lt9 .iris-square .iris-square-handle{background:0;border:3px solid #fff;-ms-filter:"alpha(Opacity=50)"}.iris-picker .iris-strip{margin-right:0;position:relative}.iris-picker .iris-strip .ui-slider-handle{position:absolute;background:0;margin:0;right:-3px;left:-3px;border:4px solid #aaa;border-width:4px 3px;width:auto;height:6px;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.2);opacity:.9;z-index:5;cursor:ns-resize}.iris-strip .ui-slider-handle:before{content:" ";position:absolute;left:-2px;right:-2px;top:-3px;bottom:-3px;border:2px solid #fff;border-radius:3px}.iris-picker .iris-slider-offset{position:absolute;top:11px;left:0;right:0;bottom:-3px;width:auto;height:auto;background:transparent;border:0;border-radius:0}.iris-picker .iris-square-handle{background:transparent;border:5px solid #aaa;border-radius:50%;border-color:rgba(128,128,128,.5);box-shadow:none;width:12px;height:12px;position:absolute;left:-10px;top:-10px;cursor:move;opacity:1;z-index:10}.iris-picker .ui-state-focus .iris-square-handle{opacity:.8}.iris-picker .iris-square-handle:hover{border-color:#999}.iris-picker .iris-square-value:focus .iris-square-handle{box-shadow:0 0 2px rgba(0,0,0,.75);opacity:.8}.iris-picker .iris-square-handle:hover::after{border-color:#fff}.iris-picker .iris-square-handle::after{position:absolute;bottom:-4px;right:-4px;left:-4px;top:-4px;border:3px solid #f9f9f9;border-color:rgba(255,255,255,.8);border-radius:50%;content:" "}.iris-picker .iris-square-value{width:8px;height:8px;position:absolute}.iris-ie-lt9 .iris-square-value,.iris-mozilla .iris-square-value{width:1px;height:1px}.iris-palette-container{position:absolute;bottom:0;left:0;margin:0;padding:0}.iris-border .iris-palette-container{left:10px;bottom:10px}.iris-picker .iris-palette{margin:0;cursor:pointer}.iris-square-handle,.ui-slider-handle{border:0;outline:0}';
   9  
  10      // Even IE9 dosen't support gradients. Elaborate sigh.
  11      UA = navigator.userAgent.toLowerCase();
  12      isIE = navigator.appName === 'Microsoft Internet Explorer';
  13      IEVersion = isIE ? parseFloat( UA.match( /msie ([0-9]{1,}[\.0-9]{0,})/ )[1] ) : 0;
  14      nonGradientIE = ( isIE && IEVersion < 10 );
  15      gradientType = false;
  16  
  17      // we don't bother with an unprefixed version, as it has a different syntax
  18      vendorPrefixes = [ '-moz-', '-webkit-', '-o-', '-ms-' ];
  19  
  20      // Bail for IE <= 7
  21      if ( nonGradientIE && IEVersion <= 7 ) {
  22          $.fn.iris = $.noop;
  23          $.support.iris = false;
  24          return;
  25      }
  26  
  27      $.support.iris = true;
  28  
  29  	function testGradientType() {
  30          var el, base,
  31              bgImageString = 'backgroundImage';
  32  
  33          if ( nonGradientIE ) {
  34              gradientType = 'filter';
  35          }
  36          else {
  37              el = $( '<div id="iris-gradtest" />' );
  38              base = 'linear-gradient(top,#fff,#000)';
  39              $.each( vendorPrefixes, function( i, val ){
  40                  el.css( bgImageString, val + base );
  41                  if ( el.css( bgImageString ).match( 'gradient' ) ) {
  42                      gradientType = i;
  43                      return false;
  44                  }
  45              });
  46              // check for legacy webkit gradient syntax
  47              if ( gradientType === false ) {
  48                  el.css( 'background', '-webkit-gradient(linear,0% 0%,0% 100%,from(#fff),to(#000))' );
  49                  if ( el.css( bgImageString ).match( 'gradient' ) ) {
  50                      gradientType = 'webkit';
  51                  }
  52              }
  53              el.remove();
  54          }
  55  
  56      }
  57  
  58      /**
  59      * Only for CSS3 gradients. oldIE will use a separate function.
  60      *
  61      * Accepts as many color stops as necessary from 2nd arg on, or 2nd
  62      * arg can be an array of color stops
  63      *
  64      * @param  {string} origin Gradient origin - top or left, defaults to left.
  65      * @return {string}        Appropriate CSS3 gradient string for use in
  66      */
  67  	function createGradient( origin, stops ) {
  68          origin = ( origin === 'top' ) ? 'top' : 'left';
  69          stops = $.isArray( stops ) ? stops : Array.prototype.slice.call( arguments, 1 );
  70          if ( gradientType === 'webkit' ) {
  71              return legacyWebkitGradient( origin, stops );
  72          } else {
  73              return vendorPrefixes[ gradientType ] + 'linear-gradient(' + origin + ', ' + stops.join(', ') + ')';
  74          }
  75      }
  76  
  77      /**
  78      * Stupid gradients for a stupid browser.
  79      */
  80  	function stupidIEGradient( origin, stops ) {
  81          var type, self, lastIndex, filter, startPosProp, endPosProp, dimensionProp, template, html;
  82  
  83          origin = ( origin === 'top' ) ? 'top' : 'left';
  84          stops = $.isArray( stops ) ? stops : Array.prototype.slice.call( arguments, 1 );
  85          // 8 hex: AARRGGBB
  86          // GradientType: 0 vertical, 1 horizontal
  87          type = ( origin === 'top' ) ? 0 : 1;
  88          self = $( this );
  89          lastIndex = stops.length - 1;
  90          filter = 'filter';
  91          startPosProp = ( type === 1 ) ? 'left' : 'top';
  92          endPosProp = ( type === 1 ) ? 'right' : 'bottom';
  93          dimensionProp = ( type === 1 ) ? 'height' : 'width';
  94          template = '<div class="iris-ie-gradient-shim" style="position:absolute;' + dimensionProp + ':100%;' + startPosProp + ':%start%;' + endPosProp + ':%end%;' + filter + ':%filter%;" data-color:"%color%"></div>';
  95          html = '';
  96          // need a positioning context
  97          if ( self.css('position') === 'static' ) {
  98              self.css( {position: 'relative' } );
  99          }
 100  
 101          stops = fillColorStops( stops );
 102          $.each(stops, function( i, startColor ) {
 103              var endColor, endStop, filterVal;
 104  
 105              // we want two at a time. if we're on the last pair, bail.
 106              if ( i === lastIndex ) {
 107                  return false;
 108              }
 109  
 110              endColor = stops[ i + 1 ];
 111              //if our pairs are at the same color stop, moving along.
 112              if ( startColor.stop === endColor.stop ) {
 113                  return;
 114              }
 115  
 116              endStop = 100 - parseFloat( endColor.stop ) + '%';
 117              startColor.octoHex = new Color( startColor.color ).toIEOctoHex();
 118              endColor.octoHex = new Color( endColor.color ).toIEOctoHex();
 119  
 120              filterVal = 'progid:DXImageTransform.Microsoft.Gradient(GradientType=' + type + ', StartColorStr=\'' + startColor.octoHex + '\', EndColorStr=\'' + endColor.octoHex + '\')';
 121              html += template.replace( '%start%', startColor.stop ).replace( '%end%', endStop ).replace( '%filter%', filterVal );
 122          });
 123          self.find( '.iris-ie-gradient-shim' ).remove();
 124          $( html ).prependTo( self );
 125      }
 126  
 127  	function legacyWebkitGradient( origin, colorList ) {
 128          var stops = [];
 129          origin = ( origin === 'top' ) ? '0% 0%,0% 100%,' : '0% 100%,100% 100%,';
 130          colorList = fillColorStops( colorList );
 131          $.each( colorList, function( i, val ){
 132              stops.push( 'color-stop(' + ( parseFloat( val.stop ) / 100 ) + ', ' + val.color + ')' );
 133          });
 134          return '-webkit-gradient(linear,' + origin + stops.join(',') + ')';
 135      }
 136  
 137  	function fillColorStops( colorList ) {
 138          var colors = [],
 139              percs = [],
 140              newColorList = [],
 141              lastIndex = colorList.length - 1;
 142  
 143          $.each( colorList, function( index, val ) {
 144              var color = val,
 145                  perc = false,
 146                  match = val.match( /1?[0-9]{1,2}%$/ );
 147  
 148              if ( match ) {
 149                  color = val.replace( /\s?1?[0-9]{1,2}%$/, '' );
 150                  perc = match.shift();
 151              }
 152              colors.push( color );
 153              percs.push( perc );
 154          });
 155  
 156          // back fill first and last
 157          if ( percs[0] === false ) {
 158              percs[0] = '0%';
 159          }
 160  
 161          if ( percs[lastIndex] === false ) {
 162              percs[lastIndex] = '100%';
 163          }
 164  
 165          percs = backFillColorStops( percs );
 166  
 167          $.each( percs, function( i ){
 168              newColorList[i] = { color: colors[i], stop: percs[i] };
 169          });
 170          return newColorList;
 171      }
 172  
 173  	function backFillColorStops( stops ) {
 174          var first = 0,
 175              last = stops.length - 1,
 176              i = 0,
 177              foundFirst = false,
 178              incr,
 179              steps,
 180              step,
 181              firstVal;
 182  
 183          if ( stops.length <= 2 || $.inArray( false, stops ) < 0 ) {
 184              return stops;
 185          }
 186          while ( i < stops.length - 1 ) {
 187              if ( ! foundFirst && stops[i] === false ) {
 188                  first = i - 1;
 189                  foundFirst = true;
 190              } else if ( foundFirst && stops[i] !== false ) {
 191                  last = i;
 192                  i = stops.length;
 193              }
 194              i++;
 195          }
 196          steps = last - first;
 197          firstVal = parseInt( stops[first].replace('%'), 10 );
 198          incr = ( parseFloat( stops[last].replace('%') ) - firstVal ) / steps;
 199          i = first + 1;
 200          step = 1;
 201          while ( i < last ) {
 202              stops[i] = ( firstVal + ( step * incr ) ) + '%';
 203              step++;
 204              i++;
 205          }
 206          return backFillColorStops( stops );
 207      }
 208  
 209      $.fn.gradient = function() {
 210          var args = arguments;
 211          return this.each( function() {
 212              // this'll be oldishIE
 213              if ( nonGradientIE ) {
 214                  stupidIEGradient.apply( this, args );
 215              } else {
 216                  // new hotness
 217                  $( this ).css( 'backgroundImage', createGradient.apply( this, args ) );
 218              }
 219          });
 220      };
 221  
 222      $.fn.raninbowGradient = function( origin, args ) {
 223          var opts, template, i, steps;
 224  
 225          origin = origin || 'top';
 226          opts = $.extend( {}, { s: 100, l: 50 }, args );
 227          template = 'hsl(%h%,' + opts.s + '%,' + opts.l + '%)';
 228          i = 0;
 229          steps = [];
 230          while ( i <= 360 ) {
 231              steps.push( template.replace('%h%', i) );
 232              i += 30;
 233          }
 234          return this.each(function() {
 235              $(this).gradient( origin, steps );
 236          });
 237      };
 238  
 239      // the colorpicker widget def.
 240      Iris = {
 241          options: {
 242              color: false,
 243              mode: 'hsl',
 244              controls: {
 245                  horiz: 's', // horizontal defaults to saturation
 246                  vert: 'l', // vertical defaults to lightness
 247                  strip: 'h' // right strip defaults to hue
 248              },
 249              hide: true, // hide the color picker by default
 250              border: true, // draw a border around the collection of UI elements
 251              target: false, // a DOM element / jQuery selector that the element will be appended within. Only used when called on an input.
 252              width: 200, // the width of the collection of UI elements
 253              palettes: false // show a palette of basic colors beneath the square.
 254          },
 255          _color: '',
 256          _palettes: [ '#000', '#fff', '#d33', '#d93', '#ee2', '#81d742', '#1e73be', '#8224e3' ],
 257          _inited: false,
 258          _defaultHSLControls: {
 259              horiz: 's',
 260              vert: 'l',
 261              strip: 'h'
 262          },
 263          _defaultHSVControls: {
 264              horiz: 'h',
 265              vert: 'v',
 266              strip: 's'
 267          },
 268          _scale: {
 269              h: 360,
 270              s: 100,
 271              l: 100,
 272              v: 100
 273          },
 274          _create: function() {
 275              var self = this,
 276                  el = self.element,
 277                  color = self.options.color || el.val();
 278  
 279              if ( gradientType === false ) {
 280                  testGradientType();
 281              }
 282  
 283              if ( el.is( 'input' ) ) {
 284                  if ( self.options.target ) {
 285                      self.picker = $( _html ).appendTo( self.options.target );
 286                  } else {
 287                      self.picker = $( _html ).insertAfter( el );
 288                  }
 289  
 290                  self._addInputListeners( el );
 291              } else {
 292                  el.append( _html );
 293                  self.picker = el.find( '.iris-picker' );
 294              }
 295  
 296              // Browsers / Versions
 297              // Feature detection doesn't work for these, and $.browser is deprecated
 298              if ( isIE ) {
 299                  if ( IEVersion === 9 ) {
 300                      self.picker.addClass( 'iris-ie-9' );
 301                  } else if ( IEVersion <= 8 ) {
 302                      self.picker.addClass( 'iris-ie-lt9' );
 303                  }
 304              } else if ( UA.indexOf('compatible') < 0 && UA.indexOf('khtml') < 0 && UA.match( /mozilla/ ) ) {
 305                  self.picker.addClass( 'iris-mozilla' );
 306              }
 307  
 308              if ( self.options.palettes ) {
 309                  self._addPalettes();
 310              }
 311  
 312              self._color = new Color( color ).setHSpace( self.options.mode );
 313              self.options.color = self._color.toString();
 314  
 315              // prep 'em for re-use
 316              self.controls = {
 317                  square:      self.picker.find( '.iris-square' ),
 318                  squareDrag:  self.picker.find( '.iris-square-value' ),
 319                  horiz:       self.picker.find( '.iris-square-horiz' ),
 320                  vert:        self.picker.find( '.iris-square-vert' ),
 321                  strip:       self.picker.find( '.iris-strip' ),
 322                  stripSlider: self.picker.find( '.iris-strip .iris-slider-offset' )
 323              };
 324  
 325              // small sanity check - if we chose hsv, change default controls away from hsl
 326              if ( self.options.mode === 'hsv' && self._has('l', self.options.controls) ) {
 327                  self.options.controls = self._defaultHSVControls;
 328              } else if ( self.options.mode === 'hsl' && self._has('v', self.options.controls) ) {
 329                  self.options.controls = self._defaultHSLControls;
 330              }
 331  
 332              // store it. HSL gets squirrely
 333              self.hue = self._color.h();
 334  
 335              if ( self.options.hide ) {
 336                  self.picker.hide();
 337              }
 338  
 339              if ( self.options.border ) {
 340                  self.picker.addClass( 'iris-border' );
 341              }
 342  
 343              self._initControls();
 344              self.active = 'external';
 345              self._dimensions();
 346              self._change();
 347          },
 348          _has: function(needle, haystack) {
 349              var ret = false;
 350              $.each(haystack, function(i,v){
 351                  if ( needle === v ) {
 352                      ret = true;
 353                      // exit the loop
 354                      return false;
 355                  }
 356              });
 357              return ret;
 358          },
 359          _addPalettes: function () {
 360              var container = $( '<div class="iris-palette-container" />' ),
 361                  palette = $( '<a class="iris-palette" tabindex="0" />' ),
 362                  colors = $.isArray( this.options.palettes ) ? this.options.palettes : this._palettes;
 363  
 364              // do we have an existing container? Empty and reuse it.
 365              if ( this.picker.find( '.iris-palette-container' ).length ) {
 366                  container = this.picker.find( '.iris-palette-container' ).detach().html( '' );
 367              }
 368  
 369              $.each(colors, function(index, val) {
 370                  palette.clone().data( 'color', val )
 371                      .css( 'backgroundColor', val ).appendTo( container )
 372                      .height( 10 ).width( 10 );
 373              });
 374  
 375              this.picker.append(container);
 376          },
 377          _paint: function() {
 378              var self = this;
 379              self._paintDimension( 'top', 'strip' );
 380              self._paintDimension( 'top', 'vert' );
 381              self._paintDimension( 'left', 'horiz' );
 382          },
 383          _paintDimension: function( origin, control ) {
 384              var self = this,
 385                  c = self._color,
 386                  mode = self.options.mode,
 387                  color = self._getHSpaceColor(),
 388                  target = self.controls[ control ],
 389                  controlOpts = self.options.controls,
 390                  stops;
 391  
 392              // don't paint the active control
 393              if ( control === self.active || ( self.active === 'square' && control !== 'strip' ) ) {
 394                  return;
 395              }
 396  
 397              switch ( controlOpts[ control ] ) {
 398                  case 'h':
 399                      if ( mode === 'hsv' ) {
 400                          color = c.clone();
 401                          switch ( control ) {
 402                              case 'horiz':
 403                                  color[controlOpts.vert](100);
 404                                  break;
 405                              case 'vert':
 406                                  color[controlOpts.horiz](100);
 407                                  break;
 408                              case 'strip':
 409                                  color.setHSpace('hsl');
 410                                  break;
 411                          }
 412                          stops = color.toHsl();
 413                      } else {
 414                          if ( control === 'strip' ) {
 415                              stops = { s: color.s, l: color.l };
 416                          } else {
 417                              stops = { s: 100, l: color.l };
 418                          }
 419                      }
 420  
 421                      target.raninbowGradient( origin, stops );
 422                      break;
 423                  case 's':
 424                      if ( mode === 'hsv' ) {
 425                          if ( control === 'vert' ) {
 426                              stops = [ c.clone().a(0).s(0).toCSS('rgba'), c.clone().a(1).s(0).toCSS('rgba') ];
 427                          } else if ( control === 'strip' ) {
 428                              stops = [ c.clone().s(100).toCSS('hsl'), c.clone().s(0).toCSS('hsl') ];
 429                          } else if ( control === 'horiz' ) {
 430                              stops = [ '#fff', 'hsl(' + color.h + ',100%,50%)' ];
 431                          }
 432                      } else { // implicit mode === 'hsl'
 433                          if ( control === 'vert' && self.options.controls.horiz === 'h' ) {
 434                              stops = ['hsla(0, 0%, ' + color.l + '%, 0)', 'hsla(0, 0%, ' + color.l + '%, 1)'];
 435                          } else {
 436                              stops = ['hsl('+ color.h +',0%,50%)', 'hsl(' + color.h + ',100%,50%)'];
 437                          }
 438                      }
 439  
 440  
 441                      target.gradient( origin, stops );
 442                      break;
 443                  case 'l':
 444                      if ( control === 'strip' ) {
 445                          stops = ['hsl(' + color.h + ',100%,100%)', 'hsl(' + color.h + ', ' + color.s + '%,50%)', 'hsl('+ color.h +',100%,0%)'];
 446                      } else {
 447                          stops = ['#fff', 'rgba(255,255,255,0) 50%', 'rgba(0,0,0,0) 50%', 'rgba(0,0,0,1)'];
 448                      }
 449                      target.gradient( origin, stops );
 450                      break;
 451                  case 'v':
 452                          if ( control === 'strip' ) {
 453                              stops = [ c.clone().v(100).toCSS(), c.clone().v(0).toCSS() ];
 454                          } else {
 455                              stops = ['rgba(0,0,0,0)', '#000'];
 456                          }
 457                          target.gradient( origin, stops );
 458                      break;
 459                  default:
 460                      break;
 461              }
 462          },
 463  
 464          _getHSpaceColor: function() {
 465              return ( this.options.mode === 'hsv' ) ? this._color.toHsv() : this._color.toHsl();
 466          },
 467  
 468          _dimensions: function( reset ) {
 469              // whatever size
 470              var self = this,
 471                  opts = self.options,
 472                  controls = self.controls,
 473                  square = controls.square,
 474                  strip = self.picker.find( '.iris-strip' ),
 475                  squareWidth = '77.5%',
 476                  stripWidth = '12%',
 477                  totalPadding = 20,
 478                  innerWidth = opts.border ? opts.width - totalPadding : opts.width,
 479                  controlsHeight,
 480                  paletteCount = $.isArray( opts.palettes ) ? opts.palettes.length : self._palettes.length,
 481                  paletteMargin, paletteWidth, paletteContainerWidth;
 482  
 483              if ( reset ) {
 484                  square.css( 'width', '' );
 485                  strip.css( 'width', '' );
 486                  self.picker.css( {width: '', height: ''} );
 487              }
 488  
 489              squareWidth = innerWidth * ( parseFloat( squareWidth ) / 100 );
 490              stripWidth = innerWidth * ( parseFloat( stripWidth ) / 100 );
 491              controlsHeight = opts.border ? squareWidth + totalPadding : squareWidth;
 492  
 493              square.width( squareWidth ).height( squareWidth );
 494              strip.height( squareWidth ).width( stripWidth );
 495              self.picker.css( { width: opts.width, height: controlsHeight } );
 496  
 497              if ( ! opts.palettes ) {
 498                  return self.picker.css( 'paddingBottom', '' );
 499              }
 500  
 501              // single margin at 2%
 502              paletteMargin = squareWidth * 2 / 100;
 503              paletteContainerWidth = squareWidth - ( ( paletteCount - 1 ) * paletteMargin );
 504              paletteWidth = paletteContainerWidth / paletteCount;
 505              self.picker.find('.iris-palette').each( function( i ) {
 506                  var margin = i === 0 ? 0 : paletteMargin;
 507                  $( this ).css({
 508                      width: paletteWidth,
 509                      height: paletteWidth,
 510                      marginLeft: margin
 511                  });
 512              });
 513              self.picker.css( 'paddingBottom', paletteWidth + paletteMargin );
 514              strip.height( paletteWidth + paletteMargin + squareWidth );
 515          },
 516  
 517          _addInputListeners: function( input ) {
 518              var self = this,
 519                  debounceTimeout = 100,
 520                  callback = function( event ){
 521                      var color = new Color( input.val() ),
 522                          val = input.val().replace( /^#/, '' );
 523  
 524                      input.removeClass( 'iris-error' );
 525                      // we gave a bad color
 526                      if ( color.error ) {
 527                          // don't error on an empty input - we want those allowed
 528                          if ( val !== '' ) {
 529                              input.addClass( 'iris-error' );
 530                          }
 531                      } else {
 532                          if ( color.toString() !== self._color.toString() ) {
 533                              // let's not do this on keyup for hex shortcodes
 534                              if ( ! ( event.type === 'keyup' && val.match( /^[0-9a-fA-F]{3}$/ ) ) ) {
 535                                  self._setOption( 'color', color.toString() );
 536                              }
 537                          }
 538                      }
 539                  };
 540  
 541              input.on( 'change', callback ).on( 'keyup', self._debounce( callback, debounceTimeout ) );
 542  
 543              // If we initialized hidden, show on first focus. The rest is up to you.
 544              if ( self.options.hide ) {
 545                  input.one( 'focus', function() {
 546                      self.show();
 547                  });
 548              }
 549          },
 550  
 551          _initControls: function() {
 552              var self = this,
 553                  controls = self.controls,
 554                  square = controls.square,
 555                  controlOpts = self.options.controls,
 556                  stripScale = self._scale[controlOpts.strip];
 557  
 558              controls.stripSlider.slider({
 559                  orientation: 'vertical',
 560                  max: stripScale,
 561                  slide: function( event, ui ) {
 562                      self.active = 'strip';
 563                      // "reverse" for hue.
 564                      if ( controlOpts.strip === 'h' ) {
 565                          ui.value = stripScale - ui.value;
 566                      }
 567  
 568                      self._color[controlOpts.strip]( ui.value );
 569                      self._change.apply( self, arguments );
 570                  }
 571              });
 572  
 573              controls.squareDrag.draggable({
 574                  containment: controls.square.find( '.iris-square-inner' ),
 575                  zIndex: 1000,
 576                  cursor: 'move',
 577                  drag: function( event, ui ) {
 578                      self._squareDrag( event, ui );
 579                  },
 580                  start: function() {
 581                      square.addClass( 'iris-dragging' );
 582                      $(this).addClass( 'ui-state-focus' );
 583                  },
 584                  stop: function() {
 585                      square.removeClass( 'iris-dragging' );
 586                      $(this).removeClass( 'ui-state-focus' );
 587                  }
 588              }).on( 'mousedown mouseup', function( event ) {
 589                  var focusClass = 'ui-state-focus';
 590                  event.preventDefault();
 591                  if (event.type === 'mousedown' ) {
 592                      self.picker.find( '.' + focusClass ).removeClass( focusClass ).blur();
 593                      $(this).addClass( focusClass ).focus();
 594                  } else {
 595                      $(this).removeClass( focusClass );
 596                  }
 597              }).on( 'keydown', function( event ) {
 598                  var container = controls.square,
 599                      draggable = controls.squareDrag,
 600                      position = draggable.position(),
 601                      distance = self.options.width / 100; // Distance in pixels the draggable should be moved: 1 "stop"
 602  
 603                  // make alt key go "10"
 604                  if ( event.altKey ) {
 605                      distance *= 10;
 606                  }
 607  
 608                  // Reposition if one of the directional keys is pressed
 609                  switch ( event.keyCode ) {
 610                      case 37: position.left -= distance; break; // Left
 611                      case 38: position.top  -= distance; break; // Up
 612                      case 39: position.left += distance; break; // Right
 613                      case 40: position.top  += distance; break; // Down
 614                      default: return true; // Exit and bubble
 615                  }
 616  
 617                  // Keep draggable within container
 618                  position.left = Math.max( 0, Math.min( position.left, container.width() ) );
 619                  position.top =  Math.max( 0, Math.min( position.top, container.height() ) );
 620  
 621                  draggable.css(position);
 622                  self._squareDrag( event, { position: position });
 623                  event.preventDefault();
 624              });
 625  
 626              // allow clicking on the square to move there and keep dragging
 627              square.mousedown( function( event ) {
 628                  var squareOffset, pos;
 629                  // only left click
 630                  if ( event.which !== 1 ) {
 631                      return;
 632                  }
 633  
 634                  // prevent bubbling from the handle: no infinite loops
 635                  if ( ! $( event.target ).is( 'div' ) ) {
 636                      return;
 637                  }
 638  
 639                  squareOffset = self.controls.square.offset();
 640                  pos = {
 641                          top: event.pageY - squareOffset.top,
 642                          left: event.pageX - squareOffset.left
 643                  };
 644                  event.preventDefault();
 645                  self._squareDrag( event, { position: pos } );
 646                  event.target = self.controls.squareDrag.get(0);
 647                  self.controls.squareDrag.css( pos ).trigger( event );
 648              });
 649  
 650              // palettes
 651              if ( self.options.palettes ) {
 652                  self._paletteListeners();
 653              }
 654          },
 655  
 656          _paletteListeners: function() {
 657              var self = this;
 658              self.picker.find('.iris-palette-container').on('click.palette', '.iris-palette', function() {
 659                  self._color.fromCSS( $(this).data('color') );
 660                  self.active = 'external';
 661                  self._change();
 662              }).on( 'keydown.palette', '.iris-palette', function( event ) {
 663                  if ( ! ( event.keyCode === 13 || event.keyCode === 32 ) ) {
 664                      return true;
 665                  }
 666                  event.stopPropagation();
 667                  $( this ).click();
 668              });
 669          },
 670  
 671          _squareDrag: function( event, ui ) {
 672              var self = this,
 673                  controlOpts = self.options.controls,
 674                  dimensions = self._squareDimensions(),
 675                  vertVal = Math.round( ( dimensions.h - ui.position.top ) / dimensions.h * self._scale[controlOpts.vert] ),
 676                  horizVal = self._scale[controlOpts.horiz] - Math.round( ( dimensions.w - ui.position.left ) / dimensions.w * self._scale[controlOpts.horiz] );
 677  
 678              self._color[controlOpts.horiz]( horizVal )[controlOpts.vert]( vertVal );
 679  
 680              self.active = 'square';
 681              self._change.apply( self, arguments );
 682          },
 683  
 684          _setOption: function( key, value ) {
 685              var self = this,
 686                  oldValue = self.options[key],
 687                  doDimensions = false,
 688                  hexLessColor,
 689                  newColor,
 690                  method;
 691  
 692              // ensure the new value is set. We can reset to oldValue if some check wasn't met.
 693              self.options[key] = value;
 694  
 695              switch(key) {
 696                  case 'color':
 697                      // cast to string in case we have a number
 698                      value = '' + value;
 699                      hexLessColor = value.replace( /^#/, '' );
 700                      newColor = new Color( value ).setHSpace( self.options.mode );
 701                      if ( newColor.error ) {
 702                          self.options[key] = oldValue;
 703                      } else {
 704                          self._color = newColor;
 705                          self.options.color = self.options[key] = self._color.toString();
 706                          self.active = 'external';
 707                          self._change();
 708                      }
 709                      break;
 710                  case 'palettes':
 711                      doDimensions = true;
 712  
 713                      if ( value ) {
 714                          self._addPalettes();
 715                      } else {
 716                          self.picker.find('.iris-palette-container').remove();
 717                      }
 718  
 719                      // do we need to add events?
 720                      if ( ! oldValue ) {
 721                          self._paletteListeners();
 722                      }
 723                      break;
 724                  case 'width':
 725                      doDimensions = true;
 726                      break;
 727                  case 'border':
 728                      doDimensions = true;
 729                      method = value ? 'addClass' : 'removeClass';
 730                      self.picker[method]('iris-border');
 731                      break;
 732                  case 'mode':
 733                  case 'controls':
 734                      // if nothing's changed, let's bail, since this causes re-rendering the whole widget
 735                      if ( oldValue === value ) {
 736                          return;
 737                      }
 738  
 739                      // we're using these poorly named variables because they're already scoped.
 740                      // method is the element that Iris was called on. oldValue will be the options
 741                      method = self.element;
 742                      oldValue = self.options;
 743                      oldValue.hide = ! self.picker.is( ':visible' );
 744                      self.destroy();
 745                      self.picker.remove();
 746                      return $(self.element).iris(oldValue);
 747              }
 748  
 749              // Do we need to recalc dimensions?
 750              if ( doDimensions ) {
 751                  self._dimensions(true);
 752              }
 753          },
 754  
 755          _squareDimensions: function( forceRefresh ) {
 756              var square = this.controls.square,
 757                  dimensions,
 758                  control;
 759  
 760              if ( forceRefresh !== undef && square.data('dimensions') ) {
 761                  return square.data('dimensions');
 762              }
 763  
 764              control = this.controls.squareDrag;
 765              dimensions = {
 766                  w: square.width(),
 767                  h: square.height()
 768              };
 769              square.data( 'dimensions', dimensions );
 770              return dimensions;
 771          },
 772  
 773          _isNonHueControl: function( active, type ) {
 774              if ( active === 'square' && this.options.controls.strip === 'h' ) {
 775                  return true;
 776              } else if ( type === 'external' || ( type === 'h' && active === 'strip' ) ) {
 777                  return false;
 778              }
 779  
 780              return true;
 781          },
 782  
 783          _change: function() {
 784              var self = this,
 785                  controls = self.controls,
 786                  color = self._getHSpaceColor(),
 787                  actions = [ 'square', 'strip' ],
 788                  controlOpts = self.options.controls,
 789                  type = controlOpts[self.active] || 'external',
 790                  oldHue = self.hue;
 791  
 792              if ( self.active === 'strip' ) {
 793                  // take no action on any of the square sliders if we adjusted the strip
 794                  actions = [];
 795              } else if ( self.active !== 'external' ) {
 796                  // for non-strip, non-external, strip should never change
 797                  actions.pop(); // conveniently the last item
 798              }
 799  
 800              $.each( actions, function(index, item) {
 801                  var value, dimensions, cssObj;
 802                  if ( item !== self.active ) {
 803                      switch ( item ) {
 804                          case 'strip':
 805                              // reverse for hue
 806                              value = ( controlOpts.strip === 'h' ) ? self._scale[controlOpts.strip] - color[controlOpts.strip] : color[controlOpts.strip];
 807                              controls.stripSlider.slider( 'value', value );
 808                              break;
 809                          case 'square':
 810                              dimensions = self._squareDimensions();
 811                              cssObj = {
 812                                  left: color[controlOpts.horiz] / self._scale[controlOpts.horiz] * dimensions.w,
 813                                  top: dimensions.h - ( color[controlOpts.vert] / self._scale[controlOpts.vert] * dimensions.h )
 814                              };
 815  
 816                              self.controls.squareDrag.css( cssObj );
 817                              break;
 818                      }
 819                  }
 820              });
 821  
 822              // Ensure that we don't change hue if we triggered a hue reset
 823              if ( color.h !== oldHue && self._isNonHueControl( self.active, type ) ) {
 824                  self._color.h(oldHue);
 825              }
 826  
 827              // store hue for repeating above check next time
 828              self.hue = self._color.h();
 829  
 830              self.options.color = self._color.toString();
 831  
 832              // only run after the first time
 833              if ( self._inited ) {
 834                  self._trigger( 'change', { type: self.active }, { color: self._color } );
 835              }
 836  
 837              if ( self.element.is( ':input' ) && ! self._color.error ) {
 838                  self.element.removeClass( 'iris-error' );
 839                  if ( self.element.val() !== self._color.toString() ) {
 840                      self.element.val( self._color.toString() );
 841                  }
 842              }
 843  
 844              self._paint();
 845              self._inited = true;
 846              self.active = false;
 847          },
 848          // taken from underscore.js _.debounce method
 849          _debounce: function( func, wait, immediate ) {
 850              var timeout, result;
 851              return function() {
 852                  var context = this,
 853                      args = arguments,
 854                      later,
 855                      callNow;
 856  
 857                  later = function() {
 858                      timeout = null;
 859                      if ( ! immediate) {
 860                          result = func.apply( context, args );
 861                      }
 862                  };
 863  
 864                  callNow = immediate && !timeout;
 865                  clearTimeout( timeout );
 866                  timeout = setTimeout( later, wait );
 867                  if ( callNow ) {
 868                      result = func.apply( context, args );
 869                  }
 870                  return result;
 871              };
 872          },
 873          show: function() {
 874              this.picker.show();
 875          },
 876          hide: function() {
 877              this.picker.hide();
 878          },
 879          toggle: function() {
 880              this.picker.toggle();
 881          },
 882          color: function(newColor) {
 883              if ( newColor === true ) {
 884                  return this._color.clone();
 885              } else if ( newColor === undef ) {
 886                  return this._color.toString();
 887              }
 888              this.option('color', newColor);
 889          }
 890      };
 891      // initialize the widget
 892      $.widget( 'a8c.iris', Iris );
 893      // add CSS
 894      $( '<style id="iris-css">' + _css + '</style>' ).appendTo( 'head' );
 895  
 896  }( jQuery ));
 897  /*! Color.js - v0.9.11 - 2013-08-09
 898  * https://github.com/Automattic/Color.js
 899  * Copyright (c) 2013 Matt Wiebe; Licensed GPLv2 */
 900  (function(global, undef) {
 901  
 902      var Color = function( color, type ) {
 903          if ( ! ( this instanceof Color ) )
 904              return new Color( color, type );
 905  
 906          return this._init( color, type );
 907      };
 908  
 909      Color.fn = Color.prototype = {
 910          _color: 0,
 911          _alpha: 1,
 912          error: false,
 913          // for preserving hue/sat in fromHsl().toHsl() flows
 914          _hsl: { h: 0, s: 0, l: 0 },
 915          // for preserving hue/sat in fromHsv().toHsv() flows
 916          _hsv: { h: 0, s: 0, v: 0 },
 917          // for setting hsl or hsv space - needed for .h() & .s() functions to function properly
 918          _hSpace: 'hsl',
 919          _init: function( color ) {
 920              var func = 'noop';
 921              switch ( typeof color ) {
 922                      case 'object':
 923                          // alpha?
 924                          if ( color.a !== undef )
 925                              this.a( color.a );
 926                          func = ( color.r !== undef ) ? 'fromRgb' :
 927                              ( color.l !== undef ) ? 'fromHsl' :
 928                              ( color.v !== undef ) ? 'fromHsv' : func;
 929                          return this[func]( color );
 930                      case 'string':
 931                          return this.fromCSS( color );
 932                      case 'number':
 933                          return this.fromInt( parseInt( color, 10 ) );
 934              }
 935              return this;
 936          },
 937  
 938          _error: function() {
 939              this.error = true;
 940              return this;
 941          },
 942  
 943          clone: function() {
 944              var newColor = new Color( this.toInt() ),
 945                  copy = ['_alpha', '_hSpace', '_hsl', '_hsv', 'error'];
 946              for ( var i = copy.length - 1; i >= 0; i-- ) {
 947                  newColor[ copy[i] ] = this[ copy[i] ];
 948              }
 949              return newColor;
 950          },
 951  
 952          setHSpace: function( space ) {
 953              this._hSpace = ( space === 'hsv' ) ? space : 'hsl';
 954              return this;
 955          },
 956  
 957          noop: function() {
 958              return this;
 959          },
 960  
 961          fromCSS: function( color ) {
 962              var list,
 963                  leadingRE = /^(rgb|hs(l|v))a?\(/;
 964              this.error = false;
 965  
 966              // whitespace and semicolon trim
 967              color = color.replace(/^\s+/, '').replace(/\s+$/, '').replace(/;$/, '');
 968  
 969              if ( color.match(leadingRE) && color.match(/\)$/) ) {
 970                  list = color.replace(/(\s|%)/g, '').replace(leadingRE, '').replace(/,?\);?$/, '').split(',');
 971  
 972                  if ( list.length < 3 )
 973                      return this._error();
 974  
 975                  if ( list.length === 4 ) {
 976                      this.a( parseFloat( list.pop() ) );
 977                      // error state has been set to true in .a() if we passed NaN
 978                      if ( this.error )
 979                          return this;
 980                  }
 981  
 982                  for (var i = list.length - 1; i >= 0; i--) {
 983                      list[i] = parseInt(list[i], 10);
 984                      if ( isNaN( list[i] ) )
 985                          return this._error();
 986                  }
 987  
 988                  if ( color.match(/^rgb/) ) {
 989                      return this.fromRgb( {
 990                          r: list[0],
 991                          g: list[1],
 992                          b: list[2]
 993                      } );
 994                  } else if ( color.match(/^hsv/) ) {
 995                      return this.fromHsv( {
 996                          h: list[0],
 997                          s: list[1],
 998                          v: list[2]
 999                      } );
1000                  } else {
1001                      return this.fromHsl( {
1002                          h: list[0],
1003                          s: list[1],
1004                          l: list[2]
1005                      } );
1006                  }
1007              } else {
1008                  // must be hex amirite?
1009                  return this.fromHex( color );
1010              }
1011          },
1012  
1013          fromRgb: function( rgb, preserve ) {
1014              if ( typeof rgb !== 'object' || rgb.r === undef || rgb.g === undef || rgb.b === undef )
1015                  return this._error();
1016  
1017              this.error = false;
1018              return this.fromInt( parseInt( ( rgb.r << 16 ) + ( rgb.g << 8 ) + rgb.b, 10 ), preserve );
1019          },
1020  
1021          fromHex: function( color ) {
1022              color = color.replace(/^#/, '').replace(/^0x/, '');
1023              if ( color.length === 3 ) {
1024                  color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
1025              }
1026  
1027              // rough error checking - this is where things go squirrely the most
1028              this.error = ! /^[0-9A-F]{6}$/i.test( color );
1029              return this.fromInt( parseInt( color, 16 ) );
1030          },
1031  
1032          fromHsl: function( hsl ) {
1033              var r, g, b, q, p, h, s, l;
1034  
1035              if ( typeof hsl !== 'object' || hsl.h === undef || hsl.s === undef || hsl.l === undef )
1036                  return this._error();
1037  
1038              this._hsl = hsl; // store it
1039              this._hSpace = 'hsl'; // implicit
1040              h = hsl.h / 360; s = hsl.s / 100; l = hsl.l / 100;
1041              if ( s === 0 ) {
1042                  r = g = b = l; // achromatic
1043              }
1044              else {
1045                  q = l < 0.5 ? l * ( 1 + s ) : l + s - l * s;
1046                  p = 2 * l - q;
1047                  r = this.hue2rgb( p, q, h + 1/3 );
1048                  g = this.hue2rgb( p, q, h );
1049                  b = this.hue2rgb( p, q, h - 1/3 );
1050              }
1051              return this.fromRgb( {
1052                  r: r * 255,
1053                  g: g * 255,
1054                  b: b * 255
1055              }, true ); // true preserves hue/sat
1056          },
1057  
1058          fromHsv: function( hsv ) {
1059              var h, s, v, r, g, b, i, f, p, q, t;
1060              if ( typeof hsv !== 'object' || hsv.h === undef || hsv.s === undef || hsv.v === undef )
1061                  return this._error();
1062  
1063              this._hsv = hsv; // store it
1064              this._hSpace = 'hsv'; // implicit
1065  
1066              h = hsv.h / 360; s = hsv.s / 100; v = hsv.v / 100;
1067              i = Math.floor( h * 6 );
1068              f = h * 6 - i;
1069              p = v * ( 1 - s );
1070              q = v * ( 1 - f * s );
1071              t = v * ( 1 - ( 1 - f ) * s );
1072  
1073              switch( i % 6 ) {
1074                  case 0:
1075                      r = v; g = t; b = p;
1076                      break;
1077                  case 1:
1078                      r = q; g = v; b = p;
1079                      break;
1080                  case 2:
1081                      r = p; g = v; b = t;
1082                      break;
1083                  case 3:
1084                      r = p; g = q; b = v;
1085                      break;
1086                  case 4:
1087                      r = t; g = p; b = v;
1088                      break;
1089                  case 5:
1090                      r = v; g = p; b = q;
1091                      break;
1092              }
1093  
1094              return this.fromRgb( {
1095                  r: r * 255,
1096                  g: g * 255,
1097                  b: b * 255
1098              }, true ); // true preserves hue/sat
1099  
1100          },
1101          // everything comes down to fromInt
1102          fromInt: function( color, preserve ) {
1103              this._color = parseInt( color, 10 );
1104  
1105              if ( isNaN( this._color ) )
1106                  this._color = 0;
1107  
1108              // let's coerce things
1109              if ( this._color > 16777215 )
1110                  this._color = 16777215;
1111              else if ( this._color < 0 )
1112                  this._color = 0;
1113  
1114              // let's not do weird things
1115              if ( preserve === undef ) {
1116                  this._hsv.h = this._hsv.s = this._hsl.h = this._hsl.s = 0;
1117              }
1118              // EVENT GOES HERE
1119              return this;
1120          },
1121  
1122          hue2rgb: function( p, q, t ) {
1123              if ( t < 0 ) {
1124                  t += 1;
1125              }
1126              if ( t > 1 ) {
1127                  t -= 1;
1128              }
1129              if ( t < 1/6 ) {
1130                  return p + ( q - p ) * 6 * t;
1131              }
1132              if ( t < 1/2 ) {
1133                  return q;
1134              }
1135              if ( t < 2/3 ) {
1136                  return p + ( q - p ) * ( 2/3 - t ) * 6;
1137              }
1138              return p;
1139          },
1140  
1141          toString: function() {
1142              var hex = parseInt( this._color, 10 ).toString( 16 );
1143              if ( this.error )
1144                  return '';
1145              // maybe left pad it
1146              if ( hex.length < 6 ) {
1147                  for (var i = 6 - hex.length - 1; i >= 0; i--) {
1148                      hex = '0' + hex;
1149                  }
1150              }
1151              return '#' + hex;
1152          },
1153  
1154          toCSS: function( type, alpha ) {
1155              type = type || 'hex';
1156              alpha = parseFloat( alpha || this._alpha );
1157              switch ( type ) {
1158                  case 'rgb':
1159                  case 'rgba':
1160                      var rgb = this.toRgb();
1161                      if ( alpha < 1 ) {
1162                          return "rgba( " + rgb.r + ", " + rgb.g + ", " + rgb.b + ", " + alpha + " )";
1163                      }
1164                      else {
1165                          return "rgb( " + rgb.r + ", " + rgb.g + ", " + rgb.b + " )";
1166                      }
1167                      break;
1168                  case 'hsl':
1169                  case 'hsla':
1170                      var hsl = this.toHsl();
1171                      if ( alpha < 1 ) {
1172                          return "hsla( " + hsl.h + ", " + hsl.s + "%, " + hsl.l + "%, " + alpha + " )";
1173                      }
1174                      else {
1175                          return "hsl( " + hsl.h + ", " + hsl.s + "%, " + hsl.l + "% )";
1176                      }
1177                      break;
1178                  default:
1179                      return this.toString();
1180              }
1181          },
1182  
1183          toRgb: function() {
1184              return {
1185                  r: 255 & ( this._color >> 16 ),
1186                  g: 255 & ( this._color >> 8 ),
1187                  b: 255 & ( this._color )
1188              };
1189          },
1190  
1191          toHsl: function() {
1192              var rgb = this.toRgb();
1193              var r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255;
1194              var max = Math.max( r, g, b ), min = Math.min( r, g, b );
1195              var h, s, l = ( max + min ) / 2;
1196  
1197              if ( max === min ) {
1198                  h = s = 0; // achromatic
1199              } else {
1200                  var d = max - min;
1201                  s = l > 0.5 ? d / ( 2 - max - min ) : d / ( max + min );
1202                  switch ( max ) {
1203                      case r: h = ( g - b ) / d + ( g < b ? 6 : 0 );
1204                          break;
1205                      case g: h = ( b - r ) / d + 2;
1206                          break;
1207                      case b: h = ( r - g ) / d + 4;
1208                          break;
1209                  }
1210                  h /= 6;
1211              }
1212  
1213              // maintain hue & sat if we've been manipulating things in the HSL space.
1214              h = Math.round( h * 360 );
1215              if ( h === 0 && this._hsl.h !== h ) {
1216                  h = this._hsl.h;
1217              }
1218              s = Math.round( s * 100 );
1219              if ( s === 0 && this._hsl.s ) {
1220                  s = this._hsl.s;
1221              }
1222  
1223              return {
1224                  h: h,
1225                  s: s,
1226                  l: Math.round( l * 100 )
1227              };
1228  
1229          },
1230  
1231          toHsv: function() {
1232              var rgb = this.toRgb();
1233              var r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255;
1234              var max = Math.max( r, g, b ), min = Math.min( r, g, b );
1235              var h, s, v = max;
1236              var d = max - min;
1237              s = max === 0 ? 0 : d / max;
1238  
1239              if ( max === min ) {
1240                  h = s = 0; // achromatic
1241              } else {
1242                  switch( max ){
1243                      case r:
1244                          h = ( g - b ) / d + ( g < b ? 6 : 0 );
1245                          break;
1246                      case g:
1247                          h = ( b - r ) / d + 2;
1248                          break;
1249                      case b:
1250                          h = ( r - g ) / d + 4;
1251                          break;
1252                  }
1253                  h /= 6;
1254              }
1255  
1256              // maintain hue & sat if we've been manipulating things in the HSV space.
1257              h = Math.round( h * 360 );
1258              if ( h === 0 && this._hsv.h !== h ) {
1259                  h = this._hsv.h;
1260              }
1261              s = Math.round( s * 100 );
1262              if ( s === 0 && this._hsv.s ) {
1263                  s = this._hsv.s;
1264              }
1265  
1266              return {
1267                  h: h,
1268                  s: s,
1269                  v: Math.round( v * 100 )
1270              };
1271          },
1272  
1273          toInt: function() {
1274              return this._color;
1275          },
1276  
1277          toIEOctoHex: function() {
1278              // AARRBBGG
1279              var hex = this.toString();
1280              var AA = parseInt( 255 * this._alpha, 10 ).toString(16);
1281              if ( AA.length === 1 ) {
1282                  AA = '0' + AA;
1283              }
1284              return '#' + AA + hex.replace(/^#/, '' );
1285          },
1286  
1287          toLuminosity: function() {
1288              var rgb = this.toRgb();
1289              return 0.2126 * Math.pow( rgb.r / 255, 2.2 ) + 0.7152 * Math.pow( rgb.g / 255, 2.2 ) + 0.0722 * Math.pow( rgb.b / 255, 2.2);
1290          },
1291  
1292          getDistanceLuminosityFrom: function( color ) {
1293              if ( ! ( color instanceof Color ) ) {
1294                  throw 'getDistanceLuminosityFrom requires a Color object';
1295              }
1296              var lum1 = this.toLuminosity();
1297              var lum2 = color.toLuminosity();
1298              if ( lum1 > lum2 ) {
1299                  return ( lum1 + 0.05 ) / ( lum2 + 0.05 );
1300              }
1301              else {
1302                  return ( lum2 + 0.05 ) / ( lum1 + 0.05 );
1303              }
1304          },
1305  
1306          getMaxContrastColor: function() {
1307              var lum = this.toLuminosity();
1308              var hex = ( lum >= 0.5 ) ? '000000' : 'ffffff';
1309              return new Color( hex );
1310          },
1311  
1312          getReadableContrastingColor: function( bgColor, minContrast ) {
1313              if ( ! bgColor instanceof Color ) {
1314                  return this;
1315              }
1316  
1317              // you shouldn't use less than 5, but you might want to.
1318              var targetContrast = ( minContrast === undef ) ? 5 : minContrast;
1319              // working things
1320              var contrast = bgColor.getDistanceLuminosityFrom( this );
1321              var maxContrastColor = bgColor.getMaxContrastColor();
1322              var maxContrast = maxContrastColor.getDistanceLuminosityFrom( bgColor );
1323  
1324              // if current max contrast is less than the target contrast, we had wishful thinking.
1325              // still, go max
1326              if ( maxContrast <= targetContrast ) {
1327                  return maxContrastColor;
1328              }
1329              // or, we might already have sufficient contrast
1330              else if ( contrast >= targetContrast ) {
1331                  return this;
1332              }
1333  
1334              var incr = ( 0 === maxContrastColor.toInt() ) ? -1 : 1;
1335              while ( contrast < targetContrast ) {
1336                  this.l( incr, true ); // 2nd arg turns this into an incrementer
1337                  contrast = this.getDistanceLuminosityFrom( bgColor );
1338                  // infininite loop prevention: you never know.
1339                  if ( this._color === 0 || this._color === 16777215 ) {
1340                      break;
1341                  }
1342              }
1343  
1344              return this;
1345  
1346          },
1347  
1348          a: function( val ) {
1349              if ( val === undef )
1350                  return this._alpha;
1351  
1352              var a = parseFloat( val );
1353  
1354              if ( isNaN( a ) )
1355                  return this._error();
1356  
1357              this._alpha = a;
1358              return this;
1359          },
1360  
1361          // TRANSFORMS
1362  
1363          darken: function( amount ) {
1364              amount = amount || 5;
1365              return this.l( - amount, true );
1366          },
1367  
1368          lighten: function( amount ) {
1369              amount = amount || 5;
1370              return this.l( amount, true );
1371          },
1372  
1373          saturate: function( amount ) {
1374              amount = amount || 15;
1375              return this.s( amount, true );
1376          },
1377  
1378          desaturate: function( amount ) {
1379              amount = amount || 15;
1380              return this.s( - amount, true );
1381          },
1382  
1383          toGrayscale: function() {
1384              return this.setHSpace('hsl').s( 0 );
1385          },
1386  
1387          getComplement: function() {
1388              return this.h( 180, true );
1389          },
1390  
1391          getSplitComplement: function( step ) {
1392              step = step || 1;
1393              var incr = 180 + ( step * 30 );
1394              return this.h( incr, true );
1395          },
1396  
1397          getAnalog: function( step ) {
1398              step = step || 1;
1399              var incr = step * 30;
1400              return this.h( incr, true );
1401          },
1402  
1403          getTetrad: function( step ) {
1404              step = step || 1;
1405              var incr = step * 60;
1406              return this.h( incr, true );
1407          },
1408  
1409          getTriad: function( step ) {
1410              step = step || 1;
1411              var incr = step * 120;
1412              return this.h( incr, true );
1413          },
1414  
1415          _partial: function( key ) {
1416              var prop = shortProps[key];
1417              return function( val, incr ) {
1418                  var color = this._spaceFunc('to', prop.space);
1419  
1420                  // GETTER
1421                  if ( val === undef )
1422                      return color[key];
1423  
1424                  // INCREMENT
1425                  if ( incr === true )
1426                      val = color[key] + val;
1427  
1428                  // MOD & RANGE
1429                  if ( prop.mod )
1430                      val = val % prop.mod;
1431                  if ( prop.range )
1432                      val = ( val < prop.range[0] ) ? prop.range[0] : ( val > prop.range[1] ) ? prop.range[1] : val;
1433  
1434                  // NEW VALUE
1435                  color[key] = val;
1436  
1437                  return this._spaceFunc('from', prop.space, color);
1438              };
1439          },
1440  
1441          _spaceFunc: function( dir, s, val ) {
1442              var space = s || this._hSpace,
1443                  funcName = dir + space.charAt(0).toUpperCase() + space.substr(1);
1444              return this[funcName](val);
1445          }
1446      };
1447  
1448      var shortProps = {
1449          h: {
1450              mod: 360
1451          },
1452          s: {
1453              range: [0,100]
1454          },
1455          l: {
1456              space: 'hsl',
1457              range: [0,100]
1458          },
1459          v: {
1460              space: 'hsv',
1461              range: [0,100]
1462          },
1463          r: {
1464              space: 'rgb',
1465              range: [0,255]
1466          },
1467          g: {
1468              space: 'rgb',
1469              range: [0,255]
1470          },
1471          b: {
1472              space: 'rgb',
1473              range: [0,255]
1474          }
1475      };
1476  
1477      for ( var key in shortProps ) {
1478          if ( shortProps.hasOwnProperty( key ) )
1479              Color.fn[key] = Color.fn._partial(key);
1480      }
1481  
1482      // play nicely with Node + browser
1483      if ( typeof exports === 'object' )
1484          module.exports = Color;
1485      else
1486          global.Color = Color;
1487  
1488  }(this));