chroma-js
Version:
JavaScript library for color conversions
558 lines (449 loc) • 14.2 kB
text/coffeescript
###*
chroma.js
Copyright (c) 2011-2013, Gregor Aisch
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The name Gregor Aisch may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL GREGOR AISCH OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
: https://github.com/gka/chroma.js
###
root = (exports ? this)
chroma = root.chroma ?= {}
# Browserify-compatible export
module.exports = chroma if module?
class Color
constructor: (x,y,z,m) ->
me = @
if not x? and not y? and not z? and not m?
x = [255,0,255]
if type(x) == "array" and x.length == 3
m ?= y
[x,y,z] = x
if type(x) == "string"
m = 'hex'
else
m ?= 'rgb'
if m == 'rgb'
me._rgb = [x,y,z]
else if m == 'hsl'
me._rgb = hsl2rgb x,y,z
else if m == 'hsv'
me._rgb = hsv2rgb x,y,z
else if m == 'hex'
me._rgb = hex2rgb x
else if m == 'lab'
me._rgb = lab2rgb x,y,z
else if m == 'lch'
me._rgb = lch2rgb x,y,z
else if m == 'hsi'
me._rgb = hsi2rgb x,y,z
me_rgb = clip_rgb me._rgb
rgb: ->
hex: ->
rgb2hex
toString: ->
hsl: ->
rgb2hsl
hsv: ->
rgb2hsv
lab: ->
rgb2lab
lch: ->
rgb2lch
hsi: ->
rgb2hsi
luminance: ->
luminance
name: ->
h =
for k of chroma.colors
if h == chroma.colors[k]
return k
h
interpolate: (f, col, m) ->
###
interpolates between colors
###
me = @
m ?= 'rgb'
col = new Color(col) if type(col) == "string"
if m == 'hsl' or m == 'hsv' or m == 'lch' or m == 'hsi'
if m == 'hsl'
xyz0 = me.hsl()
xyz1 = col.hsl()
else if m == 'hsv'
xyz0 = me.hsv()
xyz1 = col.hsv()
else if m == 'hsi'
xyz0 = me.hsi()
xyz1 = col.hsi()
else if m == 'lch'
xyz0 = me.lch()
xyz1 = col.lch()
if m.substr(0, 1) == 'h'
[hue0, sat0, lbv0] = xyz0
[hue1, sat1, lbv1] = xyz1
else
[lbv0, sat0, hue0] = xyz0
[lbv1, sat1, hue1] = xyz1
if not isNaN(hue0) and not isNaN(hue1)
if hue1 > hue0 and hue1 - hue0 > 180
dh = hue1-(hue0+360)
else if hue1 < hue0 and hue0 - hue1 > 180
dh = hue1+360-hue0
else
dh = hue1 - hue0
hue = hue0+f*dh
else if not isNaN(hue0)
hue = hue0
sat = sat0 if lbv1 == 1 or lbv1 == 0
else if not isNaN(hue1)
hue = hue1
sat = sat1 if lbv0 == 1 or lbv0 == 0
else
hue = undefined
sat ?= sat0 + f*(sat1 - sat0)
lbv = lbv0 + f*(lbv1-lbv0)
if m.substr(0, 1) == 'h'
new Color hue, sat, lbv, m
else
new Color lbv, sat, hue, m
else if m == 'rgb'
xyz0 = me._rgb
xyz1 = col._rgb
new Color(xyz0[0]+f*(xyz1[0]-xyz0[0]), xyz0[1] + f*(xyz1[1]-xyz0[1]), xyz0[2] + f*(xyz1[2]-xyz0[2]), m)
else if m == 'lab'
xyz0 = me.lab()
xyz1 = col.lab()
new Color(xyz0[0]+f*(xyz1[0]-xyz0[0]), xyz0[1] + f*(xyz1[1]-xyz0[1]), xyz0[2] + f*(xyz1[2]-xyz0[2]), m)
else
throw "color mode "+m+" is not supported"
darken: (amount=20) ->
me = @
lch = me.lch()
lch[0] -= amount
chroma.lch lch
darker: (amount) ->
amount
brighten: (amount=20) ->
-amount
brighter: (amount) ->
amount
saturate: (amount=20) ->
me = @
lch = me.lch()
lch[1] += amount
chroma.lch lch
desaturate: (amount=20) ->
-amount
hex2rgb = (hex) ->
if hex.match /^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
if hex.length == 4 or hex.length == 7
hex = hex.substr(1)
if hex.length == 3
hex = hex.split("")
hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]
u = parseInt(hex, 16)
r = u >> 16
g = u >> 8 & 0xFF
b = u & 0xFF
return [r,g,b]
# check for css colors, too
if rgb = css2rgb hex
return rgb
throw "unknown color: "+hex
css2rgb = (css) ->
# named X11 colors
if chroma.colors? and chroma.colors[css]
return hex2rgb chroma.colors[css]
# rgb(250,20,0)
if m = css.match /rgb\(\s*(\-?\d+),\s*(\-?\d+)\s*,\s*(\-?\d+)\s*\)/
return m.slice 1,4
# rgb(100%,0%,0%)
if m = css.match /rgb\(\s*(\-?\d+)%,\s*(\-?\d+)%\s*,\s*(\-?\d+)%\s*\)/
rgb = m.slice 1,4
for i of rgb
rgb[i] = Math.round rgb[i] * 2.55
return rgb
if m = css.match /hsl\(\s*(\-?\d+),\s*(\-?\d+)%\s*,\s*(\-?\d+)%\s*\)/
hsl = m.slice 1,4
hsl[1] *= 0.01
hsl[2] *= 0.01
return hsl2rgb hsl
rgb2hex = () ->
[r,g,b] = unpack arguments
u = r << 16 | g << 8 | b
str = "000000" + u.toString(16) #.toUpperCase()
"#" + str.substr(str.length - 6)
hsv2rgb = () ->
[h,s,v] = unpack arguments
v *= 255
if s is 0
r = g = b = v
else
h = 0 if h is 360
h -= 360 if h > 360
h += 360 if h < 0
h /= 60
i = Math.floor h
f = h - i
p = v * (1 - s)
q = v * (1 - s * f)
t = v * (1 - s * (1 - f))
switch i
when 0 then [r,g,b] = [v, t, p]
when 1 then [r,g,b] = [q, v, p]
when 2 then [r,g,b] = [p, v, t]
when 3 then [r,g,b] = [p, q, v]
when 4 then [r,g,b] = [t, p, v]
when 5 then [r,g,b] = [v, p, q]
r = Math.round r
g = Math.round g
b = Math.round b
[r, g, b]
rgb2hsv = () ->
[r,g,b] = unpack arguments
min = Math.min(r, g, b)
max = Math.max(r, g, b)
delta = max - min
v = max / 255.0
if max == 0
h = undefined
s = 0
else
s = delta / max
if r is max then h = (g - b) / delta
if g is max then h = 2+(b - r) / delta
if b is max then h = 4+(r - g) / delta
h *= 60;
if h < 0 then h += 360
[h, s, v]
hsl2rgb = () ->
[h,s,l] = unpack arguments
if s == 0
r = g = b = l*255
else
t3 = [0,0,0]
c = [0,0,0]
t2 = if l < 0.5 then l * (1+s) else l+s-l*s
t1 = 2 * l - t2
h /= 360
t3[0] = h + 1/3
t3[1] = h
t3[2] = h - 1/3
for i in [0..2]
t3[i] += 1 if t3[i] < 0
t3[i] -= 1 if t3[i] > 1
if 6 * t3[i] < 1
c[i] = t1 + (t2 - t1) * 6 * t3[i]
else if 2 * t3[i] < 1
c[i] = t2
else if 3 * t3[i] < 2
c[i] = t1 + (t2 - t1) * ((2 / 3) - t3[i]) * 6
else
c[i] = t1
[r,g,b] = [Math.round(c[0]*255),Math.round(c[1]*255),Math.round(c[2]*255)]
[r,g,b]
rgb2hsl = (r,g,b) ->
if r != undefined and r.length == 3
[r,g,b] = r
r /= 255
g /= 255
b /= 255
min = Math.min(r, g, b)
max = Math.max(r, g, b)
l = (max + min) / 2
if max == min
s = 0
h = undefined
else
s = if l < 0.5 then (max - min) / (max + min) else (max - min) / (2 - max - min)
if r == max then h = (g - b) / (max - min)
else if (g == max) then h = 2 + (b - r) / (max - min)
else if (b == max) then h = 4 + (r - g) / (max - min)
h *= 60;
h += 360 if h < 0
[h,s,l]
# Corresponds roughly to RGB brighter/darker
K = 18
# D65 standard referent
X = 0.950470
Y = 1
Z = 1.088830
lab2rgb = (l,a,b) ->
###
adapted to match d3 implementation
###
if l != undefined and l.length == 3
[l,a,b] = l
if l != undefined and l.length == 3
[l,a,b] = l
y = (l + 16) / 116
x = y + a / 500
z = y - b / 200;
x = lab_xyz(x) * X
y = lab_xyz(y) * Y
z = lab_xyz(z) * Z
r = xyz_rgb( 3.2404542 * x - 1.5371385 * y - 0.4985314 * z)
g = xyz_rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z)
b = xyz_rgb( 0.0556434 * x - 0.2040259 * y + 1.0572252 * z)
[limit(r,0,255), limit(g,0,255), limit(b,0,255)]
rgb2lab = () ->
[r,g,b] = unpack arguments
r = rgb_xyz r
g = rgb_xyz g
b = rgb_xyz b
x = xyz_lab (0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / X
y = xyz_lab (0.2126729 * r + 0.7151522 * g + 0.0721750 * b) / Y
z = xyz_lab (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / Z
[116 * y - 16, 500 * (x - y), 200 * (y - z)]
lch2lab = () ->
###
Convert from a qualitative parameter h and a quantitative parameter l to a 24-bit pixel. These formulas were invented by David Dalrymple to obtain maximum contrast without going out of gamut if the parameters are in the range 0-1.
A saturation multiplier was added by Gregor Aisch
###
[l,c,h] = unpack arguments
h = h * Math.PI / 180
[l, Math.cos(h) * c, Math.sin(h) * c]
lch2rgb = (l,c,h) ->
[L,a,b] = lch2lab l,c,h
[r,g,b] = lab2rgb L,a,b
[limit(r,0,255), limit(g,0,255), limit(b,0,255)]
lab_xyz = (x) ->
if x > 0.206893034 then x * x * x else (x - 4 / 29) / 7.787037
xyz_lab = (x) ->
if x > 0.008856 then Math.pow(x, 1 / 3) else 7.787037 * x + 4 / 29
xyz_rgb = (r) ->
Math.round(255 * (if r <= 0.00304 then 12.92 * r else 1.055 * Math.pow(r, 1 / 2.4) - 0.055))
rgb_xyz = (r) ->
if (r /= 255) <= 0.04045 then r / 12.92 else Math.pow((r + 0.055) / 1.055, 2.4)
lab2lch = () ->
[l, a, b] = unpack arguments
c = Math.sqrt(a * a + b * b)
h = Math.atan2(b, a) / Math.PI * 180
[l, c, h]
rgb2lch = () ->
[r,g,b] = unpack arguments
[l,a,b] = rgb2lab r,g,b
lab2lch l,a,b
rgb2hsi = () ->
###
borrowed from here:
http://hummer.stanford.edu/museinfo/doc/examples/humdrum/keyscape2/rgb2hsi.cpp
###
[r,g,b] = unpack arguments
TWOPI = Math.PI*2
r /= 255
g /= 255
b /= 255
min = Math.min(r,g,b)
i = (r+g+b) / 3
s = 1 - min/i
if s == 0
h = 0
else
h = ((r-g)+(r-b)) / 2
h /= Math.sqrt((r-g)*(r-g) + (r-b)*(g-b))
h = Math.acos(h)
if b > g
h = TWOPI - h
h /= TWOPI
[h*360,s,i]
hsi2rgb = (h,s,i) ->
###
borrowed from here:
http://hummer.stanford.edu/museinfo/doc/examples/humdrum/keyscape2/hsi2rgb.cpp
###
[h,s,i] = unpack arguments
# normalize hue
#h += 360 if h < 0
#h -= 360 if h > 360
h /= 360
if h < 1/3
b = (1-s)/3
r = (1+s*cos(TWOPI*h)/cos(PITHIRD-TWOPI*h))/3
g = 1 - (b+r)
else if h < 2/3
h -= 1/3
r = (1-s)/3
g = (1+s*cos(TWOPI*h)/cos(PITHIRD-TWOPI*h))/3
b = 1 - (r+g)
else
h -= 2/3
g = (1-s)/3
b = (1+s*cos(TWOPI*h)/cos(PITHIRD-TWOPI*h))/3
r = 1 - (g+b)
r = limit i*r*3
g = limit i*g*3
b = limit i*b*3
[r*255,g*255,b*255]
clip_rgb = (rgb) ->
for i of rgb
rgb[i] = 0 if rgb[i] < 0
rgb[i] = 255 if rgb[i] > 255
rgb
luminance = (r,g,b) ->
# relative luminance
# see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
[r,g,b] = unpack arguments
r = luminance_x r
g = luminance_x g
b = luminance_x b
0.2126 * r + 0.7152 * g + 0.0722 * b
luminance_x = (x) ->
x /= 255
if x <= 0.03928 then x/12.92 else Math.pow((x+0.055)/1.055, 2.4)
chroma.Color = Color
#
# static constructors
#
chroma.color = (x,y,z,m) ->
new Color x,y,z,m
chroma.hsl = (h,s,l) ->
new Color h,s,l,'hsl'
chroma.hsv = (h,s,v) ->
new Color h,s,v,'hsv'
chroma.rgb = (r,g,b) ->
new Color r,g,b,'rgb'
chroma.hex = (x) ->
new Color x
chroma.css = (x) ->
new Color x
chroma.lab = (l,a,b) ->
new Color l,a,b,'lab'
chroma.lch = (l,c,h) ->
new Color l,c,h, 'lch'
chroma.hsi = (h,s,i) ->
new Color h,s,i,'hsi'
chroma.interpolate = (a,b,f,m) ->
if not a? or not b?
return '#000'
a = new Color a if type(a) == 'string'
b = new Color b if type(b) == 'string'
a.interpolate f,b,m
chroma.contrast = (a, b) ->
# WCAG contrast ratio
# see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
a = new Color a if type(a) == 'string'
b = new Color b if type(b) == 'string'
l1 = a.luminance()
l2 = b.luminance()
if l1 > l2 then (l1 + 0.05) / (l2 + 0.05) else (l2 + 0.05) / (l1 + 0.05)