File: husl.coffee

package info (click to toggle)
node-husl 6.0.1%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 7,820 kB
  • sloc: java: 612; ansic: 285; makefile: 13; sh: 2
file content (317 lines) | stat: -rw-r--r-- 8,546 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# The math for most of this module was taken from:
#
#  * http://www.easyrgb.com
#  * http://www.brucelindbloom.com
#  * Wikipedia
#
# All numbers below taken from math/bounds.wxm wxMaxima file. We use 17
# digits of decimal precision to export the numbers, effectively exporting
# them as double precision IEEE 754 floats.
#
# "If an IEEE 754 double precision is converted to a decimal string with at
# least 17 significant digits and then converted back to double, then the 
# final number must match the original"
#
# Source: https://en.wikipedia.org/wiki/Double-precision_floating-point_format

m =
  R: [  3.2409699419045214,   -1.5373831775700935, -0.49861076029300328  ]
  G: [ -0.96924363628087983,   1.8759675015077207,  0.041555057407175613 ]
  B: [  0.055630079696993609, -0.20397695888897657, 1.0569715142428786   ]
m_inv =
  X: [ 0.41239079926595948,  0.35758433938387796, 0.18048078840183429  ]
  Y: [ 0.21263900587151036,  0.71516867876775593, 0.072192315360733715 ]
  Z: [ 0.019330818715591851, 0.11919477979462599, 0.95053215224966058  ]

refU = 0.19783000664283681
refV = 0.468319994938791

# CIE LUV constants
kappa = 903.2962962962963
epsilon = 0.0088564516790356308

# For a given lightness, return a list of 6 lines in slope-intercept
# form that represent the bounds in CIELUV, stepping over which will
# push a value out of the RGB gamut
getBounds = (L) ->
  sub1 = Math.pow(L + 16, 3) / 1560896
  sub2 = if (sub1 > epsilon) then sub1 else (L / kappa)
  ret = []
  for channel in ['R', 'G', 'B']
    [m1, m2, m3] = m[channel]
    for t in [0, 1]

      top1 = (284517 * m1 - 94839 * m3) * sub2
      top2 = (838422 * m3 + 769860 * m2 + 731718 * m1) * L * sub2 - 769860 * t * L
      bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t

      ret.push [top1 / bottom, top2 / bottom]
  return ret


intersectLineLine = (line1, line2) ->
  (line1[1] - line2[1]) / (line2[0] - line1[0])

distanceFromPole = (point) ->
  Math.sqrt(Math.pow(point[0], 2) + Math.pow(point[1], 2))


lengthOfRayUntilIntersect = (theta, line) ->
  # theta  -- angle of ray starting at (0, 0)
  # m, b   -- slope and intercept of line
  # x1, y1 -- coordinates of intersection
  # len    -- length of ray until it intersects with line
  #
  # b + m * x1        = y1
  # len              >= 0
  # len * cos(theta)  = x1
  # len * sin(theta)  = y1
  #
  #
  # b + m * (len * cos(theta)) = len * sin(theta)
  # b = len * sin(hrad) - m * len * cos(theta)
  # b = len * (sin(hrad) - m * cos(hrad))
  # len = b / (sin(hrad) - m * cos(hrad))
  #
  [m1, b1] = line
  len = b1 / (Math.sin(theta) - m1 * Math.cos(theta))
  if len < 0
    return null
  return len


# For given lightness, returns the maximum chroma. Keeping the chroma value
# below this number will ensure that for any hue, the color is within the RGB
# gamut.
maxSafeChromaForL = (L) ->
  lengths = []
  for [m1, b1] in getBounds L
    # x where line intersects with perpendicular running though (0, 0)
    x = intersectLineLine [m1, b1], [-1 / m1, 0]
    lengths.push distanceFromPole [x, b1 + x * m1]
  return Math.min lengths...

