Skip to content

Commit

Permalink
Merge pull request #776 from adroitwhiz/test-color-conversions
Browse files Browse the repository at this point in the history
Add unit tests for color conversion functions
  • Loading branch information
fsih authored Mar 2, 2021
2 parents da21131 + a930503 commit 9560de4
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 96 deletions.
97 changes: 1 addition & 96 deletions src/EffectTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

const twgl = require('twgl.js');

const {rgbToHsv, hsvToRgb} = require('./util/color-conversions');
const ShaderManager = require('./ShaderManager');

/**
Expand All @@ -26,102 +27,6 @@ const CENTER_Y = 0.5;
*/
const __hsv = [0, 0, 0];

/**
* Converts an RGB color value to HSV. Conversion formula
* adapted from http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv.
* Assumes r, g, and b are in the range [0, 255] and
* returns h, s, and v in the range [0, 1].
*
* @param {Array<number>} rgb The RGB color value
* @param {number} rgb.r The red color value
* @param {number} rgb.g The green color value
* @param {number} rgb.b The blue color value
* @param {Array<number>} dst The array to store the RGB values in
* @return {Array<number>} The `dst` array passed in
*/
const rgbToHsv = ([r, g, b], dst) => {
let K = 0.0;

r /= 255;
g /= 255;
b /= 255;
let tmp = 0;

if (g < b) {
tmp = g;
g = b;
b = tmp;

K = -1;
}

if (r < g) {
tmp = r;
r = g;
g = tmp;

K = (-2 / 6) - K;
}

const chroma = r - Math.min(g, b);
const h = Math.abs(K + ((g - b) / ((6 * chroma) + Number.EPSILON)));
const s = chroma / (r + Number.EPSILON);
const v = r;

dst[0] = h;
dst[1] = s;
dst[2] = v;

return dst;
};

/**
* Converts an HSV color value to RGB. Conversion formula
* adapted from https://gist.github.com/mjackson/5311256.
* Assumes h, s, and v are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {Array<number>} hsv The HSV color value
* @param {number} hsv.h The hue
* @param {number} hsv.s The saturation
* @param {number} hsv.v The value
* @param {Uint8Array|Uint8ClampedArray} dst The array to store the RGB values in
* @return {Uint8Array|Uint8ClampedArray} The `dst` array passed in
*/
const hsvToRgb = ([h, s, v], dst) => {
if (s === 0) {
dst[0] = dst[1] = dst[2] = (v * 255) + 0.5;
return dst;
}

// keep hue in [0,1) so the `switch(i)` below only needs 6 cases (0-5)
h %= 1;
const i = (h * 6) | 0;
const f = (h * 6) - i;
const p = v * (1 - s);
const q = v * (1 - (s * f));
const t = v * (1 - (s * (1 - f)));

let r = 0;
let g = 0;
let b = 0;

switch (i) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}

// Add 0.5 in order to round. Setting integer TypedArray elements implicitly floors.
dst[0] = (r * 255) + 0.5;
dst[1] = (g * 255) + 0.5;
dst[2] = (b * 255) + 0.5;
return dst;
};

