[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