# For a given lightness and hue, return the maximum chroma that fits in
# the RGB gamut.
maxChromaForLH = (L, H) ->
  hrad = H / 360 * Math.PI * 2
  lengths = []
  for line in getBounds L
    l = lengthOfRayUntilIntersect hrad, line
    if l != null
      lengths.push l
  return Math.min lengths...

dotProduct = (a, b) ->
  ret = 0
  for i in [0..a.length-1]
    ret += a[i] * b[i]
  return ret

# Used for rgb conversions
fromLinear = (c) ->
  if c <= 0.0031308
    12.92 * c
  else
    1.055 * Math.pow(c, 1 / 2.4) - 0.055

toLinear = (c) ->
  a = 0.055
  if c > 0.04045
    Math.pow (c + a) / (1 + a), 2.4
  else
    c / 12.92

# This map will contain our conversion functions
conv =
  'xyz': {}
  'luv': {}
  'lch': {}
  'husl': {}
  'huslp': {}
  'rgb': {}
  'hex': {}

conv.xyz.rgb = (tuple) ->
  R = fromLinear dotProduct m.R, tuple
  G = fromLinear dotProduct m.G, tuple
  B = fromLinear dotProduct m.B, tuple
  return [R, G, B]

conv.rgb.xyz = (tuple) ->
  [R, G, B] = tuple
  rgbl = [toLinear(R), toLinear(G), toLinear(B)]
  X = dotProduct m_inv.X, rgbl
  Y = dotProduct m_inv.Y, rgbl
  Z = dotProduct m_inv.Z, rgbl
  [X, Y, Z]

# http://en.wikipedia.org/wiki/CIELUV
# In these formulas, Yn refers to the reference white point. We are using
# illuminant D65, so Yn (see refY in Maxima file) equals 1. The formula is
# simplified accordingly.
Y_to_L = (Y) ->
  if Y <= epsilon
    Y * kappa
  else
    116 * Math.pow(Y, 1/3) - 16
L_to_Y = (L) ->
  if L <= 8
    L / kappa
  else
    Math.pow((L + 16) / 116, 3)

conv.xyz.luv = (tuple) ->
  [X, Y, Z] = tuple
  # Black will create a divide-by-zero error in
  # the following two lines
  if Y is 0
    return [0, 0, 0]
  L = Y_to_L(Y)
  varU = (4 * X) / (X + (15 * Y) + (3 * Z))
  varV = (9 * Y) / (X + (15 * Y) + (3 * Z))
  U = 13 * L * (varU - refU)
  V = 13 * L * (varV - refV)
  [L, U, V]

conv.luv.xyz = (tuple) ->
  [L, U, V] = tuple
  # Black will create a divide-by-zero error
  if L is 0
    return [0, 0, 0]
  varU = U / (13 * L) + refU
  varV = V / (13 * L) + refV
  Y = L_to_Y(L)
  X = 0 - (9 * Y * varU) / ((varU - 4) * varV - varU * varV)
  Z = (9 * Y - (15 * varV * Y) - (varV * X)) / (3 * varV)
  [X, Y, Z]

conv.luv.lch = (tuple) ->
  [L, U, V] = tuple
  C = Math.sqrt(Math.pow(U, 2) + Math.pow(V, 2))
  # Greys: disambiguate hue
  if C < 0.00000001
    H = 0
  else  
    Hrad = Math.atan2 V, U
    H = Hrad * 360 / 2 / Math.PI
    H = 360 + H if H < 0
  [L, C, H]

conv.lch.luv = (tuple) ->
  [L, C, H] = tuple
  Hrad = H / 360 * 2 * Math.PI
  U = Math.cos(Hrad) * C
  V = Math.sin(Hrad) * C
  [L, U, V]

conv.husl.lch = (tuple) ->
  [H, S, L] = tuple
  # White and black: disambiguate chroma
  if L > 99.9999999 or L < 0.00000001
    C = 0
  else
    max = maxChromaForLH L, H
    C = max / 100 * S
  return [L, C, H]

