Skip to content

Instantly share code, notes, and snippets.

@neolitec
Created November 7, 2011 10:19
Show Gist options
  • Save neolitec/1344610 to your computer and use it in GitHub Desktop.
Save neolitec/1344610 to your computer and use it in GitHub Desktop.
Javascript Color Class
var Color = function() {
this.r = this.g = this.b = 0;
this.h = this.s = this.l = 0;
this.a = 1;
};
/** RGB */
Color.prototype.cssRGB = function() {
return "rgb("+Math.round(255*this.r)+","+Math.round(255*this.g)+","+Math.round(255*this.b)+")";
};
Color.prototype.cssRGBA = function() {
return "rgba("+Math.round(255*this.r)+","+Math.round(255*this.g)+","+Math.round(255*this.b)+","+Math.round(this.a)+")";
};
Color.prototype.red = function() { return this.r; };
Color.prototype.green = function() { return this.g; };
Color.prototype.blue = function() { return this.b; };
/** HSL */
Color.prototype.cssHSL = function() {
return "hsl("+Math.round(360*this.h)+","+Math.round(100*this.s)+"%,"+Math.round(100*this.l)+"%)";
};
Color.prototype.cssHSLA = function() {
return "hsla("+Math.round(360*this.h)+","+Math.round(100*this.s)+"%,"+Math.round(100*this.l)+"%,"+Math.round(this.a)+")";
};
Color.prototype.hue = function() { return this.h; };
Color.prototype.saturation = function() { return this.s; };
Color.prototype.lightness = function() { return this.l; };
/** HEX */
Color.prototype.cssHEX = function() {
return "#" +
(255*this.r < 16 ? "0" : "") + Math.round(255*this.r).toString(16) +
(255*this.g < 16 ? "0" : "") + Math.round(255*this.g).toString(16) +
(255*this.b < 16 ? "0" : "") + Math.round(255*this.b).toString(16);
}
/** Transparency */
Color.prototype.alpha = function() { return this.a; };
/** Modifiers */
Color.prototype.saturate = function(v) {
if("string" == typeof v && v.indexOf("%") > -1 && (v = parseInt(v)) != 'NaN')
this.s += v/100;
else if("number" == typeof v) // range 255
this.s += v/255;
else throw new Error("error: bad modifier format (percent or number)");
if(this.s > 1) this.s = 1; else if(this.s < 0) this.s = 0;
Color.Convertor.HSLToRGB.apply(this);
};
Color.prototype.desaturate = function(v) {
this.saturate("-" + v);
};
Color.prototype.lighten = function(v) {
if("string" == typeof v && v.indexOf("%") > -1 && (v = parseInt(v)) != 'NaN')
this.l += v/100;
else if("number" == typeof v) // range 255
this.l += v/255;
else throw new Error("error: bad modifier format (percent or number)");
if(this.l > 1) this.l = 1; else if(this.l < 0) this.l = 0;
Color.Convertor.HSLToRGB.apply(this);
};
Color.prototype.darken = function(v) {
this.ligthen("-" + v);
};
Color.prototype.fadein = function(v) {
if("string" == typeof v && v.indexOf("%") > -1 && (v = parseInt(v)) != 'NaN')
this.a += v/100;
else if("number" == typeof v) // range 255
this.a += v/255;
else throw new Error("error: bad modifier format (percent or number)");
if(this.a > 1) this.a = 1; else if(this.a < 0) this.a = 0;
Color.Convertor.HSLToRGB.apply(this);
};
Color.prototype.fadeout = function(v) {
this.fadein("-" + v);
};
Color.prototype.spin = function(v) {
if("string" == typeof v && v.indexOf("%") > -1 && (v = parseInt(v)) != 'NaN')
this.h += v/100;
else if("number" == typeof v) // range 360
this.h += v/360;
else throw new Error("error: bad modifier format (percent or number)");
if(this.h > 1) this.h = 1; else if(this.h < 0) this.h = 0;
Color.Convertor.HSLToRGB.apply(this);
};
/** Debug */
Color.prototype.toString = function() {
return "<span style=\"color: "+this.cssRGB()+"\">"+this.cssRGB()+"</span> / <span style=\"color: "+this.cssHSL()+"\">"+this.cssHSL()+"</span> / <span style=\"color: "+this.cssHEX()+"\">"+this.cssHEX()+"</span> / alpha: "+this.a+"";
};
Color.makeRGB = function() {
var c = new Color(),
sanitized;
if(arguments.length < 3 || arguments.length > 4)
throw new Error("error: 3 or 4 arguments");
sanitized = Color.Sanitizer.RGB(arguments[0], arguments[1], arguments[2]);
c.r = sanitized[0];
c.g = sanitized[1];
c.b = sanitized[2];
if(arguments.length == 4) c.a = arguments[3];
Color.Convertor.RGBToHSL.apply(c);
return c;
};
Color.makeHSL = function() {
var c = new Color(),
sanitized;
if(arguments.length < 3 || arguments.length > 4)
throw new Error("error: 3 or 4 arguments");
sanitized = Color.Sanitizer.HSL(arguments[0], arguments[1], arguments[2]);
c.h = sanitized[0];
c.s = sanitized[1];
c.l = sanitized[2];
if(arguments.length == 4) c.a = arguments[3];
Color.Convertor.HSLToRGB.apply(c);
return c;
};
Color.makeHEX = function(value) {
var c = new Color(),
sanitized;
Color.Validator.checkHEX(value);
if(value.length == 3) {
sanitized = Color.Sanitizer.RGB(
parseInt(value.substr(0, 1) + value.substr(0, 1), 16),
parseInt(value.substr(1, 1) + value.substr(1, 1), 16),
parseInt(value.substr(2, 1) + value.substr(2, 1), 16)
);
} else if(value.length == 6) {
sanitized = Color.Sanitizer.RGB(
parseInt(value.substr(0, 2), 16),
parseInt(value.substr(2, 2), 16),
parseInt(value.substr(4, 2), 16)
);
} else throw new Error("error: 3 or 6 arguments");
c.r = sanitized[0];
c.g = sanitized[1];
c.b = sanitized[2];
Color.Convertor.RGBToHSL.apply(c);
return c;
};
Color.Sanitizer = {
RGB: function() {
var o = [];
if(arguments.length == 0) return;
for(var i = 0 ; i < arguments.length ; i++) {
var c = arguments[i];
if("string" == typeof c && c.indexOf("%") > -1) {
if((c = parseInt(c)) == 'NaN')
throw new Error("Bad format");
if(c < 0 || c > 100)
throw new Error("Bad format");
o[i] = c/100;
} else {
if("string" == typeof c && (c = parseInt(c)) == 'NaN') throw new Error("Bad format");
if(c < 0) throw new Error("Bad format");
else if(c >= 0 && c < 1) o[i] = c;
else if(c >= 1 && c < 256) o[i] = c/255;
else throw new Error("Bad format");
}
}
return o;
},
HSL: function() {
if(arguments.length < 3 || arguments.length > 4) throw new Error("3 or 4 arguments required");
var h = arguments[0],
s = arguments[1],
l = arguments[2];
if("string" == typeof h && (h = parseFloat(h)) == 'NaN') throw new Error("Bad format for hue");
if(h < 0 || h > 360) throw new Error("Hue out of range (0..360)");
else if(((""+h).indexOf(".") > -1 && h > 1) || (""+h).indexOf(".") == -1) h /= 360;
if("string" == typeof s && s.indexOf("%") > -1) {
if((s = parseInt(s)) == 'NaN')
throw new Error("Bad format for saturation");
if(s < 0 || s > 100)
throw new Error("Bad format for saturation");
s /= 100;
} else if(s < 0 || s > 1) throw new Error("Bad format for saturation");
if("string" == typeof l && l.indexOf("%") > -1) {
if((l = parseInt(l)) == 'NaN')
throw new Error("Bad format for lightness");
if(l < 0 || l > 100)
throw new Error("Bad format for lightness");
l /= 100;
} else if(l < 0 || l > 1) throw new Error("Bad format for lightness");
return [h, s, l];
},
};
Color.Validator = {
/**
* Check a hexa color (without #)
*/
checkHEX: function(value) {
if(value.length != 6)
throw new Error("Hexa color: bad length");
value = value.toLowerCase();
for(i in value) {
var c = value.charCodeAt(i);
if( !((c>=49 && c<=57) || (c>=97 && c<=102)) )
throw new Error("Hexa color: out of range for " + value + " at position " + i);
}
},
};
Color.Convertor = {
/**
* Calculates HSL Color
* RGB must be normalized
* Must be executed in a Color object context
* http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
*/
RGBToHSL: function() {
//
var r = this.r,
g = this.g,
b = this.b,
max = Math.max(r, g, b), min = Math.min(r, g, b);
this.l = (max + min) / 2;
if(max == min){
this.h = this.s = 0; // achromatic
} else {
var d = max - min;
this.s = this.l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: this.h = (g - b) / d + (g < b ? 6 : 0); break;
case g: this.h = (b - r) / d + 2; break;
case b: this.h = (r - g) / d + 4; break;
}
this.h /= 6;
}
},
/**
* Calculates RGB color (nomalized)
* HSL must be normalized
* Must be executed in a Color object context
* http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
*/
HSLToRGB: function() {
var h = this.h,
s = this.s,
l = this.l,
hue2rgb = function(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
if(s == 0) {
this.r = this.g = this.b = l; // achromatic
} else {
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
this.r = hue2rgb(p, q, h + 1/3);
this.g = hue2rgb(p, q, h);
this.b = hue2rgb(p, q, h - 1/3);
}
},
};
@mattlundstrom
Copy link

