[Pkg-javascript-commits] [node-husl] 01/11: Imported Upstream version 6.0.1

Ross Gammon ross-guest at moszumanska.debian.org
Wed Nov 16 21:54:06 UTC 2016


This is an automated email from the git hooks/post-receive script.

ross-guest pushed a commit to branch master
in repository node-husl.

commit b474c73f96e9cde98fabce501589d22ace30ddc1
Author: Ross Gammon <rossgammon at mail.dk>
Date:   Wed Nov 16 20:56:56 2016 +0100

    Imported Upstream version 6.0.1
---
 .gitignore                                         |  11 +
 .travis.yml                                        |   3 +
 Makefile                                           |   5 +
 README.md                                          |  72 ++++
 husl.coffee                                        | 317 +++++++++++++++++
 husl.js                                            | 382 ++++++++++++++++++++
 husl.min.js                                        | Bin 0 -> 13646 bytes
 math/bounds.wxm                                    | 128 +++++++
 math/matrix.wxm                                    |  53 +++
 package.json                                       |  24 ++
 ports/c/husl.c                                     | 371 ++++++++++++++++++++
 ports/c/husl.h                                     |  18 +
 ports/java/HuslConverter.java                      | 389 +++++++++++++++++++++
 ports/java/README.md                               |   6 +
 ports/java/test/res/raw/snapshot.json              |   1 +
 .../test/src/org/ilumbo/cover/test/ColorData.java  |  91 +++++
 .../src/org/ilumbo/cover/test/ColorDataBuffer.java |  79 +++++
 .../test/src/org/ilumbo/cover/test/Snapshot.java   |  79 +++++
 .../org/ilumbo/cover/test/SnapshotTestCase.java    | 157 +++++++++
 test/snapshot-rev0.json                            |   1 +
 test/snapshot-rev1.json                            |   1 +
 test/snapshot-rev2.json                            |   1 +
 test/snapshot-rev3.json                            |   1 +
 test/snapshot-rev4.json                            |   1 +
 test/snapshot.coffee                               |  57 +++
 test/test.coffee                                   |  61 ++++
 26 files changed, 2309 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..18f9dfb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+/node_modules