class EffectTransform {

/**
Expand Down
97 changes: 97 additions & 0 deletions src/util/color-conversions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Converts an RGB color value to HSV. Conversion formula
* adapted from http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv.
* Assumes r, g, and b are in the range [0, 255] and
* returns h, s, and v in the range [0, 1].
*
* @param {Array<number>} rgb The RGB color value
* @param {number} rgb.r The red color value
* @param {number} rgb.g The green color value
* @param {number} rgb.b The blue color value
* @param {Array<number>} dst The array to store the HSV values in
* @return {Array<number>} The `dst` array passed in
*/
const rgbToHsv = ([r, g, b], dst) => {
let K = 0.0;

r /= 255;
g /= 255;
b /= 255;
let tmp = 0;

if (g < b) {
tmp = g;
g = b;
b = tmp;

K = -1;
}

if (r < g) {
tmp = r;
r = g;
g = tmp;

K = (-2 / 6) - K;
}

const chroma = r - Math.min(g, b);
const h = Math.abs(K + ((g - b) / ((6 * chroma) + Number.EPSILON)));
const s = chroma / (r + Number.EPSILON);
const v = r;

dst[0] = h;
dst[1] = s;
dst[2] = v;

return dst;
};

/**
* Converts an HSV color value to RGB. Conversion formula
* adapted from https://gist.github.com/mjackson/5311256.
* Assumes h, s, and v are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {Array<number>} hsv The HSV color value
* @param {number} hsv.h The hue
* @param {number} hsv.s The saturation
* @param {number} hsv.v The value
* @param {Uint8Array|Uint8ClampedArray} dst The array to store the RGB values in
* @return {Uint8Array|Uint8ClampedArray} The `dst` array passed in
*/
const hsvToRgb = ([h, s, v], dst) => {
if (s === 0) {
dst[0] = dst[1] = dst[2] = (v * 255) + 0.5;
return dst;
}

// keep hue in [0,1) so the `switch(i)` below only needs 6 cases (0-5)
h %= 1;
const i = (h * 6) | 0;
const f = (h * 6) - i;
const p = v * (1 - s);
const q = v * (1 - (s * f));
const t = v * (1 - (s * (1 - f)));

let r = 0;
let g = 0;
let b = 0;

switch (i) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}

// Add 0.5 in order to round. Setting integer TypedArray elements implicitly floors.
dst[0] = (r * 255) + 0.5;
dst[1] = (g * 255) + 0.5;
dst[2] = (b * 255) + 0.5;
return dst;
};

module.exports = {rgbToHsv, hsvToRgb};
48 changes: 48 additions & 0 deletions test/unit/ColorConversionTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const {test, Test} = require('tap');

const {rgbToHsv, hsvToRgb} = require('../../src/util/color-conversions');

Test.prototype.addAssert('colorsAlmostEqual', 2, function (found, wanted, message, extra) {
/* eslint-disable no-invalid-this */
message += `: found ${JSON.stringify(Array.from(found))}, wanted ${JSON.stringify(Array.from(wanted))}`;

// should always return another assert call, or
// this.pass(message) or this.fail(message, extra)
if (found.length !== wanted.length) {
return this.fail(message, extra);
}

for (let i = 0; i < found.length; i++) {
// smallest meaningful difference--detects changes in hue value after rounding
if (Math.abs(found[i] - wanted[i]) >= 0.5 / 360) {
return this.fail(message, extra);
}
}

return this.pass(message);
/* eslint-enable no-invalid-this */
});

test('RGB to HSV', t => {
const dst = [0, 0, 0];
t.colorsAlmostEqual(rgbToHsv([255, 255, 255], dst), [0, 0, 1], 'white');
t.colorsAlmostEqual(rgbToHsv([0, 0, 0], dst), [0, 0, 0], 'black');
t.colorsAlmostEqual(rgbToHsv([127, 127, 127], dst), [0, 0, 0.498], 'grey');
t.colorsAlmostEqual(rgbToHsv([255, 255, 0], dst), [0.167, 1, 1], 'yellow');
t.colorsAlmostEqual(rgbToHsv([1, 0, 0], dst), [0, 1, 0.00392], 'dark red');

t.end();
});

test('HSV to RGB', t => {
const dst = new Uint8ClampedArray(3);
t.colorsAlmostEqual(hsvToRgb([0, 1, 1], dst), [255, 0, 0], 'red');
t.colorsAlmostEqual(hsvToRgb([1, 1, 1], dst), [255, 0, 0], 'red (hue of 1)');
t.colorsAlmostEqual(hsvToRgb([0.5, 1, 1], dst), [0, 255, 255], 'cyan');
t.colorsAlmostEqual(hsvToRgb([1.5, 1, 1], dst), [0, 255, 255], 'cyan (hue of 1.5)');
t.colorsAlmostEqual(hsvToRgb([0, 0, 0], dst), [0, 0, 0], 'black');
t.colorsAlmostEqual(hsvToRgb([0.5, 1, 0], dst), [0, 0, 0], 'black (with hue and saturation)');
t.colorsAlmostEqual(hsvToRgb([0, 1, 0.00392], dst), [1, 0, 0], 'dark red');

t.end();
});

0 comments on commit 9560de4

Please sign in to comment.