conv.lch.husl = (tuple) ->
  [L, C, H] = tuple
  # White and black: disambiguate saturation
  if L > 99.9999999 or L < 0.00000001
    S = 0
  else
    max = maxChromaForLH L, H
    S = C / max * 100
  return [H, S, L]

## PASTEL HUSL

conv.huslp.lch = (tuple) ->
  [H, S, L] = tuple
  # White and black: disambiguate chroma
  if L > 99.9999999 or L < 0.00000001
    C = 0
  else
    max = maxSafeChromaForL L
    C = max / 100 * S
  return [L, C, H]

conv.lch.huslp = (tuple) ->
  [L, C, H] = tuple
  # White and black: disambiguate saturation
  if L > 99.9999999 or L < 0.00000001
    S = 0
  else
    max = maxSafeChromaForL L
    S = C / max * 100
  return [H, S, L]

conv.rgb.hex = (tuple) ->
  hex = "#"
  for ch in tuple
    # Round to 6 decimal places
    ch = Math.round(ch * 1e6) / 1e6
    if ch < 0 or ch > 1
      throw new Error "Illegal rgb value: #{ch}"
    ch = Math.round(ch * 255).toString(16)
    ch = "0" + ch if ch.length is 1
    hex += ch
  hex

conv.hex.rgb = (hex) ->
  if hex.charAt(0) is "#"
    hex = hex.substring 1, 7
  r = hex.substring 0, 2
  g = hex.substring 2, 4
  b = hex.substring 4, 6
  parseInt(n, 16) / 255 for n in [r, g, b]

conv.lch.rgb = (tuple) ->
  conv.xyz.rgb conv.luv.xyz conv.lch.luv tuple
conv.rgb.lch = (tuple) ->
  conv.luv.lch conv.xyz.luv conv.rgb.xyz tuple

conv.husl.rgb = (tuple) ->
  conv.lch.rgb conv.husl.lch tuple
conv.rgb.husl = (tuple) ->
  conv.lch.husl conv.rgb.lch tuple
conv.huslp.rgb = (tuple) ->
  conv.lch.rgb conv.huslp.lch tuple
conv.rgb.huslp = (tuple) ->
  conv.lch.huslp conv.rgb.lch tuple

root = {}

root.fromRGB = (R, G, B) ->
  conv.rgb.husl [R, G, B]
root.fromHex = (hex) ->
  conv.rgb.husl conv.hex.rgb hex
root.toRGB = (H, S, L) ->
  conv.husl.rgb [H, S, L]
root.toHex = (H, S, L) ->
  conv.rgb.hex conv.husl.rgb [H, S, L]
root.p = {}
root.p.toRGB = (H, S, L) ->
  conv.xyz.rgb conv.luv.xyz conv.lch.luv conv.huslp.lch [H, S, L]
root.p.toHex = (H, S, L) ->
  conv.rgb.hex conv.xyz.rgb conv.luv.xyz conv.lch.luv conv.huslp.lch [H, S, L]
root.p.fromRGB = (R, G, B) ->
  conv.lch.huslp conv.luv.lch conv.xyz.luv conv.rgb.xyz [R, G, B]
root.p.fromHex = (hex) ->
  conv.lch.huslp conv.luv.lch conv.xyz.luv conv.rgb.xyz conv.hex.rgb hex

root._conv = conv
root._getBounds = getBounds
root._maxChromaForLH = maxChromaForLH
root._maxSafeChromaForL = maxSafeChromaForL

# If no framework is available, just export to the global object (window.HUSL
# in the browser)
@HUSL = root unless module? or jQuery? or requirejs?
# Export to Node.js
module.exports = root if module?
# Export to jQuery
jQuery.husl = root if jQuery?
# Export to RequireJS
define(root) if requirejs? and define?