+/docs/build/index.html
+/docs/build/img/demo
+/docs/build/css/main.css
+/docs/build/js/main.js
+
+/.idea
+*.iml
+npm-debug.log
+
+*~
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..20fd86b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: node_js
+node_js:
+  - 0.10
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..2ad4047
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+husl.js: husl.coffee
+	coffee --compile husl.coffee
+
+husl.min.js: husl.js
+	uglifyjs husl.js > husl.min.js
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f0730ae
--- /dev/null
+++ b/README.md
@@ -0,0 +1,72 @@
+[![Build Status](https://travis-ci.org/husl-colors/husl.svg?branch=master)](https://travis-ci.org/husl-colors/husl)
+[![Package Version](https://img.shields.io/npm/v/husl.svg)](https://www.npmjs.com/package/husl)
+
+[Explanation, demo, ports etc.](http://www.husl-colors.org)
+
+# Usage
+
+Client-side: include [husl.js](https://raw.githubusercontent.com/husl-colors/husl/master/husl.js) or [husl.min.js](https://raw.githubusercontent.com/husl-colors/husl/master/husl.min.js) in your webpage, access it as a global ``HUSL`` object or as a jQuery plugin with ``$.husl``.
+
+Server-side: ``npm install husl``.
+
+**husl.toHex(hue, saturation, lightness)**
+
+*hue* is a number between 0 and 360, *saturation* and *lightness* are numbers between 0 and 100. This function returns the resulting color as a hex string.
+
+**husl.toRGB(hue, saturation, lightness)**
+
+Like above, but returns an array of 3 numbers between 0 and 1, for each RGB channel.
+
+**husl.fromHex(hex)**
+
+Takes a hex string and returns the HUSL color as defined above.
+
+**husl.fromRGB(red, green, blue)**
+
+Like above, but *red*, *green* and *blue* are passed as numbers between 0 and 1.
+
+Use **husl.p.toHex**, **husl.p.toRGB**, **husl.p.fromHex** and **husl.p.fromRGB** for the pastel variant (HUSLp).
+
+HUSL can also be used as a [Stylus](http://learnboost.github.com/stylus/) plugin. See [here](https://github.com/husl-colors/husl-stylus).
+
+# Versioning
+
+This repo contains the canonical implementation of HUSL, its source code being HUSL's informal specification. Following [semantic versioning](http://semver.org/), the major version must be incremented whenever the color math changes. These changes can be tested for with snapshot files.
+
+# Testing
+
+Run `npm install` and `npm test`. Try `coffee test/snapshot.coffee` to generate a JSON file of the entire gamut to be used for debugging and regression tests. The format of the file is as follows:
+
+    {
+      "#000000": {
+        rgb: [ 0, 0, 0 ],
+        xyz: [ 0, 0, 0 ],
+        luv: [ 0, 0, 0 ],
+        lch: [ 0, 0, 0 ],
+        husl: [ 0, 0, 0 ],
+        huslp: [ 0, 0, 0 ]
+      },
+      ...
+    }
+
+# License
+
+Copyright (c) 2015 Alexei Boronine
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/husl.coffee b/husl.coffee
new file mode 100644
index 0000000..7391388
--- /dev/null
+++ b/husl.coffee
@@ -0,0 +1,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)
+ at 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?
diff --git a/husl.js b/husl.js
new file mode 100644
index 0000000..901aac6
--- /dev/null
+++ b/husl.js
@@ -0,0 +1,382 @@
+// Generated by CoffeeScript 1.9.3
+(function() {
+  var L_to_Y, Y_to_L, conv, distanceFromPole, dotProduct, epsilon, fromLinear, getBounds, intersectLineLine, kappa, lengthOfRayUntilIntersect, m, m_inv, maxChromaForLH, maxSafeChromaForL, refU, refV, root, toLinear;
+
+  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;
+
+  kappa = 903.2962962962963;
+
+  epsilon = 0.0088564516790356308;
+
+  getBounds = function(L) {
+    var bottom, channel, j, k, len1, len2, m1, m2, m3, ref, ref1, ref2, ret, sub1, sub2, t, top1, top2;
+    sub1 = Math.pow(L + 16, 3) / 1560896;
+    sub2 = sub1 > epsilon ? sub1 : L / kappa;
+    ret = [];
+    ref = ['R', 'G', 'B'];
+    for (j = 0, len1 = ref.length; j < len1; j++) {
+      channel = ref[j];
+      ref1 = m[channel], m1 = ref1[0], m2 = ref1[1], m3 = ref1[2];
+      ref2 = [0, 1];
+      for (k = 0, len2 = ref2.length; k < len2; k++) {
+        t = ref2[k];
+        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 = function(line1, line2) {
+    return (line1[1] - line2[1]) / (line2[0] - line1[0]);
+  };
+
+  distanceFromPole = function(point) {
+    return Math.sqrt(Math.pow(point[0], 2) + Math.pow(point[1], 2));
+  };
+
+  lengthOfRayUntilIntersect = function(theta, line) {
+    var b1, len, m1;
+    m1 = line[0], b1 = line[1];
+    len = b1 / (Math.sin(theta) - m1 * Math.cos(theta));
+    if (len < 0) {
+      return null;
+    }
+    return len;
+  };
+
+  maxSafeChromaForL = function(L) {
+    var b1, j, len1, lengths, m1, ref, ref1, x;
+    lengths = [];
+    ref = getBounds(L);
+    for (j = 0, len1 = ref.length; j < len1; j++) {
+      ref1 = ref[j], m1 = ref1[0], b1 = ref1[1];
+      x = intersectLineLine([m1, b1], [-1 / m1, 0]);
+      lengths.push(distanceFromPole([x, b1 + x * m1]));
+    }
+    return Math.min.apply(Math, lengths);
+  };
+
+  maxChromaForLH = function(L, H) {
+    var hrad, j, l, len1, lengths, line, ref;
+    hrad = H / 360 * Math.PI * 2;
+    lengths = [];
+    ref = getBounds(L);
+    for (j = 0, len1 = ref.length; j < len1; j++) {
+      line = ref[j];
+      l = lengthOfRayUntilIntersect(hrad, line);
+      if (l !== null) {
+        lengths.push(l);
+      }
+    }
+    return Math.min.apply(Math, lengths);
+  };
+
+  dotProduct = function(a, b) {
+    var i, j, ref, ret;
+    ret = 0;
+    for (i = j = 0, ref = a.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
+      ret += a[i] * b[i];
+    }
+    return ret;
+  };
+
+  fromLinear = function(c) {
+    if (c <= 0.0031308) {
+      return 12.92 * c;
+    } else {
+      return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
+    }
+  };
+
+  toLinear = function(c) {
+    var a;
+    a = 0.055;
+    if (c > 0.04045) {
+      return Math.pow((c + a) / (1 + a), 2.4);
+    } else {
+      return c / 12.92;
+    }
+  };
+
+  conv = {
+    'xyz': {},
+    'luv': {},
+    'lch': {},
+    'husl': {},
+    'huslp': {},
+    'rgb': {},
+    'hex': {}
+  };
+
+  conv.xyz.rgb = function(tuple) {
+    var B, G, R;
+    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 = function(tuple) {
+    var B, G, R, X, Y, Z, rgbl;
+    R = tuple[0], G = tuple[1], B = tuple[2];
+    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);
+    return [X, Y, Z];
+  };
+
+  Y_to_L = function(Y) {
+    if (Y <= epsilon) {
+      return Y * kappa;
+    } else {
+      return 116 * Math.pow(Y, 1 / 3) - 16;
+    }
+  };
+
+  L_to_Y = function(L) {
+    if (L <= 8) {
+      return L / kappa;
+    } else {
+      return Math.pow((L + 16) / 116, 3);
+    }
+  };
+
+  conv.xyz.luv = function(tuple) {
+    var L, U, V, X, Y, Z, varU, varV;
+    X = tuple[0], Y = tuple[1], Z = tuple[2];
+    if (Y === 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);
+    return [L, U, V];
+  };
+
+  conv.luv.xyz = function(tuple) {
+    var L, U, V, X, Y, Z, varU, varV;
+    L = tuple[0], U = tuple[1], V = tuple[2];
+    if (L === 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);
+    return [X, Y, Z];
+  };
+
+  conv.luv.lch = function(tuple) {
+    var C, H, Hrad, L, U, V;
+    L = tuple[0], U = tuple[1], V = tuple[2];
+    C = Math.sqrt(Math.pow(U, 2) + Math.pow(V, 2));
+    if (C < 0.00000001) {
+      H = 0;
+    } else {
+      Hrad = Math.atan2(V, U);
+      H = Hrad * 360 / 2 / Math.PI;
+      if (H < 0) {
+        H = 360 + H;
+      }
+    }
+    return [L, C, H];
+  };
+
+  conv.lch.luv = function(tuple) {
+    var C, H, Hrad, L, U, V;
+    L = tuple[0], C = tuple[1], H = tuple[2];
+    Hrad = H / 360 * 2 * Math.PI;
+    U = Math.cos(Hrad) * C;
+    V = Math.sin(Hrad) * C;
+    return [L, U, V];
+  };
+
+  conv.husl.lch = function(tuple) {
+    var C, H, L, S, max;
+    H = tuple[0], S = tuple[1], L = tuple[2];
+    if (L > 99.9999999 || L < 0.00000001) {
+      C = 0;
+    } else {
+      max = maxChromaForLH(L, H);
+      C = max / 100 * S;
+    }
+    return [L, C, H];
+  };
+
+  conv.lch.husl = function(tuple) {
+    var C, H, L, S, max;
+    L = tuple[0], C = tuple[1], H = tuple[2];
+    if (L > 99.9999999 || L < 0.00000001) {
+      S = 0;
+    } else {
+      max = maxChromaForLH(L, H);
+      S = C / max * 100;
+    }
+    return [H, S, L];
+  };
+
+  conv.huslp.lch = function(tuple) {
+    var C, H, L, S, max;
+    H = tuple[0], S = tuple[1], L = tuple[2];
+    if (L > 99.9999999 || L < 0.00000001) {
+      C = 0;
+    } else {
+      max = maxSafeChromaForL(L);
+      C = max / 100 * S;
+    }
+    return [L, C, H];
+  };
+
+  conv.lch.huslp = function(tuple) {
+    var C, H, L, S, max;
+    L = tuple[0], C = tuple[1], H = tuple[2];
+    if (L > 99.9999999 || L < 0.00000001) {
+      S = 0;
+    } else {
+      max = maxSafeChromaForL(L);
+      S = C / max * 100;
+    }
+    return [H, S, L];
+  };
+
+  conv.rgb.hex = function(tuple) {
+    var ch, hex, j, len1;
+    hex = "#";
+    for (j = 0, len1 = tuple.length; j < len1; j++) {
+      ch = tuple[j];
+      ch = Math.round(ch * 1e6) / 1e6;
+      if (ch < 0 || ch > 1) {
+        throw new Error("Illegal rgb value: " + ch);
+      }
+      ch = Math.round(ch * 255).toString(16);
+      if (ch.length === 1) {
+        ch = "0" + ch;
+      }
+      hex += ch;
+    }
+    return hex;
+  };
+
+  conv.hex.rgb = function(hex) {
+    var b, g, j, len1, n, r, ref, results;
+    if (hex.charAt(0) === "#") {
+      hex = hex.substring(1, 7);
+    }
+    r = hex.substring(0, 2);
+    g = hex.substring(2, 4);
+    b = hex.substring(4, 6);
+    ref = [r, g, b];
+    results = [];
+    for (j = 0, len1 = ref.length; j < len1; j++) {
+      n = ref[j];
+      results.push(parseInt(n, 16) / 255);
+    }
+    return results;
+  };
+
+  conv.lch.rgb = function(tuple) {
+    return conv.xyz.rgb(conv.luv.xyz(conv.lch.luv(tuple)));
+  };
+
+  conv.rgb.lch = function(tuple) {
+    return conv.luv.lch(conv.xyz.luv(conv.rgb.xyz(tuple)));
+  };
+
+  conv.husl.rgb = function(tuple) {
+    return conv.lch.rgb(conv.husl.lch(tuple));
+  };
+
+  conv.rgb.husl = function(tuple) {
+    return conv.lch.husl(conv.rgb.lch(tuple));
+  };
+
+  conv.huslp.rgb = function(tuple) {
+    return conv.lch.rgb(conv.huslp.lch(tuple));
+  };
+
+  conv.rgb.huslp = function(tuple) {
+    return conv.lch.huslp(conv.rgb.lch(tuple));
+  };
+
+  root = {};
+
+  root.fromRGB = function(R, G, B) {
+    return conv.rgb.husl([R, G, B]);
+  };
+
+  root.fromHex = function(hex) {
+    return conv.rgb.husl(conv.hex.rgb(hex));
+  };
+
+  root.toRGB = function(H, S, L) {
+    return conv.husl.rgb([H, S, L]);
+  };
+
+  root.toHex = function(H, S, L) {
+    return conv.rgb.hex(conv.husl.rgb([H, S, L]));
+  };
+
+  root.p = {};
+
+  root.p.toRGB = function(H, S, L) {
+    return conv.xyz.rgb(conv.luv.xyz(conv.lch.luv(conv.huslp.lch([H, S, L]))));
+  };
+
+  root.p.toHex = function(H, S, L) {
+    return conv.rgb.hex(conv.xyz.rgb(conv.luv.xyz(conv.lch.luv(conv.huslp.lch([H, S, L])))));
+  };
+
+  root.p.fromRGB = function(R, G, B) {
+    return conv.lch.huslp(conv.luv.lch(conv.xyz.luv(conv.rgb.xyz([R, G, B]))));
+  };
+
+  root.p.fromHex = function(hex) {
+    return 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 (!((typeof module !== "undefined" && module !== null) || (typeof jQuery !== "undefined" && jQuery !== null) || (typeof requirejs !== "undefined" && requirejs !== null))) {
+    this.HUSL = root;
+  }
+
+  if (typeof module !== "undefined" && module !== null) {
+    module.exports = root;
+  }
+
+  if (typeof jQuery !== "undefined" && jQuery !== null) {
+    jQuery.husl = root;
+  }
+
+  if ((typeof requirejs !== "undefined" && requirejs !== null) && (typeof define !== "undefined" && define !== null)) {
+    define(root);
+  }
+
+}).call(this);
diff --git a/husl.min.js b/husl.min.js
new file mode 100644
index 0000000..01be248
Binary files /dev/null and b/husl.min.js differ
diff --git a/math/bounds.wxm b/math/bounds.wxm
new file mode 100644
index 0000000..39a8347
--- /dev/null
+++ b/math/bounds.wxm
@@ -0,0 +1,128 @@
+/* [wxMaxima batch file version 1] [ DO NOT EDIT BY HAND! ]*/
+/* [ Created with wxMaxima version 15.04.0 ] */
+
+/* [wxMaxima: input   start ] */
+/* Authors: Alexei Boronine, Bastien Dejean */
+
+/* References:
+https://gist.github.com/baskerville/8297428
+http://www.brucelindbloom.com/ */
+ 
+/* The following values are taken directly from matrix.wxm */
+
+refX: 3127/3290;
+refY: 1;
+refZ: 3583/3290;
+
+M_RGB_XYZ: matrix(
+    [506752/1228815, 87881/245763,  12673/70218],
+    [87098/409605, 175762/245763,  12673/175545],
+    [7918/409605, 87881/737289, 1001167/1053270]);
+M_XYZ_RGB: invert(M_RGB_XYZ);
+
+/* Note that the standard defines these constants as decimals
+and is incorrect. We are using the correct non-standard values.
+See: http://www.brucelindbloom.com/index.html?LContinuity.html */
+epsilon: (6 / 29) ^ 3;
+kappa: (29 / 3) ^ 3;
+refU: (4 * refX) / (refX + (15 * refY) + (3 * refZ));
+refV: (9 * refY) / (refX + (15 * refY) + (3 * refZ));
+
+/* Not used, see linearRGBChannelFromLUV */
+fromLinear(c):= if (c<=0.0031308)
+    then 12.92 * c
+    else 1.055 * c ^ (1 / 2.4) - 0.055;
+/* [wxMaxima: input   end   ] */
+
+/* [wxMaxima: input   start ] */
+f_inv(t) := if (t ^ 3 > epsilon) 
+    then t ^ 3
+    else (116 * t - 16) / kappa;
+/* [wxMaxima: input   end   ] */
+
+/* [wxMaxima: input   start ] */
+/* Returns the value of a linear sRGB channel specified by its matrix 
+values. Note that this is not the final sRGB value since gamma correction 
+is not applied, however, the value happens to be correct in two cases: 
+0 and 1. (Solve fromLinear above to see for yourself). As it happens, the 
+only use for this function is to solve it for 0 and 1, which means 
+fromLinear won't be necessary.
+*/
+
+linearRGBChannelFromLUV(L, U, V, m1, m2, m3) := block(
+    [varY, varU, varV, Y, X, Z],
+    varY : f_inv((L + 16) / 116),
+    varU : U / (13 * L) + refU,
+    varV : V / (13 * L) + refV,
+    Y    : varY * refY,
+    X    : 0 - (9 * Y * varU) / ((varU - 4) * varV - varU * varV),
+    Z    : (9 * Y - (15 * varV * Y) - (varV * X)) / (3 * varV),
+    [X, Y, Z] . [m1, m2, m3]
+);
+/* [wxMaxima: input   end   ] */
+
+/* [wxMaxima: input   start ] */
+/* Given L, U, RGB channel and limit t (0 or 1), return the V value
+that will push the CIE LUV color outside the sRGB gamut for the given
+channel and the given limit. */
+
+solve(linearRGBChannelFromLUV(L, U, V, m1, m2, m3) = t, V);
+/* [wxMaxima: input   end   ] */
+
+/* [wxMaxima: input   start ] */
+/* Bounding function generator */
+
+bound(L, m1, m2, m3, t) := solve(linearRGBChannelFromLUV(L, x, y, m1, m2, m3) = t, y)[1];
+/* [wxMaxima: input   end   ] */
+
+/* [wxMaxima: input   start ] */
+/* Generate bounding function that draws a line on CIE LUV space, stepping
+outside of which will push the red RGB value below 0 */
+
+bound(50, M_XYZ_RGB[1][1], M_XYZ_RGB[1][2], M_XYZ_RGB[1][3], 0);
+/* [wxMaxima: input   end   ] */
+
+/* [wxMaxima: input   start ] */
+block(
+    [L],
+    L: 50,
+    plot2d([
+        rhs(bound(L, M_XYZ_RGB[1][1], M_XYZ_RGB[1][2], M_XYZ_RGB[1][3], 0)),
+        rhs(bound(L, M_XYZ_RGB[1][1], M_XYZ_RGB[1][2], M_XYZ_RGB[1][3], 1)),
+        rhs(bound(L, M_XYZ_RGB[2][1], M_XYZ_RGB[2][2], M_XYZ_RGB[2][3], 0)),
+        rhs(bound(L, M_XYZ_RGB[2][1], M_XYZ_RGB[2][2], M_XYZ_RGB[2][3], 1)),
+        rhs(bound(L, M_XYZ_RGB[3][1], M_XYZ_RGB[3][2], M_XYZ_RGB[3][3], 0)),
+        rhs(bound(L, M_XYZ_RGB[3][1], M_XYZ_RGB[3][2], M_XYZ_RGB[3][3], 1))
+    ], [x,-400,400], [y,-400,400], [
+        legend,
+        "red = 0",
+        "red = 1",
+        "green = 0",
+        "green = 1",
+        "blue = 0",
+        "blue = 1"
+    ], [
+        xlabel,
+        "hue"
+    ], [
+        ylabel,
+        "chroma"
+    ])
+);
+/* [wxMaxima: input   end   ] */
+
+/* [wxMaxima: input   start ] */
+fpprec: 17;
+bfloat(M_XYZ_RGB);
+bfloat(M_RGB_XYZ);
+bfloat(refX);
+bfloat(refY);
+bfloat(refZ);
+bfloat(refU);
+bfloat(refV);
+bfloat(kappa);
+bfloat(epsilon);
+/* [wxMaxima: input   end   ] */
+
+/* Maxima can't load/batch files which end with a comment! */
+"Created with wxMaxima"$
diff --git a/math/matrix.wxm b/math/matrix.wxm
new file mode 100644
index 0000000..6cca021
--- /dev/null
+++ b/math/matrix.wxm
@@ -0,0 +1,53 @@
+/* [wxMaxima batch file version 1] [ DO NOT EDIT BY HAND! ]*/
+/* [ Created with wxMaxima version 15.04.0 ] */
+
+/* [wxMaxima: input   start ] */
+/* Authors: Alexei Boronine, Bastien Dejean */
+
+/* Matrices generated with the following algorithm:                 */
+/* http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html */
+/* Which makes use of this transformation:                          */
+/* http://www.brucelindbloom.com/index.html?Eqn_xyY_to_XYZ.html     */
+
+xyY_to_XYZ(x, y, Y) := [x * Y / y, Y, (1 - x - y) * Y / y];
+
+/* sRGB primaries with D65 white point               */
+/* http://en.wikipedia.org/wiki/SRGB#The_sRGB_gamut  */
+/* http://en.wikipedia.org/wiki/Illuminant_D65       */
+
+x_r: rat(0.64);
+y_r: rat(0.33);
+x_g: rat(0.30);
+y_g: rat(0.60);
+x_b: rat(0.15);
+y_b: rat(0.06);
+x_w: rat(0.3127);
+y_w: rat(0.3290);
+
+[X_r, Y_r, Z_r]: xyY_to_XYZ(x_r, y_r, 1);
+[X_g, Y_g, Z_g]: xyY_to_XYZ(x_g, y_g, 1);
+[X_b, Y_b, Z_b]: xyY_to_XYZ(x_b, y_b, 1);
+[X_w, Y_w, Z_w]: xyY_to_XYZ(x_w, y_w, 1);
+ 
+M_P: matrix([X_r, X_g, X_b],
+            [Y_r, Y_g, Y_b],
+            [Z_r, Z_g, Z_b]);
+ 
+M_S: invert(M_P) . [X_w, Y_w, Z_w];
+
+M_RGB_XYZ: M_P * transpose(addcol(M_S, M_S, M_S));
+M_XYZ_RGB: invert(M_RGB_XYZ);
+
+fpprec: 16;
+bfloat(M_RGB_XYZ);
+bfloat(M_XYZ_RGB);
+
+/* XYZ tristimulus values for D65, used as reference white point here:
+http://www.brucelindbloom.com/index.html?Eqn_xyY_to_XYZ.html */
+
+xyY_to_XYZ(rat(0.3127), rat(0.3290), 1);
+
+/* [wxMaxima: input   end   ] */
+
+/* Maxima can't load/batch files which end with a comment! */
+"Created with wxMaxima"$
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9e80173
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+  "name": "husl",
+  "description": "Human-friendly HSL",
+  "keywords": ["color", "color space", "CIE", "RGB", "HUSL", "HSL"],
+  "version": "6.0.1",
+  "author": "Alexei Boronine <alexei at boronine.com>",
+  "license": "MIT",
+  "main": "husl.js",
+  "files": ["husl.js"],
+  "repository": {
+    "type" : "git",
+    "url" : "http://github.com/husl-colors/husl.git"
+  },
+  "homepage": "http://www.husl-colors.org",
+  "devDependencies": {
+    "coffee-script": "*",
+    "underscore": "*",
+    "uglify-js": "*",
+    "mocha": "*"
+  },
+  "scripts": {
+    "test": "mocha --compilers coffee:coffee-script/register"
+  }
+}
diff --git a/ports/c/husl.c b/ports/c/husl.c
new file mode 100644
index 0000000..bb40ded
--- /dev/null
+++ b/ports/c/husl.c
@@ -0,0 +1,371 @@
+#include "husl.h"
+
+#ifdef __cplusplus	//libs if c++ is used
+#include <cmath>
+#include <cfloat>
+#include <cstdio>
+#include <cstring>
+#include <cstdlib>
+#else				//libs if pure c is used
+#include <math.h>
+#include <float.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#endif
+
+#define PI 3.1415926535897932384626433832795
+
+float m[3][3] = {3.2406f, -1.5372f, -0.4986f, -0.9689f, 1.8758f, 0.0415f, 0.0557f, -0.2040f, 1.0570f};
+float m_inv[3][3] = {0.4124f, 0.3576f, 0.1805f, 0.2126f, 0.7152f, 0.0722f, 0.0193f, 0.1192f, 0.9505f};
+float refX = 0.95047f;
+float refY = 1.00000f;
+float refZ = 1.08883f;
+float refU = 0.19784f;
+float refV = 0.46834f;
+float lab_e = 0.008856f;
+float lab_k = 903.3f;
+
+char hex[64];
+float values[3];
+
+
+float* XYZ_RGB(float *tuple);
+float* RGB_XYZ(float *tuple);
+float* XYZ_LUV(float *tuple);
+float* LUV_XYZ(float *tuple);
+float* LUV_LCH(float *tuple);
+float* LCH_LUV(float *tuple);
+float* HUSL_LCH(float *tuple);
+float* LCH_HUSL(float *tuple);
+
+
+float maxChroma(float L, float H);
+float* rgbPrepare(float *tuple);
+
+float dotProduct(float* a, float * b, int len);
+float round(float num, int places);
+float f(float t);
+float f_inv(float t);
+float fromLinear(float c);
+float toLinear(float c);
+
+
+//////////////////////////////////////////////////////////////////////////////////
+
+
+void HUSLtoRGB( float *r, float *g, float *b, float h, float s,float l )
+{
+	values[0] = h;
+	values[1] = s;
+	values[2] = l;
+
+	XYZ_RGB(LUV_XYZ(LCH_LUV(HUSL_LCH(values))));
+
+	*r = values[0];
+	*g = values[1];
+	*b = values[2];
+
+	return;
+}
+
+void RGBtoHUSL( float *h, float *s,float *l, float r, float g, float b )
+{
+	values[0] = r;
+	values[1] = g;
+	values[2] = b;
+
+	LCH_HUSL(LUV_LCH(XYZ_LUV(RGB_XYZ(values))));
+
+	*h = values[0];
+	*s = values[1];
+	*l = values[2];
+
+	return;
+}
+
+
+
+
+
+
+float maxChroma(float L, float H){
+
+	float C, bottom, cosH, hrad, lbottom, m1, m2, m3, rbottom, result, sinH, sub1, sub2, t, top;
+	int _i, _j, _len, _len1;
+	float *row;
+	float _ref[2] = {0.0f, 1.0f};
+
+
+	hrad = (float) ((H / 360.0f) * 2 * PI);
+	sinH = (float) (sin(hrad));
+	cosH = (float) (cos(hrad));
+	sub1 = (float) (pow(L + 16, 3) / 1560896.0);
+	sub2 = sub1 > 0.008856 ? sub1 : (float) (L / 903.3);
+	result = FLT_MAX;
+	for (_i = 0, _len = 3; _i < _len; ++_i) {
+		row = m[_i];
+		m1 = row[0], m2 = row[1], m3 = row[2];
+		top = (float) ((0.99915 * m1 + 1.05122 * m2 + 1.14460 * m3) * sub2);
+		rbottom = (float) (0.86330 * m3 - 0.17266 * m2);
+		lbottom = (float) (0.12949 * m3 - 0.38848 * m1);
+		bottom = (rbottom * sinH + lbottom * cosH) * sub2;
+
+		for (_j = 0, _len1 = 2; _j < _len1; ++_j) {
+			t = _ref[_j];
+			C = (float) (L * (top - 1.05122 * t) / (bottom + 0.17266 * sinH * t));
+			if ((C > 0 && C < result)) {
+				result = C;
+			}
+		}
+	}
+	return result;
+}
+
+float dotProduct(float* a, float * b, int len){
+
+	int i, _i, _ref;
+	float ret = 0.0f;
+	for (i = _i = 0, _ref = len - 1;    0 <= _ref ? _i <= _ref : _i >= _ref;    i = 0 <= _ref ? ++_i : --_i) {
+		ret += a[i] * b[i];
+	}
+	return ret;
+
+}
+
+float round( float num, int places )
+{
+	float n;
+	n = (float) (pow(10.0f, places));
+	return (float) (floor(num * n) / n);
+}
+
+float f( float t )
+{
+	if (t > lab_e) {
+		return (float) (pow(t, 1.0f / 3.0f));
+	} else {
+		return (float) (7.787 * t + 16 / 116.0);
+	}
+}
+
+float f_inv( float t )
+{
+	if (pow(t, 3) > lab_e) {
+		return (float) (pow(t, 3));
+	} else {
+		return (116 * t - 16) / lab_k;
+	}
+}
+
+float fromLinear( float c )
+{
+	if (c <= 0.0031308) {
+		return 12.92f * c;
+	} else {
+		return (float) (1.055 * pow(c, 1 / 2.4f) - 0.055);
+	}
+}
+
+float toLinear( float c )
+{
+	float a = 0.055f;
+
+	if (c > 0.04045) {
+		return (float) (pow((c + a) / (1 + a), 2.4f));
+	} else {
+		return (float) (c / 12.92);
+	}
+}
+
+float* rgbPrepare( float *tuple )
+{
+	int i;
+
+	for(i = 0; i < 3; ++i){
+		tuple[i] = round(tuple[i], 3);
+
+		if (tuple[i] < 0 || tuple[i] > 1) {
+			if(tuple[i] < 0)
+				tuple[i] = 0;
+			else
+				tuple[i] = 1;
+			//printf("Illegal rgb value: %f\n", tuple[i]);
+		}
+
+		tuple[i] = round(tuple[i]*255, 0);
+	}
+
+	return tuple;
+}
+
+float* XYZ_RGB( float *tuple )
+{
+	float B, G, R;
+	R = fromLinear(dotProduct(m[0], tuple, 3));
+	G = fromLinear(dotProduct(m[1], tuple, 3));
+	B = fromLinear(dotProduct(m[2], tuple, 3));
+
+	tuple[0] = R;
+	tuple[1] = G;
+	tuple[2] = B;
+
+	return tuple;
+}
+
+float* RGB_XYZ( float *tuple )
+{
+	float B, G, R, X, Y, Z;
+	float rgbl[3];
+
+	R = tuple[0];
+	G = tuple[1]; 
+	B = tuple[2];
+
+	rgbl[0] = toLinear(R);
+	rgbl[1] = toLinear(G);
+	rgbl[2] = toLinear(B);
+
+	X = dotProduct(m_inv[0], rgbl, 3);
+	Y = dotProduct(m_inv[1], rgbl, 3);
+	Z = dotProduct(m_inv[2], rgbl, 3);
+
+	tuple[0] = X;
+	tuple[1] = Y;
+	tuple[2] = Z;
+
+	return tuple;
+}
+
+float* XYZ_LUV( float *tuple )
+{
+	float L, U, V, X, Y, Z, varU, varV;
+
+	X = tuple[0]; 
+	Y = tuple[1]; 
+	Z = tuple[2];
+
+	varU = (4 * X) / (X + (15.0f * Y) + (3 * Z));
+	varV = (9 * Y) / (X + (15.0f * Y) + (3 * Z));
+	L = 116 * f(Y / refY) - 16;
+	U = 13 * L * (varU - refU);
+	V = 13 * L * (varV - refV);
+
+	tuple[0] = L;
+	tuple[1] = U;
+	tuple[2] = V;
+
+	return tuple;
+}
+
+float* LUV_XYZ( float *tuple )
+{
+	float L, U, V, X, Y, Z, varU, varV, varY;
+
+	L = tuple[0]; 
+	U = tuple[1]; 
+	V = tuple[2];
+
+	if (L == 0) {
+		tuple[2] = tuple[1] = tuple[0] = 0.0f;
+		return tuple;
+	}
+
+	varY = f_inv((L + 16) / 116.0f);
+	varU = U / (13.0f * L) + refU;
+	varV = V / (13.0f * L) + refV;
+	Y = varY * refY;
+	X = 0 - (9 * Y * varU) / ((varU - 4.0f) * varV - varU * varV);
+	Z = (9 * Y - (15 * varV * Y) - (varV * X)) / (3.0f * varV);
+
+	tuple[0] = X;
+	tuple[1] = Y;
+	tuple[2] = Z;
+
+	return tuple;
+}
+
+float* LUV_LCH( float *tuple )
+{
+	float C, H, Hrad, L, U, V;
+
+	L = tuple[0]; 
+	U = tuple[1]; 
+	V = tuple[2];
+
+	C = (float) (pow(pow(U, 2) + pow(V, 2), (1 / 2.0f)));
+	Hrad = (float) (atan2(V, U));
+	H = (float) (Hrad * 360.0f / 2.0f / PI);
+	if (H < 0) {
+		H = 360 + H;
+	}
+
+	tuple[0] = L;
+	tuple[1] = C;
+	tuple[2] = H;
+
+	return tuple;
+}
+
+float* LCH_LUV( float *tuple )
+{
+	float C, H, Hrad, L, U, V;
+
+	L = tuple[0]; 
+	C = tuple[1]; 
+	H = tuple[2];
+
+	Hrad = (float) (H / 360.0 * 2.0 * PI);
+	U = (float) (cos(Hrad) * C);
+	V = (float) (sin(Hrad) * C);
+
+	tuple[0] = L;
+	tuple[1] = U;
+	tuple[2] = V;
+
+	return tuple;
+}
+
+float* HUSL_LCH( float *tuple )
+{
+	float C, H, L, S, max;
+
+	H = tuple[0]; 
+	S = tuple[1]; 
+	L = tuple[2];
+
+	max = maxChroma(L, H);
+	C = max / 100.0f * S;
+
+	tuple[0] = L;
+	tuple[1] = C;
+	tuple[2] = H;
+
+	return tuple;
+}
+
+float* LCH_HUSL( float *tuple )
+{
+	float C, H, L, S, max;
+
+	L = tuple[0]; 
+	C = tuple[1]; 
+	H = tuple[2];
+
+	max = maxChroma(L, H);
+	S = C / max * 100;
+	
+	tuple[0] = H;
+	tuple[1] = S;
+	tuple[2] = L;
+
+	return tuple;
+}
+
+
+
+
+
+
+
+
diff --git a/ports/c/husl.h b/ports/c/husl.h
new file mode 100644
index 0000000..2bc812d
--- /dev/null
+++ b/ports/c/husl.h
@@ -0,0 +1,18 @@
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//These are the only 2 functions you have to use. Don't care about the ones in husl.c
+
+//Pass in HUSL values and get back RGB values, H ranges from 0 to 360, S and L from 0 to 100.
+//RGB values will range from 0 to 1.
+void HUSLtoRGB(float *r, float *g, float *b, float h, float s,float l);
+
+//Pass in RGB values ranging from 0 to 1 and get back HUSL values.
+//H ranges from 0 to 360, S and L from 0 to 100.
+void RGBtoHUSL(float *h, float *s,float *l, float r, float g, float b);
+
+
+#ifdef __cplusplus
+} 
+#endif
\ No newline at end of file
diff --git a/ports/java/HuslConverter.java b/ports/java/HuslConverter.java
new file mode 100644
index 0000000..5a35fd1
--- /dev/null
+++ b/ports/java/HuslConverter.java
@@ -0,0 +1,389 @@
+package com.boronine.husl;
+
+
+public class HuslConverter {
+
+	/* package */ static float PI = 3.1415926535897932384626433832795f;
+	// Used for rgb ↔ xyz conversions.
+	/* package */ static float m[][] = {{3.2406f, -1.5372f, -0.4986f},
+								  {-0.9689f, 1.8758f, 0.0415f},
+								  {0.0557f, -0.2040f, 1.0570f}};
+	private static float m_inv[][] = {{0.4124f, 0.3576f, 0.1805f},
+									  {0.2126f, 0.7152f, 0.0722f},
+									  {0.0193f, 0.1192f, 0.9505f}};
+	// Hard-coded D65 standard illuminant.
+	private static float refX = 0.95047f;
+	private static float refY = 1.00000f;
+	private static float refZ = 1.08883f;
+	private static float refU = 0.19784f; // 4 * refX / (refX + 15 * refY + 3 * refZ)
+	private static float refV = 0.46834f; // 9 * refY / (refX + 15 * refY + 3 * refZ)
+	// CIE LAB and LUV constants.
+	private static float lab_e = 0.008856f;
+	private static float lab_k = 903.3f;
+	
+	private static final int RGB_R = 0;
+	private static final int RGB_G = 1;
+	private static final int RGB_B = 2;
+
+	private static final int XYZ_X = 0;
+	private static final int XYZ_Y = 1;
+	private static final int XYZ_Z = 2;
+
+	private static final int LUV_L = 0;
+	private static final int LUV_U = 1;
+	private static final int LUV_V = 2;
+
+	private static final int LCH_L = 0;
+	private static final int LCH_C = 1;
+	private static final int LCH_H = 2;
+
+	private static final int HUSL_H = 0;
+	private static final int HUSL_S = 1;
+	private static final int HUSL_L = 2;
+
+	/**
+	 * For a given lightness and hue, return the maximum chroma that fits in the RGB gamut.
+	 */
+	public static float maxChroma(float L, float H) {
+		// The CoffeeScript and JavaScript versions of HUSL have this function broken up into several
+		// schönfinkeling/currying-style functions. This however doesn't work as well in Java. Therefore, everything is cramped
+		// up into one function.
+		float result = Float.POSITIVE_INFINITY;
+		final float hrad = H / 360 * 2 * HuslConverter.PI;
+		final float sinH = (float) Math.sin(hrad);
+		final float cosH = (float) Math.cos(hrad);
+		final float sub1 = (float) Math.pow(L + 16, 3) / 1560896f;
+		final float sub2 = sub1 > 0.008856f ? sub1 : L / 903.3f;
+		// Loop over the channels (red, green and blue).
+		for (int channel = 0; 3 != channel; channel++) {
+			final float[] channelM = HuslConverter.m[channel];
+			final float top = (0.99915f * channelM[0] + 1.05122f * channelM[1] + 1.14460f * channelM[2]) * sub2;
+			final float rbottom = 0.86330f * channelM[2] - 0.17266f * channelM[1];
+			final float lbottom = 0.12949f * channelM[2] - 0.38848f * channelM[0];
+			final float bottom = (rbottom * sinH + lbottom * cosH) * sub2;
+			// Calculate the C values that you can put together with the given L and H to produce a colour that with
+			// <RGB channel> = 1 or 2. This means that if C goes any higher, the colour will step outside of the RGB gamut.
+			final float C0 = L * top / bottom;
+			if (C0 > 0 && C0 < result) {
+				result = C0;
+			}
+			final float C1 = L * (top - 1.05122f * 1) / (bottom + 0.17266f * sinH);
+			if (C1 > 0 && C1 < result) {
+				result = C1;
+			}
+		}
+		return result;
+	}
+
+	private static float dotProduct(float a[], float b[]) {
+		float result = 0;
+		for (int index = 0; 3 != index; index++) {
+			result += a[index] * b[index];
+		}
+		return result;
+	}
+
+	private static float round(float num, int places) {
+		float n;
+		n = (float) Math.pow(10.0f, places);
+		return (float) (Math.floor(num * n) / n);
+	}
+
+	// Used for Lab and Luv conversions.
+	private static float f(float t) {
+		if (t > lab_e) {
+			return (float) Math.pow(t, 1f / 3);
+		} else {
+			return 7.787f * t + 16 / 116f;
+		}
+	}
+	private static float f_inv(float t) {
+		final float proposedResult = (float) Math.pow(t, 3);
+		if (proposedResult > lab_e) {
+			return proposedResult;
+		} else {
+			return (116 * t - 16) / lab_k;
+		}
+	}
+
+	// Used for RGB conversions.
+	private static float fromLinear(float c) {
+		if (c <= 0.0031308f) {
+			return 12.92f * c;
+		} else {
+			return 1.055f * (float) Math.pow(c, 1 / 2.4f) - 0.055f;
+		}
+	}
+	private static float toLinear(float c) {
+		if (c > 0.04045f) {
+			return (float) Math.pow((c + 0.055f) / 1.055f, 2.4f);
+		} else {
+			return c / 12.92f;
+		}
+	}
+
+	/**
+	 * Converts an XYZ tuple to an RGB one, altering the passed array to represent the output (discarding the input).
+	 */
+	private static void unsafeConvertXyzToRgb(float tuple[]) {
+		// Tuple represents input.
+		final float R = fromLinear(dotProduct(m[0], tuple));
+		final float G = fromLinear(dotProduct(m[1], tuple));
+		// Tuple is being filled with output.
+		tuple[RGB_B] = fromLinear(dotProduct(m[2], tuple));
+		tuple[RGB_R] = R;
+		tuple[RGB_G] = G;
+	}
+
+	/**
+	 * Converts an XYZ tuple to an RGB one.
+	 */
+	public static float[] convertXyzToRgb(float xyzTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{xyzTuple[0], xyzTuple[1], xyzTuple[2]};
+		unsafeConvertXyzToRgb(result);
+		return result;
+	}
+
+	/**
+	 * Converts an RGB tuple to an XYZ one, altering the passed array to represent the output (discarding the input).
+	 */
+	private static void unsafeConvertRgbToXyz(float tuple[]) {
+		// Tuple represents input.
+		float rgbl[] = new float[]{toLinear(tuple[0]), toLinear(tuple[1]), toLinear(tuple[2])};
+		// Tuple is being filled with output.
+		tuple[XYZ_X] = dotProduct(m_inv[0], rgbl);
+		tuple[XYZ_Y] = dotProduct(m_inv[1], rgbl);
+		tuple[XYZ_Z] = dotProduct(m_inv[2], rgbl);
+	}
+
+	/**
+	 * Converts an RGB tuple to an XYZ one.
+	 */
+	public static float[] convertRgbToXyz(float rgbTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{rgbTuple[0], rgbTuple[1], rgbTuple[2]};
+		unsafeConvertRgbToXyz(result);
+		return result;
+	}
+
+	/**
+	 * Converts an XYZ tuple to an LUV one, altering the passed array to represent the output (discarding the input).
+	 */
+	private static void unsafeConvertXyzToLuv(float tuple[]) {
+		// Tuple represents input.
+		final float X = tuple[XYZ_X];
+		final float Y = tuple[XYZ_Y];
+		final float Z = tuple[XYZ_Z];
+		final float varU = 4 * X / (X + 15 * Y + 3 * Z);
+		final float varV = 9 * Y / (X + 15 * Y + 3 * Z);
+		// Tuple is being filled with output.
+		final float L;
+		// Black will create a divide-by-zero error.
+		if (0 == (L = 116 * f(Y / refY) - 16)) {
+			tuple[0] = tuple[1] = tuple[2] = 0;
+			return;
+		}
+		tuple[LUV_L] = L;
+		tuple[LUV_U] = 13 * L * (varU - refU);
+		tuple[LUV_V] = 13 * L * (varV - refV);
+	}
+
+	/**
+	 * Converts an XYZ tuple to an LUV one.
+	 */
+	public static float[] convertXyzToLuv(float xzyTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{xzyTuple[0], xzyTuple[1], xzyTuple[2]};
+		unsafeConvertXyzToLuv(result);
+		return result;
+	}
+
+	/**
+	 * Converts an LUV tuple to an XYZ one, altering the passed array to represent the output (discarding the input).
+	 */
+	private static void unsafeConvertLuvToXyz(float tuple[]) {
+		// Tuple represents input. Black will create a divide-by-zero error.
+		if (tuple[LUV_L] == 0) {
+			// Tuple is being filled with output. The X = L in the tuple is left untouched.
+			/* tuple[XYZ_X] = */ tuple[XYZ_Y] = tuple[XYZ_Z] = 0;
+			return;
+		}
+		final float L = tuple[LUV_L];
+		final float varY = f_inv((L + 16) / 116);
+		final float varU = tuple[LUV_U] / (13 * L) + refU;
+		final float varV = tuple[LUV_V] / (13 * L) + refV;
+		// Tuple is being filled with output.
+		final float Y = tuple[XYZ_Y] = varY * refY;
+		final float X = tuple[XYZ_X] = -9 * Y * varU / ((varU - 4) * varV - varU * varV);
+		/* final float Z = */ tuple[XYZ_Z] = (9 * Y - 15 * varV * Y - varV * X) / (3 * varV);
+	}
+
+	/**
+	 * Converts an LUV tuple to an XYZ one.
+	 */
+	public static float[] convertLuvToXyz(float luvTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{luvTuple[0], luvTuple[1], luvTuple[2]};
+		unsafeConvertLuvToXyz(result);
+		return result;
+	}
+
+	/**
+	 * Converts an LUV tuple to an LCH one, altering the passed array to represent the output (discarding the input).
+	 */
+	private static void unsafeConvertLuvToLch(float tuple[]) {
+		// Tuple represents input.
+		final float U = tuple[LUV_U];
+		final float V = tuple[LUV_V];
+		// Tuple is being filled with output. The L in the tuple is left untouched.
+		tuple[LCH_C] = (float) Math.sqrt(U * U + V * V);
+		final float Hrad = (float) Math.atan2(V, U);
+		float H = Hrad * 360 / 2 / PI;
+		if (H < 0) {
+			H += 360;
+		}
+		tuple[LCH_H] = H;
+	}
+
+	/**
+	 * Converts an LUV tuple to an LCH one.
+	 */
+	public static float[] convertLuvToLch(float luvTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{luvTuple[0], luvTuple[1], luvTuple[2]};
+		unsafeConvertLuvToLch(result);
+		return result;
+	}
+
+	/**
+	 * Converts an LCH tuple to an LUV one, altering the passed array to represent the output (discarding the input).
+	 */
+	private static void unsafeConvertLchToLuv(float tuple[]) {
+		// Tuple represents input.
+		final float C = tuple[LCH_C];
+		final float Hrad = tuple[LCH_H] / 360 * 2 * PI;
+		// Tuple is being filled with output. The L in the tuple is left untouched.
+		tuple[LUV_U] = (float) Math.cos(Hrad) * C;
+		tuple[LUV_V] = (float) Math.sin(Hrad) * C;
+	}
+
+	/**
+	 * Converts an LCH tuple to an LUV one.
+	 */
+	public static float[] convertLchToLuv(float lchTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{lchTuple[0], lchTuple[1], lchTuple[2]};
+		unsafeConvertLchToLuv(result);
+		return result;
+	}
+
+	/**
+	 * Converts an HUSL tuple to an LCH one, altering the passed array to represent the output (discarding the input).
+	 */
+	private static void unsafeConvertHuslToLch(float tuple[]) {
+		// Tuple represents input.
+		final float H = tuple[HUSL_H];
+		final float L = tuple[HUSL_L];
+		// Bad things happen when you reach a limit.
+		if (L > 99.9999f) {
+			// Tuple is being filled with output. 
+			tuple[LCH_L] = 100;
+			tuple[LCH_C] = 0;
+			tuple[LCH_H] = H;
+			return;
+		} else if (L < 0.00001f) {
+			// Tuple is being filled with output. 
+			tuple[LCH_L] = tuple[LCH_C] = 0;
+			tuple[LCH_H] = H;
+			return;
+		}
+		// Tuple is being filled with output.
+		// I already tried this scaling function to improve the chroma uniformity. It did not work very well.
+		// tuple[LCH_C] = Math.pow(tuple[HUSL_S] / 100,  1 / t) * maxChroma(L, H)
+		tuple[LCH_C] = maxChroma(L, H) / 100 * tuple[HUSL_S];
+		tuple[LCH_L] = L;
+		tuple[LCH_H] = H;
+	}
+
+	/**
+	 * Converts an HUSL tuple to an LCH one.
+	 */
+	public static float[] convertHuslToLch(float huslTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{huslTuple[0], huslTuple[1], huslTuple[2]};
+		unsafeConvertHuslToLch(result);
+		return result;
+	}
+
+	/**
+	 * Converts an LCH tuple to an HUSL one, altering the passed array to represent the output (discarding the input).
+	 */
+	private static void unsafeConvertLchToHusl(float tuple[]) {
+		// Tuple represents input.
+		final float L = tuple[LCH_L];
+		final float H = tuple[LCH_H];
+		// Bad things happen when you reach a limit.
+		if (L > 99.9999f) {
+			// Tuple is being filled with output. 
+			tuple[HUSL_H] = H;
+			tuple[HUSL_S] = 0;
+			tuple[HUSL_L] = 100;
+			return;
+		} else if (L < 0.00001f) {
+			// Tuple is being filled with output. 
+			tuple[HUSL_H] = H;
+			tuple[HUSL_S] = tuple[HUSL_L] = 0;
+			return;
+		}
+		// Tuple is being filled with output. 
+		tuple[HUSL_S] = tuple[LCH_C] / maxChroma(L, H) * 100;
+		tuple[HUSL_H] = H;
+		tuple[HUSL_L] = L;
+	}
+
+	/**
+	 * Converts an LCH tuple to an HUSL one.
+	 */
+	public static float[] convertLchToHusl(float lchTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{lchTuple[0], lchTuple[1], lchTuple[2]};
+		unsafeConvertLchToHusl(result);
+		return result;
+	}
+
+	/**
+	 * Converts an HUSL tuple to an RGB one.
+	 */
+	public static float[] convertHuslToRgb(float huslTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{huslTuple[0], huslTuple[1], huslTuple[2]};
+		// Calculate the LCH values.
+		unsafeConvertHuslToLch(result);
+		// Calculate the LUV values.
+		unsafeConvertLchToLuv(result);
+		// Calculate the XYZ values.
+		unsafeConvertLuvToXyz(result);
+		// Calculate the RGB values.
+		unsafeConvertXyzToRgb(result);
+		return result;
+	}
+
+	/**
+	 * Converts an RGB tuple to an HUSL one.
+	 */
+	public static float[] convertRgbToHusl(float rgbTuple[]) {
+		// Clone the tuple, to avoid changing the input.
+		final float[] result = new float[]{rgbTuple[0], rgbTuple[1], rgbTuple[2]};
+		// Calculate the XYZ values.
+		unsafeConvertRgbToXyz(result);
+		// Calculate the LUV values.
+		unsafeConvertXyzToLuv(result);
+		// Calculate the LCH values.
+		unsafeConvertLuvToLch(result);
+		// Calculate the HUSL values.
+		unsafeConvertLchToHusl(result);
+		return result;
+	}
+
+}
\ No newline at end of file
diff --git a/ports/java/README.md b/ports/java/README.md
new file mode 100644
index 0000000..f5a9709
--- /dev/null
+++ b/ports/java/README.md
@@ -0,0 +1,6 @@
+Tests added by @Pimm. Here's what he says about running them:
+
+> Providing Eclipse and [Android SDK](http://developer.android.com/sdk/index.html) are available:
+> One can run the tests by creating an Android project in Eclipse which somehow includes the `HuslConverter` class, and then create an Android test project which is configured to test the former project. There's documentation about this over at [developer.android.com](http://developer.android.com/tools/testing/testing_eclipse.html).
+> 
+> If no Android device is available, the tests can be run using an emulator (included in the SDK). It can of course be done with other IDEs, as well.
diff --git a/ports/java/test/res/raw/snapshot.json b/ports/java/test/res/raw/snapshot.json
new file mode 120000
index 0000000..2ead194
--- /dev/null
+++ b/ports/java/test/res/raw/snapshot.json
@@ -0,0 +1 @@
+../../../snapshot-2.x.x.json
\ No newline at end of file
diff --git a/ports/java/test/src/org/ilumbo/cover/test/ColorData.java b/ports/java/test/src/org/ilumbo/cover/test/ColorData.java
new file mode 100644
index 0000000..64ec514
--- /dev/null
+++ b/ports/java/test/src/org/ilumbo/cover/test/ColorData.java
@@ -0,0 +1,91 @@
+package org.ilumbo.cover.test;
+
+public final class ColorData {
+	public static final int PROPERTIES_RGB = 0;
+	public static final int PROPERTIES_RGB_XYZ = 1;
+	public static final int PROPERTIES_RGB_THROUGH_LUV = 2;
+	public static final int PROPERTIES_RGB_THROUGH_LCH = 3;
+	public static final int PROPERTIES_RGB_THROUGH_HUSL = 4;
+	public static final int PROPERTIES_RGB_THROUGH_HUSL_HUSLP = 5;
+	public final float[] husl;
+	public final float[] huslp;
+	public final float[] lch;
+	public final float[] luv;
+	public final float[] rgb;
+	public final float[] xyz;
+	public ColorData(float[] rgb, float[] xyz, float[] luv, float[] lch, float[] husl, float[] huslp) {
+		this.rgb = rgb;
+		this.xyz = xyz;
+		this.luv = luv;
+		this.lch = lch;
+		this.husl = husl;
+		this.huslp = huslp;
+	}
+	@Override
+	public final String toString() {
+		return toString(Integer.MAX_VALUE);
+	}
+	/**
+	 * Returns a string representation, containing the passed properties.
+	 */
+	public final String toString(int properties) {
+		final StringBuilder resultBuilder = new StringBuilder(64)
+			.append("[");
+		if (properties >= PROPERTIES_RGB) {
+			resultBuilder.append("[r: ")
+				.append(rgb[0])
+				.append(", g: ")
+				.append(rgb[1])
+				.append(", b: ")
+				.append(rgb[2])
+				.append("]");
+			if (properties >= PROPERTIES_RGB_XYZ) {
+				resultBuilder.append(" ↔ [x: ")
+					.append(xyz[0])
+					.append(", y: ")
+					.append(xyz[1])
+					.append(", z: ")
+					.append(xyz[2])
+					.append("]");
+				if (properties >= PROPERTIES_RGB_THROUGH_LUV) {
+					resultBuilder.append(" ↔ [l: ")
+						.append(luv[0])
+						.append(", u: ")
+						.append(luv[1])
+						.append(", v: ")
+						.append(luv[2])
+						.append("]");
+					if (properties >= PROPERTIES_RGB_THROUGH_LCH) {
+						resultBuilder.append(" ↔ [l: ")
+							.append(lch[0])
+							.append(", c: ")
+							.append(lch[1])
+							.append(", h: ")
+							.append(lch[2])
+							.append("]");
+						if (properties >= PROPERTIES_RGB_THROUGH_HUSL) {
+							resultBuilder.append(" ↔ [h: ")
+								.append(husl[0])
+								.append(", s: ")
+								.append(husl[1])
+								.append(", l: ")
+								.append(husl[2])
+								.append("]");
+							if (properties >= PROPERTIES_RGB_THROUGH_HUSL_HUSLP) {
+								resultBuilder.append(" ∨ [h: ")
+									.append(huslp[0])
+									.append(", s: ")
+									.append(huslp[1])
+									.append(", l: ")
+									.append(huslp[2])
+									.append("]");
+							}
+						}
+					}
+				}
+			}
+		}
+		return resultBuilder.append("]")
+				.toString();
+	}
+}
\ No newline at end of file
diff --git a/ports/java/test/src/org/ilumbo/cover/test/ColorDataBuffer.java b/ports/java/test/src/org/ilumbo/cover/test/ColorDataBuffer.java
new file mode 100644
index 0000000..55a7d54
--- /dev/null
+++ b/ports/java/test/src/org/ilumbo/cover/test/ColorDataBuffer.java
@@ -0,0 +1,79 @@
+package org.ilumbo.cover.test;
+
+public final class ColorDataBuffer {
+	private static final class Pointer {
+		public float[] array;
+		public int index;
+	}
+	private float[] bufferedHusl;
+	private float[] bufferedHuslp;
+	private float[] bufferedLch;
+	private float[] bufferedLuv;
+	private float[] bufferedRgb;
+	private float[] bufferedXyz;
+	private Pointer pointer;
+	public ColorDataBuffer() {
+		pointer = new Pointer();
+		reset();
+	}
+	/**
+	 * Builds the color data using the pushed values, and then resets this buffer so it can be re-used.
+	 */
+	public final ColorData build() {
+		final ColorData result = new ColorData(bufferedRgb, bufferedXyz, bufferedLuv, bufferedLch, bufferedHusl, bufferedHuslp);
+		reset();
+		return result;
+	}
+	private final void point(float[] array) {
+		pointer.array = array;
+		pointer.index = 0;
+	}
+	/**
+	 * Indicates the array of the color data which will be pushed to next. One must point to every array exactly once.
+	 */
+	public final void point(String name) {
+		if ("husl".equals(name)) {
+			throwIfNotNull(name, bufferedHusl);
+			point(bufferedHusl = new float[3]);
+		} else if ("huslp".equals(name)) {
+			throwIfNotNull(name, bufferedHuslp);
+			point(bufferedHuslp = new float[3]);
+		} else if ("lch".equals(name)) {
+			throwIfNotNull(name, bufferedLch);
+			point(bufferedLch = new float[3]);
+		} else if ("luv".equals(name)) {
+			throwIfNotNull(name, bufferedLuv);
+			point(bufferedLuv = new float[3]);
+		} else if ("rgb".equals(name)) {
+			throwIfNotNull(name, bufferedRgb);
+			point(bufferedRgb = new float[3]);
+		} else if ("xyz".equals(name)) {
+			throwIfNotNull(name, bufferedXyz);
+			point(bufferedXyz = new float[3]);
+		}
+	}
+	/**
+	 * Adds a value to the array previously pointed to.
+	 */
+	public final void push(float value) {
+		if (null == pointer.array) {
+			throw new IllegalStateException();
+		}
+		// Might throw an IndexOutOfBoundsException. That's OK.
+		pointer.array[pointer.index++] = value;
+	}
+	private final void reset() {
+		pointer.array = null;
+		bufferedHusl = null;
+		bufferedHuslp = null;
+		bufferedLch = null;
+		bufferedLuv = null;
+		bufferedRgb = null;
+		bufferedXyz = null;
+	}
+	private final void throwIfNotNull(String name, float[] array) {
+		if (null != array) {
+			throw new IllegalStateException("\"" + name + "\" was already pointed to.");
+		}
+	}
+}
\ No newline at end of file
diff --git a/ports/java/test/src/org/ilumbo/cover/test/Snapshot.java b/ports/java/test/src/org/ilumbo/cover/test/Snapshot.java
new file mode 100644
index 0000000..911be09
--- /dev/null
+++ b/ports/java/test/src/org/ilumbo/cover/test/Snapshot.java
@@ -0,0 +1,79 @@
+package org.ilumbo.cover.test;
+
+import java.util.Iterator;
+
+import android.util.SparseArray;
+
+/**
+ * A map which maps color strings (such as "##FFCC32") to color data objects.
+ */
+public final class Snapshot implements Iterable<Integer> {
+	private final class SnapshotIterator implements Iterator<Integer> {
+		private int index;
+		private int length;
+		public SnapshotIterator() {
+			index = 0;
+			length = members.size();
+		}
+		@Override
+		public final boolean hasNext() {
+			return index != length;
+		}
+		@Override
+		public final Integer next() {
+			return members.keyAt(index++);
+		}
+		@Override
+		public final void remove() {
+			throw new UnsupportedOperationException();
+		}
+	}
+	private final SparseArray<ColorData> members;
+	public Snapshot() {
+		members = new SparseArray<ColorData>(4096);
+	}
+	public final void addMember(String colorString, ColorData data) {
+		members.put(Integer.parseInt(colorString.substring(1), 0x10), data);
+	}
+	public final void addMember(int color, ColorData data) {
+		members.put(color, data);
+	}
+	@Override
+	public final Iterator<Integer> iterator() {
+		return new SnapshotIterator();
+	}
+	public final ColorData getMember(String colorString) {
+		return members.get(Integer.parseInt(colorString.substring(1)));
+	}
+	public final ColorData getMember(int color) {
+		return members.get(color);
+	}
+	@Override
+	public final String toString() {
+		final StringBuilder resultBuilder = new StringBuilder(1024)
+			.append("[");
+		final int memberCount = members.size();
+		for (int index = 0; memberCount != index; index++) {
+			resultBuilder.append("[#");
+			final String hexKey = Integer.toHexString(members.keyAt(index));
+			int missingCharacterCount = 6 - hexKey.length();
+			while (0 != missingCharacterCount--) {
+				resultBuilder.append("0");
+			}
+			resultBuilder.append(hexKey)
+				.append(": ")
+				.append(members.valueAt(index).toString())
+				.append("]");
+			if (index + 1 != memberCount) {
+				resultBuilder.append(", ");
+			}
+			// Return only the first 10 members. Otherwise, things take way too long.
+			if (9 == index) {
+				resultBuilder.append("…");
+				break;
+			}
+		}
+		return resultBuilder.append("]")
+				.toString();
+	}
+}
\ No newline at end of file
diff --git a/ports/java/test/src/org/ilumbo/cover/test/SnapshotTestCase.java b/ports/java/test/src/org/ilumbo/cover/test/SnapshotTestCase.java
new file mode 100644
index 0000000..1b438d5
--- /dev/null
+++ b/ports/java/test/src/org/ilumbo/cover/test/SnapshotTestCase.java
@@ -0,0 +1,157 @@
+package org.ilumbo.cover.test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import android.test.ActivityTestCase;
+import android.util.JsonReader;
+import android.util.JsonToken;
+
+import com.boronine.husl.HuslConverter;
+
+public final class SnapshotTestCase extends ActivityTestCase {
+	/**
+	 * The "current" (or actual) snapshot.
+	 */
+	private Snapshot currentSnapshot;
+	/**
+	 * The "stable" (or expected) snapshot.
+	 */
+	private Snapshot stableSnapshot;
+	private final Snapshot loadStableSnapshot() {
+		// Get the stream that reads the stable snapshot file.
+		final InputStream inputStream = getInstrumentation().getContext().getResources().openRawResource(R.raw.snapshot);
+		// Create a reader that reads the stable snapshot file and decodes it as JSON.
+		final JsonReader inputReader = new JsonReader(new InputStreamReader(inputStream));
+		// Turn the JSON into an object.
+		final Snapshot snapshot = new Snapshot();
+		try {
+			inputReader.beginObject();
+			final ColorDataBuffer colorDataBuffer = new ColorDataBuffer();
+			while (JsonToken.END_OBJECT != inputReader.peek()) {
+				final String colorString = inputReader.nextName();
+				inputReader.beginObject();
+				while (JsonToken.END_OBJECT != inputReader.peek()) {
+					colorDataBuffer.point(inputReader.nextName());
+					inputReader.beginArray();
+					while (JsonToken.END_ARRAY != inputReader.peek()) {
+						colorDataBuffer.push((float) inputReader.nextDouble());
+					}
+					inputReader.endArray();
+				}
+				snapshot.addMember(colorString, colorDataBuffer.build());
+				inputReader.endObject();
+			}
+			inputReader.endObject();
+		} catch (IOException exception) {
+			exception.printStackTrace();
+		}
+		return snapshot;
+	}
+	@Override
+	protected final void setUp() throws Exception {
+		if (null != stableSnapshot && null != currentSnapshot) {
+			return;
+		}
+		// Load the stable snapshot.
+		stableSnapshot = loadStableSnapshot();
+		// Generate the "current" (or actual) snapshot.
+		currentSnapshot = new Snapshot();
+		final char[] hexadecimals = "0123456789ABCDEF".toCharArray();
+		for (int red = 0; 16 != red; red++) {
+			for (int green = 0; 16 != green; green++) {
+				for (int blue = 0; 16 != blue; blue++) {
+					final String rgbString = new StringBuilder(7)
+						.append("#")
+						.append(hexadecimals[red]).append(hexadecimals[red])
+						.append(hexadecimals[green]).append(hexadecimals[green])
+						.append(hexadecimals[blue]).append(hexadecimals[blue])
+						.toString();
+					final float rgb[] = new float[]{((red << 4) | red) / 255f, ((green << 4) | green) / 255f, ((blue << 4) | blue) / 255f};
+					final float xyz[] = HuslConverter.convertRgbToXyz(rgb);
+					final float luv[] = HuslConverter.convertXyzToLuv(xyz);
+					final float lch[] = HuslConverter.convertLuvToLch(luv);
+					final float husl[] = HuslConverter.convertLchToHusl(lch);
+					// The Java port doesn't convert to HUSLp, so simply use the HUSL value. This will cause the test to fail.
+					final float huslp[] = husl;
+					currentSnapshot.addMember(rgbString,
+							new ColorData(rgb, xyz, luv, lch, husl, huslp));
+				}
+			}
+		}
+	}
+	public final void testStep0Rgb() {
+		// This is kind-of a sanity check.
+		for (final int color : currentSnapshot) {
+			final ColorData stable = stableSnapshot.getMember(color);
+			String message = "color: " + Integer.toHexString(color);
+			assertNotNull(message, stable);
+			final ColorData current = currentSnapshot.getMember(color);
+			message += ", current: " + current.toString(ColorData.PROPERTIES_RGB);
+			assertEquals(message, stable.rgb[0], current.rgb[0], 1e-3f);
+			assertEquals(message, stable.rgb[1], current.rgb[1], 1e-3f);
+			assertEquals(message, stable.rgb[2], current.rgb[2], 1e-3f);
+		}
+	}
+	public final void testStep1Xyz() {
+		for (final int color : currentSnapshot) {
+			final ColorData stable = stableSnapshot.getMember(color);
+			String message = "color: " + Integer.toHexString(color);
+			assertNotNull(message, stable);
+			final ColorData current = currentSnapshot.getMember(color);
+			message += ", current: " + current.toString(ColorData.PROPERTIES_RGB_XYZ);
+			assertEquals(message, stable.xyz[0], current.xyz[0], 1e-3f);
+			assertEquals(message, stable.xyz[1], current.xyz[1], 1e-3f);
+			assertEquals(message, stable.xyz[2], current.xyz[2], 1e-3f);
+		}
+	}
+	public final void testStep2Luv() {
+		for (final int color : currentSnapshot) {
+			final ColorData stable = stableSnapshot.getMember(color);
+			String message = "color: " + Integer.toHexString(color);
+			assertNotNull(message, stable);
+			final ColorData current = currentSnapshot.getMember(color);
+			message += ", current: " + current.toString(ColorData.PROPERTIES_RGB_THROUGH_LUV);
+			assertEquals(message, stable.luv[0], current.luv[0], 1e-1f);
+			assertEquals(message, stable.luv[1], current.luv[1], 1e-1f);
+			assertEquals(message, stable.luv[2], current.luv[2], 1e-1f);
+		}
+	}
+	public final void testStep3Lch() {
+		for (final int color : currentSnapshot) {
+			final ColorData stable = stableSnapshot.getMember(color);
+			String message = "Color: " + Integer.toHexString(color);
+			assertNotNull(message, stable);
+			final ColorData current = currentSnapshot.getMember(color);
+			message += ", current: " + current.toString(ColorData.PROPERTIES_RGB_THROUGH_LCH);
+			assertEquals(message, stable.lch[0], current.lch[0], 1e-1f);
+			assertEquals(message, stable.lch[1], current.lch[1], 1e-1f);
+			assertEquals(message, stable.lch[2], current.lch[2], 1e-1f);
+		}
+	}
+	public final void testStep4Husl() {
+		for (final int color : currentSnapshot) {
+			final ColorData stable = stableSnapshot.getMember(color);
+			String message = "Color: " + Integer.toHexString(color);
+			assertNotNull(message, stable);
+			final ColorData current = currentSnapshot.getMember(color);
+			message += ", current: " + current.toString(ColorData.PROPERTIES_RGB_THROUGH_HUSL);
+			assertEquals(message, stable.husl[0], current.husl[0], 1e-1f);
+			assertEquals(message, stable.husl[1], current.husl[1], 1e-1f);
+			assertEquals(message, stable.husl[2], current.husl[2], 1e-1f);
+		}
+	}
+	public final void testStep4Huslp() {
+		for (final int color : currentSnapshot) {
+			final ColorData stable = stableSnapshot.getMember(color);
+			String message = "Color: " + Integer.toHexString(color);
+			assertNotNull(message, stable);
+			final ColorData current = currentSnapshot.getMember(color);
+			message += ", current: " + current.toString(ColorData.PROPERTIES_RGB_THROUGH_HUSL_HUSLP);
+			assertEquals(message, stable.huslp[0], current.huslp[0], 1e-3f);
+			assertEquals(message, stable.huslp[1], current.huslp[1], 1e-3f);
+			assertEquals(message, stable.huslp[2], current.huslp[2], 1e-3f);
+		}
+	}
+}
\ No newline at end of file
diff --git a/test/snapshot-rev0.json b/test/snapshot-rev0.json
new file mode 100644
index 0000000..812ab59
--- /dev/null
+++ b/test/snapshot-rev0.json
@@ -0,0 +1 @@
+{"#000000":{"rgb":[0,0,0],"xyz":[0,0,0],"luv":[0,0,0],"lch":[0,0,0],"husl":[0,0,0],"huslp":[0,0,0]},"#000011":{"rgb":[0,0,0.06666666666666667],"xyz":[0.0010117731881685915,0.0004047092752674366,0.005327924738804688],"luv":[0.36557065067487216,-0.1063797978389175,-1.4752928579415854],"lch":[0.36557065067487216,1.479123280217609,265.87567824971296],"husl":[265.87567824971296,100.0046360785146,0.36557065067487216],"huslp":[265.87567824971296,513.3429528121093,0.36557065067487216]},"#000022" [...]
\ No newline at end of file
diff --git a/test/snapshot-rev1.json b/test/snapshot-rev1.json
new file mode 100644
index 0000000..a9a5076
--- /dev/null
+++ b/test/snapshot-rev1.json
@@ -0,0 +1 @@
+{"#000000":{"rgb":[0,0,0],"xyz":[0,0,0],"luv":[0,0,0],"lch":[0,0,0],"husl":[0,0,0],"huslp":[0,0,0]},"#000011":{"rgb":[0,0,0.06666666666666667],"xyz":[0.0010114227573936414,0.00040456910295747335,0.005326826522273399],"luv":[0.36544577229740066,-0.10642364443796126,-1.4748442523501146],"lch":[0.36544577229740066,1.4786789917982963,265.8727335161398],"husl":[265.8727335161398,100.00000000000901,0.36544577229740066],"huslp":[265.8727335161398,368.46709896811126,0.36544577229740066]},"#00002 [...]
\ No newline at end of file
diff --git a/test/snapshot-rev2.json b/test/snapshot-rev2.json
new file mode 100644
index 0000000..fbd2eb9
--- /dev/null
+++ b/test/snapshot-rev2.json
@@ -0,0 +1 @@
+{"#000000":{"rgb":[0,0,0],"xyz":[0,0,0],"luv":[0,0,0],"lch":[0,0,0],"husl":[0,0,0],"huslp":[0,0,0]},"#000011":{"rgb":[0,0,0.06666666666666667],"xyz":[0.0010114227573936414,0.00040456910295747335,0.005326826522273399],"luv":[0.36544577229740066,-0.10642364443796126,-1.4748442523501146],"lch":[0.36544577229740066,1.4786789917982963,265.8727335161398],"husl":[265.8727335161398,100.000000000009,0.36544577229740066],"huslp":[265.8727335161398,513.3411079125324,0.36544577229740066]},"#000022": [...]
\ No newline at end of file
diff --git a/test/snapshot-rev3.json b/test/snapshot-rev3.json
new file mode 100644
index 0000000..234e23b
--- /dev/null
+++ b/test/snapshot-rev3.json
@@ -0,0 +1 @@
+{"#000000":{"rgb":[0,0,0],"xyz":[0,0,0],"luv":[0,0,0],"lch":[0,0,0],"husl":[0,0,0],"huslp":[0,0,0]},"#000011":{"rgb":[0,0,0.06666666666666667],"xyz":[0.0010116654996371217,0.0004046661998548544,0.0053281049647556315],"luv":[0.36553347952621895,-0.10640253083479542,-1.4751207214237791],"lch":[0.36553347952621895,1.478953224866108,265.8743202181779],"husl":[265.8743202181779,100.00000000000087,0.36553347952621895],"huslp":[265.8743202181779,513.4126968442804,0.36553347952621895]},"#000022" [...]
diff --git a/test/snapshot-rev4.json b/test/snapshot-rev4.json
new file mode 100644
index 0000000..f135c9c
--- /dev/null
+++ b/test/snapshot-rev4.json
@@ -0,0 +1 @@
+{"#000000":{"rgb":[0,0,0],"xyz":[0,0,0],"luv":[0,0,0],"lch":[0,0,0],"husl":[0,0,0],"huslp":[0,0,0]},"#000011":{"rgb":[0,0,0.06666666666666667],"xyz":[0.0010116654996371458,0.00040466619985485833,0.005328104964755635],"luv":[0.3655334795651904,-0.10640253084615538,-1.4751207215810498],"lch":[0.3655334795651904,1.4789532250237882,265.87432021817733],"husl":[265.87432021817733,100.00000000000003,0.3655334795651904],"huslp":[265.87432021817733,513.4126968442829,0.3655334795651904]},"#000022" [...]
diff --git a/test/snapshot.coffee b/test/snapshot.coffee
new file mode 100644
index 0000000..266e77d
--- /dev/null
+++ b/test/snapshot.coffee
@@ -0,0 +1,57 @@
+husl = require '../husl.coffee'
+
+digits = '0123456789abcdef'
+
+snapshot = ->
+  samples = {}
+
+  # Take 16 ^ 3 = 4096 samples
+  for r in digits
+    for g in digits
+      for b in digits
+        hex = '#' + r + r + g + g + b + b
+        rgb = husl._conv.hex.rgb hex
+        xyz = husl._conv.rgb.xyz rgb
+        luv = husl._conv.xyz.luv xyz
+        lch = husl._conv.luv.lch luv
+        samples[hex] =
+          rgb: rgb
+          xyz: xyz
+          luv: luv
+          lch: lch
+          husl: husl._conv.lch.husl lch
+          huslp: husl._conv.lch.huslp lch
+
+  return samples
+
+testPrecision = (numDigits) ->
+  # Test how many digits of HUSL decimal precision is enough to unambiguously
+  # specify a hex-encoded RGB color. Spoiler alert: it's 4.
+  # Adapted from: https://gist.github.com/roryokane/f15bb23abcf9938c0707
+  for r1 in digits
+    for g1 in digits
+      for b1 in digits
+        # Assuming that only the least significant hex digit can cause a
+        # collision. Otherwise this program uses too much memory.
+        console.log "Testing #" + r1 + "_" + g1 + "_" + b1 + "_"
+        accum = {}
+        for r2 in digits
+          for g2 in digits
+            for b2 in digits
+              hex = '#' + r1 + r2 + g1 + g2 + b1 + b2
+              hsl = husl.fromHex(hex)
+              key = [ch.toFixed(numDigits) for ch in hsl].join('|')
+              if accum[key]
+                console.log "FOUND COLLISION:"
+                console.log hex, accum[key]
+                console.log key
+                return
+              else
+                accum[key] = hex
+
+module.exports =
+  snapshot: snapshot
+  testPrecision: testPrecision
+
+if require.main == module
+  console.log JSON.stringify snapshot()
diff --git a/test/test.coffee b/test/test.coffee
new file mode 100644
index 0000000..dbc37c0
--- /dev/null
+++ b/test/test.coffee
@@ -0,0 +1,61 @@
+assert = require 'assert'
+husl = require '../husl.coffee'
+meta = require '../package.json'
+snapshot = require './snapshot.coffee'
+_ = require 'underscore'
+
+describe 'HUSL consistency', ->
+  manySamples = (assertion) ->
+    samples = '0123456789abcdef'
+    for r in samples
+      for g in samples
+        for b in samples
+          assertion '#' + r + r + g + g + b + b
+
+  it 'should convert between HUSL and hex', ->
+    manySamples (hex) ->
+      assert.deepEqual hex, husl.toHex (husl.fromHex hex)...
+  it 'should convert between HUSLp and hex', ->
+    manySamples (hex) ->
+      assert.deepEqual hex, husl.p.toHex (husl.p.fromHex hex)...
+
+rgbRangeTolerance = 0.00000000001
+snapshotTolerance = 0.00000000001
+
+describe 'Fits within RGB ranges', ->
+  it 'should fit', ->
+    for H in (n for n in [0..360] by 5)
+      for S in (n for n in [0..100] by 5)
+        for L in (n for n in [0..100] by 5)
+          RGB = husl.toRGB H, S, L
+          for channel in RGB
+            assert -rgbRangeTolerance <= channel <= 1 + rgbRangeTolerance, "HUSL: #{[H, S, L]} -> #{RGB}"
+
+          RGB = husl.p.toRGB H, S, L
+          for channel in RGB
+            assert -rgbRangeTolerance <= channel <= 1 + rgbRangeTolerance, "HUSLp: #{[H, S, L]} -> #{RGB}"
+
+
+describe 'HUSL snapshot', ->
+
+  it 'should match the stable snapshot', ->
+    @timeout 10000
+
+    current = snapshot.snapshot()
+    stable = require './snapshot-rev4.json'
+
+    for hex, stableSamples of stable
+      currentSamples = current[hex]
+      for tag, stableTuple of stableSamples
+        currentTuple = currentSamples[tag]
+        for i in [0..2]
+          diff = Math.abs currentTuple[i] - stableTuple[i]
+          assert (diff < snapshotTolerance), """
+            The snapshots for #{hex} don't match at #{tag}
+            Stable:  #{stableTuple}
+            Current: #{currentTuple}
+            """
+
+
+
+

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-husl.git



More information about the Pkg-javascript-commits mailing list