Skip to content

Commit 50ff576

Browse files
authored
Merge pull request #542 from adroitwhiz/effect-transform-parity
Update EffectTransform.transformColor to match GPU "color" + "brightness" effects
2 parents 954cfff + 310f6b5 commit 50ff576

File tree

1 file changed

+103
-87
lines changed

1 file changed

+103
-87
lines changed

src/EffectTransform.js

Lines changed: 103 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -20,95 +20,106 @@ const CENTER_X = 0.5;
2020
*/
2121
const CENTER_Y = 0.5;
2222

23-
// color conversions grabbed from https://gist.github.com/mjackson/5311256
23+
/**
24+
* Reused memory location for storing an HSV color value.
25+
* @type {Array<number>}
26+
*/
27+
const __hsv = [0, 0, 0];
2428

2529
/**
26-
* Converts an RGB color value to HSL. Conversion formula
27-
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
28-
* Assumes r, g, and b are contained in the set [0, 255] and
29-
* returns h, s, and l in the set [0, 1].
30+
* Converts an RGB color value to HSV. Conversion formula
31+
* adapted from http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv.
32+
* Assumes r, g, and b are in the range [0, 255] and
33+
* returns h, s, and v in the range [0, 1].
3034
*
31-
* @param {number} r The red color value
32-
* @param {number} g The green color value
33-
* @param {number} b The blue color value
34-
* @return {Array} The HSL representation
35+
* @param {Array<number>} rgb The RGB color value
36+
* @param {number} rgb.r The red color value
37+
* @param {number} rgb.g The green color value
38+
* @param {number} rgb.b The blue color value
39+
* @param {Array<number>} dst The array to store the RGB values in
40+
* @return {Array<number>} The `dst` array passed in
3541
*/
36-
const rgbToHsl = ([r, g, b]) => {
42+
const rgbToHsv = ([r, g, b], dst) => {
43+
let K = 0.0;
44+
3745
r /= 255;
3846
g /= 255;
3947
b /= 255;
48+
let tmp = 0;
4049

41-
const max = Math.max(r, g, b);
42-
const min = Math.min(r, g, b);
43-
let h;
44-
let s;
45-
const l = (max + min) / 2;
46-
47-
if (max === min) {
48-
h = s = 0; // achromatic
49-
} else {
50-
const d = max - min;
51-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
52-
53-
switch (max) {
54-
case r: h = ((g - b) / d) + (g < b ? 6 : 0); break;
55-
case g: h = ((b - r) / d) + 2; break;
56-
case b: h = ((r - g) / d) + 4; break;
57-
}
50+
if (g < b) {
51+
tmp = g;
52+
g = b;
53+
b = tmp;
5854

59-
h /= 6;
55+
K = -1;
6056
}
6157

62-
return [h, s, l];
63-
};
58+
if (r < g) {
59+
tmp = r;
60+
r = g;
61+
g = tmp;
6462

65-
/**
66-
* Helper function for hslToRgb is called with varying 't' values to get
67-
* red green and blue values from the p/q/t color space calculations
68-
* @param {number} p vector coordinates
69-
* @param {number} q vector coordinates
70-
* @param {number} t vector coordinates
71-
* @return {number} amount of r/g/b byte
72-
*/
73-
const hue2rgb = (p, q, t) => {
74-
if (t < 0) t += 1;
75-
if (t > 1) t -= 1;
76-
if (t < 1 / 6) return p + ((q - p) * 6 * t);
77-
if (t < 1 / 2) return q;
78-
if (t < 2 / 3) return p + ((q - p) * ((2 / 3) - t) * 6);
79-
return p;
80-
};
63+
K = (-2 / 6) - K;
64+
}
65+
66+
const chroma = r - Math.min(g, b);
67+
const h = Math.abs(K + ((g - b) / ((6 * chroma) + Number.EPSILON)));
68+
const s = chroma / (r + Number.EPSILON);
69+
const v = r;
8170

71+
dst[0] = h;
72+
dst[1] = s;
73+
dst[2] = v;
74+
75+
return dst;
76+
};
8277

8378
/**
84-
* Converts an HSL color value to RGB. Conversion formula
85-
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
86-
* Assumes h, s, and l are contained in the set [0, 1] and
79+
* Converts an HSV color value to RGB. Conversion formula
80+
* adapted from https://gist.github.com/mjackson/5311256.
81+
* Assumes h, s, and v are contained in the set [0, 1] and
8782
* returns r, g, and b in the set [0, 255].
8883
*
89-
* @param {number} h The hue
90-
* @param {number} s The saturation
91-
* @param {number} l The lightness
92-
* @return {Array} The RGB representation
84+
* @param {Array<number>} hsv The HSV color value
85+
* @param {number} hsv.h The hue
86+
* @param {number} hsv.s The saturation
87+
* @param {number} hsv.v The value
88+
* @param {Uint8Array|Uint8ClampedArray} dst The array to store the RGB values in
89+
* @return {Uint8Array|Uint8ClampedArray} The `dst` array passed in
9390
*/
94-
const hslToRgb = ([h, s, l]) => {
95-
let r;
96-
let g;
97-
let b;
98-
91+
const hsvToRgb = ([h, s, v], dst) => {
9992
if (s === 0) {
100-
r = g = b = l; // achromatic
101-
} else {
102-
103-
const q = l < 0.5 ? l * (1 + s) : l + s - (l * s);
104-
const p = (2 * l) - q;
93+
dst[0] = dst[1] = dst[2] = (v * 255) + 0.5;
94+
return dst;
95+
}
10596

106-
r = hue2rgb(p, q, h + (1 / 3));
107-
g = hue2rgb(p, q, h);
108-
b = hue2rgb(p, q, h - (1 / 3));
97+
// keep hue in [0,1) so the `switch(i)` below only needs 6 cases (0-5)
98+
h %= 1;
99+
const i = (h * 6) | 0;
100+
const f = (h * 6) - i;
101+
const p = v * (1 - s);
102+
const q = v * (1 - (s * f));
103+
const t = v * (1 - (s * (1 - f)));
104+
105+
let r = 0;
106+
let g = 0;
107+
let b = 0;
108+
109+
switch (i) {
110+
case 0: r = v; g = t; b = p; break;
111+
case 1: r = q; g = v; b = p; break;
112+
case 2: r = p; g = v; b = t; break;
113+
case 3: r = p; g = q; b = v; break;
114+
case 4: r = t; g = p; b = v; break;
115+
case 5: r = v; g = p; b = q; break;
109116
}
110117

111-
return [r * 255, g * 255, b * 255];
118+
// Add 0.5 in order to round. Setting integer TypedArray elements implicitly floors.
119+
dst[0] = (r * 255) + 0.5;
120+
dst[1] = (g * 255) + 0.5;
121+
dst[2] = (b * 255) + 0.5;
122+
return dst;
112123
};
113124

114125
class EffectTransform {
@@ -146,38 +157,43 @@ class EffectTransform {
146157
inOutColor[1] /= alpha;
147158
inOutColor[2] /= alpha;
148159

149-
// vec3 hsl = convertRGB2HSL(gl_FragColor.xyz);
150-
const hsl = rgbToHsl(inOutColor);
151-
152160
if (enableColor) {
161+
// vec3 hsv = convertRGB2HSV(gl_FragColor.xyz);
162+
const hsv = rgbToHsv(inOutColor, __hsv);
163+
153164
// this code forces grayscale values to be slightly saturated
154165
// so that some slight change of hue will be visible
155166
// const float minLightness = 0.11 / 2.0;
156-
const minL = 0.11 / 2.0;
167+
const minV = 0.11 / 2.0;
157168
// const float minSaturation = 0.09;
158169
const minS = 0.09;
159-
// if (hsl.z < minLightness) hsl = vec3(0.0, 1.0, minLightness);
160-
if (hsl[2] < minL) {
161-
hsl[0] = 0;
162-
hsl[1] = 1;
163-
hsl[2] = minL;
164-
// else if (hsl.y < minSaturation) hsl = vec3(0.0, minSaturation, hsl.z);
165-
} else if (hsl[1] < minS) {
166-
hsl[0] = 0;
167-
hsl[1] = minS;
170+
// if (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness);
171+
if (hsv[2] < minV) {
172+
hsv[0] = 0;
173+
hsv[1] = 1;
174+
hsv[2] = minV;
175+
// else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z);
176+
} else if (hsv[1] < minS) {
177+
hsv[0] = 0;
178+
hsv[1] = minS;
168179
}
169180

170-
// hsl.x = mod(hsl.x + u_color, 1.0);
171-
// if (hsl.x < 0.0) hsl.x += 1.0;
172-
hsl[0] = (uniforms.u_color + hsl[0] + 1) % 1;
181+
// hsv.x = mod(hsv.x + u_color, 1.0);
182+
// if (hsv.x < 0.0) hsv.x += 1.0;
183+
hsv[0] = (uniforms.u_color + hsv[0] + 1);
184+
185+
// gl_FragColor.rgb = convertHSV2RGB(hsl);
186+
hsvToRgb(hsv, inOutColor);
173187
}
174188

175189
if (enableBrightness) {
176-
// hsl.z = clamp(hsl.z + u_brightness, 0.0, 1.0);
177-
hsl[2] = Math.min(1, hsl[2] + uniforms.u_brightness);
190+
const brightness = uniforms.u_brightness * 255;
191+
// gl_FragColor.rgb = clamp(gl_FragColor.rgb + vec3(u_brightness), vec3(0), vec3(1));
192+
// We don't need to clamp because the Uint8ClampedArray does that for us
193+
inOutColor[0] += brightness;
194+
inOutColor[1] += brightness;
195+
inOutColor[2] += brightness;
178196
}
179-
// gl_FragColor.rgb = convertHSL2RGB(hsl);
180-
inOutColor.set(hslToRgb(hsl));
181197

182198
// gl_FragColor.rgb *= gl_FragColor.a + epsilon;
183199
// Now we're doing the reverse, premultiplying by the alpha once again.

0 commit comments

Comments
 (0)