Color.Validator.checkHEX throws an error with certain values that should work, like "000000", otherwise awesome class!

@IkarosKappler
Copy link

Changing the 49 in the checkHEX function to a 48 resolves the issue:

    /**
     * Check a hexa color (without #)
     */
    checkHEX: function(value) {
        if(value.length != 6)
            throw new Error("Hexa color: bad length");
        value = value.toLowerCase();
        for(i in value) {
            var c = value.charCodeAt(i);
            if( !((c>=48 && c<=57) || (c>=97 && c<=102)) )
                throw new Error("Hexa color: out of range for " + value + " at position " + i);
        }
    }

A great class by the way!

@IkarosKappler
Copy link

IkarosKappler commented Oct 23, 2020

Ported your class to TypeScript. Not really tested.

/**
 * @author Extended, bugfixed and ported to TypeScript by Ikaros Kappler.
 * @modified 2018-xx-xx Added a clone() function.
 * @modified 2018-xx-xx Allowing leading '#' in the makeHEX() function.
 * @modified 2018-11-28 Fixed the checkHEX() function to accept 000000.
 * @modified 2019-11-18 Added a generic parse(string) function that detects the format.
 * @modified 2020-01-09 Fixed a bug in the parse(string) function. Hex colors with only three elements were considered faulty.
 * @modified 2020-10-23 Ported to Typescript.
 * @modified 2021-02-08 Fixed a lot of es2015 compatibility issues.
 * @modified 2021-02-08 Added basic tsdoc/jsdoc comments.
 * @modified 2021-11-05 Fixing the regex to parse rgba-strings.
 * @version 0.0.10
 **/

/**
 * @classdesc A color class, inspired by neolitec's Javascript class.
 *    Original found at
 *      https://gist.github.com/neolitec/1344610
 *    Thanks to neolitec
 */
export class Color {
  /**
   * @member {number}
   * @memberof Color
   * @instance
   */
  r: number;

  /**
   * @member {number}
   * @memberof Color
   * @instance
   */
  g: number;

  /**
   * @member {number}
   * @memberof Color
   * @instance
   */
  b: number;

  /**
   * @member {number}
   * @memberof Color
   * @instance
   */
  h: number;

  /**
   * @member {number}
   * @memberof Color
   * @instance
   */
  s: number;

  /**
   * @member {number}
   * @memberof Color
   * @instance
   */
  l: number;

  /**
   * @member {number}
   * @memberof Color
   * @instance
   */
  a: number;

  /**
   * Construct a new color with `r=0 g=0 b=0`.
   *
   * Consider using the `makeHex`, `makeRGB` or `makeHSL` functions.
   *
   * @constructor
   * @instance
   * @memberof Color
   */
  constructor() {
    this.r = this.g = this.b = 0;
    this.h = this.s = this.l = 0;
    this.a = 1;
  }

  // --- RGB ----------------------------------
  /**
   * Get this color as a CSS `rgb` string.
   *
   * Consult this for details: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
   *
   * @method cssRGB
   * @instance
   * @memberof Color
   * @return {string} This color as a CSS rgb string.
   */
  cssRGB(): string {
    return "rgb(" + Math.round(255 * this.r) + "," + Math.round(255 * this.g) + "," + Math.round(255 * this.b) + ")";
  }

  /**
   * Get this color as a CSS `rgba` string.
   *
   * Consult this for details: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
   *
   * @method cssRGBA
   * @instance
   * @memberof Color
   * @return {string} This color as a CSS rgba string.
   */
  cssRGBA(): string {
    return (
      "rgba(" + Math.round(255 * this.r) + "," + Math.round(255 * this.g) + "," + Math.round(255 * this.b) + "," + this.a + ")"
    );
  }

  /**
   * Get the red component of this RGB(A)color. This method just returns the `r` color attribute.
   *
   * @method red
   * @instance
   * @memberof Color
   * @return {number} A value between 0.0 and 1.0.
   */
  red(): number {
    return this.r;
  }

  /**
   * Get the green component of this RGB(A) color. This method just returns the `g` color attribute.
   *
   * @method green
   * @instance
   * @memberof Color
   * @return {number} A value between 0.0 and 1.0.
   */
  green(): number {
    return this.g;
  }

  /**
   * Get the blue component of this RGB(A) color. This method just returns the `b` color attribute.
   *
   * @method blue
   * @instance
   * @memberof Color
   * @return {number} A value between 0.0 and 1.0.
   */
  blue(): number {
    return this.b;
  }

  // --- HSL ----------------------------------
  /**
   * Get this color as a CSS `hsl` string.
   *
   * @method cssHSL
   * @instance
   * @memberof Color
   * @return {string} This color as a CSS hsl string.
   */
  cssHSL(): string {
    return "hsl(" + Math.round(360 * this.h) + "," + Math.round(100 * this.s) + "%," + Math.round(100 * this.l) + "%)";
  }

  /**
   * Get this color as a CSS `hsla` string.
   *
   * @method cssHSLA
   * @instance
   * @memberof Color
   * @return {string} This color as a CSS hsla string.
   */
  cssHSLA(): string {
    return (
      "hsla(" +
      Math.round(360 * this.h) +
      "," +
      Math.round(100 * this.s) +
      "%," +
      Math.round(100 * this.l) +
      "%," +
      Math.round(this.a) +
      ")"
    );
  }

  /**
   * Get the hue component of this HSL(A) color. This method just returns the `h` color attribute.
   *
   * @method hue
   * @instance
   * @memberof Color
   * @return {number} A value between 0.0 and 1.0.
   */
  hue(): number {
    return this.h;
  }

  /**
   * Get the saturation component of this HSL(A) color. This method just returns the `s` color attribute.
   *
   * @method saturation
   * @instance
   * @memberof Color
   * @return {number} A value between 0.0 and 1.0.
   */
  saturation(): number {
    return this.s;
  }

  /**
   * Get the lightness component of this HSL(A) color. This method just returns the `l` color attribute.
   *
   * @method lightness
   * @instance
   * @memberof Color
   * @return {number} A value between 0.0 and 1.0.
   */
  lightness(): number {
    return this.l;
  }

  // --- HEX ----------------------------------
  /**
   * Get this color as a CSS-HEX string (non-alpha): #rrggbb
   *
   * @method cssHEX
   * @instance
   * @memberof Color
   * @return {string} This color as a CSS-HEX string.
   */
  cssHEX(): string {
    return (
      "#" +
      (255 * this.r < 16 ? "0" : "") +
      Math.round(255 * this.r).toString(16) +
      (255 * this.g < 16 ? "0" : "") +
      Math.round(255 * this.g).toString(16) +
      (255 * this.b < 16 ? "0" : "") +
      Math.round(255 * this.b).toString(16)
    );
  }

  // --- Transparency ----------------------------------

  /**
   * Get the alpha channel (transparency) of this color.
   *
   * @method alpha
   * @instance
   * @memberof Color
   * @return {number} A value between 0.0 and 1.0.
   */
  alpha(): number {
    return this.a;
  }

  // --- Modifiers ----------------------------------

  saturate(v: string | number): void {
    if ("string" == typeof v && v.indexOf("%") > -1 && (v = parseInt(v)) != NaN) this.s += v / 100;
    else if ("number" == typeof v)
      // range 255
      this.s += v / 255;
    else throw new Error("error: bad modifier format (percent or number)");
    if (this.s > 1) this.s = 1;
    else if (this.s < 0) this.s = 0;
    Color.Converter.HSLToRGB(this);
  }
  desaturate(v: string | number): void {
    this.saturate("-" + v);
  }
  lighten(v: string | number): void {
    if ("string" == typeof v && v.indexOf("%") > -1 && (v = parseInt(v)) != NaN) this.l += v / 100;
    else if ("number" == typeof v)
      // range 255
      this.l += v / 255;
    else throw new Error("error: bad modifier format (percent or number)");
    if (this.l > 1) this.l = 1;
    else if (this.l < 0) this.l = 0;
    Color.Converter.HSLToRGB(this);
  }
  darken(v: string | number): void {
    this.lighten("-" + v);
  }
  fadein(v: string | number): void {
    if ("string" == typeof v && v.indexOf("%") > -1 && (v = parseInt(v)) != NaN) this.a += v / 100;
    else if ("number" == typeof v)
      // range 255
      this.a += v / 255;
    else throw new Error("error: bad modifier format (percent or number)");
    if (this.a > 1) this.a = 1;
    else if (this.a < 0) this.a = 0;
    Color.Converter.HSLToRGB(this);
  }
  fadeout(v: string | number): void {
    this.fadein("-" + v);
  }
  spin(v: string | number): void {
    if ("string" == typeof v && v.indexOf("%") > -1 && (v = parseInt(v)) != NaN) this.h += v / 100;
    else if ("number" == typeof v)
      // range 360
      this.h += v / 360;
    else throw new Error("error: bad modifier format (percent or number)");
    if (this.h > 1) this.h = 1;
    else if (this.h < 0) this.h = 0;
    Color.Converter.HSLToRGB(this);
  }

  static makeRGB(...args: any[]): Color {
    const c: Color = new Color();
    let sanitized: Array<number>;
    if (arguments.length < 3 || arguments.length > 4) throw new Error("error: 3 or 4 arguments");
    sanitized = Color.Sanitizer.RGB(arguments[0], arguments[1], arguments[2]);
    c.r = sanitized[0];
    c.g = sanitized[1];
    c.b = sanitized[2];
    if (arguments.length == 4) c.a = arguments[3];
    Color.Converter.RGBToHSL(c);
    return c;
  }

  static makeHSL(...args: Array<number | string>): Color {
    const c: Color = new Color();
    let sanitized: Array<number>;
    if (arguments.length < 3 || arguments.length > 4) throw new Error("error: 3 or 4 arguments");
    sanitized = Color.Sanitizer.HSL(arguments[0], arguments[1], arguments[2]);
    c.h = sanitized[0];
    c.s = sanitized[1];
    c.l = sanitized[2];
    if (arguments.length == 4) c.a = arguments[3];
    Color.Converter.HSLToRGB(c);
    return c;
  }

  static makeHEX(value: string): Color {
    var c = new Color(),
      sanitized;
    // Edit Ika 2018-0308
    // Allow leading '#'
    if (value && value.startsWith("#")) value = value.substr(1);
    Color.Validator.checkHEX(value);
    if (value.length == 3) {
      sanitized = Color.Sanitizer.RGB(
        parseInt(value.substr(0, 1) + value.substr(0, 1), 16),
        parseInt(value.substr(1, 1) + value.substr(1, 1), 16),
        parseInt(value.substr(2, 1) + value.substr(2, 1), 16)
      );
    } else if (value.length == 6) {
      sanitized = Color.Sanitizer.RGB(
        parseInt(value.substr(0, 2), 16),
        parseInt(value.substr(2, 2), 16),
        parseInt(value.substr(4, 2), 16)
      );
    } else throw new Error("error: 3 or 6 arguments");
    c.r = sanitized[0];
    c.g = sanitized[1];
    c.b = sanitized[2];
    Color.Converter.RGBToHSL(c);
    return c;
  }

  /**
   * Parse the given color string. Currently only these formate are recognized: hex, rgb, rgba.
   *
   * @method parse
   * @static
   * @memberof Color
   * @param {string} str - The string representation to parse.
   * @return {Color} The color instance that's represented by the given string.
   */
  static parse(str: string): Color {
    if (typeof str == "undefined") return null;
    if ((str = str.trim().toLowerCase()).length == 0) return null;
    if (str.startsWith("#")) return Color.makeHEX(str.substring(1, str.length));
    if (str.startsWith("rgb")) {
      var parts = str.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,?\s*(\d*(?:\.\d+\s*)?)\)$/);
      if (!parts) {
        throw "Unrecognized color format (2): " + str;
      }
      // [ str, r, g, b, a|undefined ]
      if (typeof parts[4] == "undefined") return Color.makeRGB(parts[1], parts[2], parts[3]);
      else return Color.makeRGB(parts[1], parts[2], parts[3], parts[4]);
    } else {
      throw "Unrecognized color format (1): " + str;
    }
  }

  private static Sanitizer = {
    RGB: function (...args: any[]) {
      var o = [];
      if (arguments.length == 0) {
        return [];
      }
      // const allAreFrac = Color.testFrac( arguments );
      for (var i = 0; i < arguments.length; i++) {
        var c = arguments[i];
        if ("string" == typeof c && c.indexOf("%") > -1) {
          if ((c = parseInt(c)) == NaN) throw new Error("Bad format");
          if (c < 0 || c > 100) throw new Error("Bad format");
          o[i] = c / 100;
        } else {
          // console.log( 'allAreFrac', allAreFrac, arguments );
          if ("string" == typeof c && (c = parseInt(c)) == NaN) throw new Error("Bad format");
          if (c < 0) throw new Error("Bad format");
          //else if( allAreFrac ) o[i] = c; // c >= 0 && c <= 1 (all)
          else if (c >= 0 && c < 1) o[i] = c;
          // else if(c >= 0.0 && c <= 1.0) o[i] = c;
          else if (c >= 1 && c < 256) o[i] = c / 255;
          // ???
          // else if(c >= 0 && c < 256) o[i] = c/255;
          else throw new Error("Bad format (" + c + ")");
        }
      }
      return o;
    },

    HSL: function (...args: Array<string | number>): Array<number> {
      if (arguments.length < 3 || arguments.length > 4) throw new Error("3 or 4 arguments required");
      var h = arguments[0],
        s = arguments[1],
        l = arguments[2];
      if ("string" == typeof h && (h = parseFloat(h)) == NaN) throw new Error("Bad format for hue");
      if (h < 0 || h > 360) throw new Error("Hue out of range (0..360)");
      else if ((("" + h).indexOf(".") > -1 && h > 1) || ("" + h).indexOf(".") == -1) h /= 360;
      if ("string" == typeof s && s.indexOf("%") > -1) {
        if ((s = parseInt(s)) == NaN) throw new Error("Bad format for saturation");
        if (s < 0 || s > 100) throw new Error("Bad format for saturation");
        s /= 100;
      } else if (s < 0 || s > 1) throw new Error("Bad format for saturation");
      if ("string" == typeof l && l.indexOf("%") > -1) {
        if ((l = parseInt(l)) == NaN) throw new Error("Bad format for lightness");
        if (l < 0 || l > 100) throw new Error("Bad format for lightness");
        l /= 100;
      } else if (l < 0 || l > 1) throw new Error("Bad format for lightness");
      return [h, s, l];
    }
  }; // ENd sanitizer

  private static Validator = {
    /**
     * Check a hexa color (without #)
     */
    checkHEX: (value: string) => {
      if (value.length != 6 && value.length != 3) throw new Error("Hexa color: bad length (" + value.length + ")," + value);
      value = value.toLowerCase();
      //for( var i in value ) {
      for (var i = 0; i < value.length; i++) {
        var c: number = value.charCodeAt(i);
        if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102)))
          throw new Error(`Hexa color: out of range for "${value}" at position ${i}.`);
      }
    }
  };

  private static Converter = {
    /**
     * Calculates HSL Color.
     * RGB must be normalized.
     * http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
     */
    RGBToHSL: (color: Color) => {
      var r: number = color.r;
      var g: number = color.g;
      var b: number = color.b;
      var max: number = Math.max(r, g, b);
      var min: number = Math.min(r, g, b);
      color.l = (max + min) / 2;
      if (max == min) {
        color.h = color.s = 0; // achromatic
      } else {
        var d: number = max - min;
        color.s = color.l > 0.5 ? d / (2 - max - min) : d / (max + min);
        switch (max) {
          case r:
            color.h = (g - b) / d + (g < b ? 6 : 0);
            break;
          case g:
            color.h = (b - r) / d + 2;
            break;
          case b:
            color.h = (r - g) / d + 4;
            break;
        }
        color.h /= 6;
      }
    },

    /**
     * Calculates RGB color (nomalized).
     * HSL must be normalized.
     *
     * http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
     */
    HSLToRGB: (color: Color) => {
      var h: number = color.h;
      var s: number = color.s;
      var l: number = color.l;
      if (s == 0) {
        color.r = color.g = color.b = l; // achromatic
      } else {
        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        color.r = Color.Converter.hue2rgb(p, q, h + 1 / 3);
        color.g = Color.Converter.hue2rgb(p, q, h);
        color.b = Color.Converter.hue2rgb(p, q, h - 1 / 3);
      }
    },

    hue2rgb: (p: number, q: number, t: number): number => {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    }
  };

  /**
   * Create a clone of this color (RGB).
   *
   * @method clone
   * @instance
   * @memberof Color
   * @return {Color} A clone of this color (in RGB mode).
   */
  clone(): Color {
    return Color.makeRGB(this.r, this.g, this.b, this.a);
  }

  /**
   * Interpolate this color on the RGB scale.
   *
   * @method interpolate
   * @instance
   * @memberof Color
   * @param {Color} c - The color to interpolate to.
   * @param {number} t - An interpolation value between 0.0 and 1.0.
   * @return {Color} A clone of this color (in RGB mode).
   */
  interpolate(c: Color, t: number): Color {
    this.r += (c.r - c.r) * t;
    this.g += (c.g - c.g) * t;
    this.b += (c.b - c.b) * t;
    this.a += (c.a - c.a) * t;
    return this;
  }
} // END class

@IkarosKappler
Copy link

@theking2
Copy link

Is there a canonical way to instantiate and compare color values?

@IkarosKappler
Copy link

In my experience different people have different preferences. Trying an OO approach with mutable values here. In most cases the color is parsed by some input or config string anyway, but feel free to modify the constructor.

And comparing colors ... sounds very fiddly.

@neolitec
Copy link
Author

neolitec commented Sep 10, 2024

Wow, I'm glad this can still be useful after several years 😁
@IkarosKappler do you still use it in your projects or did you change for another library (chroma.js, ...)? I'm curious weather publishing a small npm package could be interesting or not...

@IkarosKappler
Copy link

Hi @neolitec , your color class was exactly what I was looking for some years ago: simple and small. I am still using it in a fun project and adapted it a bit to my needs.
https://github.com/IkarosKappler/plotboilerplate/blob/main/src/ts/utils/datastructures/Color.ts

I like the idea of publishing an npm package. Go ahead :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment