Compare commits

...

8 Commits

Author SHA1 Message Date
Karishma Chadha
81f7e25b62 Merge pull request #837 from adroitwhiz/fix-mystery-mode
Set v_texCoord in mystery mode
2021-03-18 20:21:50 -04:00
adroitwhiz
df17da1dc8 Set v_texCoord in mystery mode 2021-03-16 03:59:57 -04:00
Karishma Chadha
25797be895 Merge branch 'hotfix/totally-normal-2020' into hotfix/totally-normal-2021 2021-03-05 12:20:38 -05:00
DD Liu
9560de4d75 Merge pull request #776 from adroitwhiz/test-color-conversions
Add unit tests for color conversion functions
2021-03-02 15:40:46 -05:00
adroitwhiz
a930503793 Update colors-almost-equal threshold 2021-02-25 20:58:12 -05:00
adroitwhiz
e075e5f5eb Add unit tests for color conversion functions 2021-02-11 18:41:20 -05:00
rschamp
238228e6f2 Make hotfix tag 2020-03-23 22:14:57 -04:00
adroitwhiz
ec40b654d4 Enhance mysterious atmosphere 2020-03-22 21:58:52 -04:00
7 changed files with 258 additions and 99 deletions

View File

@@ -6,6 +6,7 @@
const twgl = require('twgl.js');
const {rgbToHsv, hsvToRgb} = require('./util/color-conversions');
const ShaderManager = require('./ShaderManager');
/**
@@ -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 {
/**

View File

@@ -139,6 +139,13 @@ class RenderWebGL extends EventEmitter {
throw new Error('Could not get WebGL context: this browser or environment may not support WebGL.');
}
this._mystery = {
modeActive: false,
mouseMoveListener: null,
bufferInfo: null,
mouseCoords: [0, 0]
};
/** @type {RenderWebGL.UseGpuModes} */
this._useGpuMode = RenderWebGL.UseGpuModes.Automatic;
@@ -242,6 +249,7 @@ class RenderWebGL extends EventEmitter {
resize (pixelsWide, pixelsTall) {
const {canvas} = this._gl;
const pixelRatio = window.devicePixelRatio || 1;
const newWidth = pixelsWide * pixelRatio;
const newHeight = pixelsTall * pixelRatio;
@@ -250,10 +258,16 @@ class RenderWebGL extends EventEmitter {
if (canvas.width !== newWidth || canvas.height !== newHeight) {
canvas.width = newWidth;
canvas.height = newHeight;
if (this._mystery.modeActive && this._mystery.bufferInfo) {
twgl.resizeFramebufferInfo(this._gl, this._mystery.bufferInfo, [
{format: this._gl.RGBA}
], pixelsWide * pixelRatio, pixelsTall * pixelRatio);
}
// Resizing the canvas causes it to be cleared, so redraw it.
this.draw();
}
}
/**
@@ -653,7 +667,43 @@ class RenderWebGL extends EventEmitter {
gl.clearColor(...this._backgroundColor4f);
gl.clear(gl.COLOR_BUFFER_BIT);
this._drawThese(this._drawList, ShaderManager.DRAW_MODE.default, this._projection);
let drawList = this._drawList;
if (this._mystery.modeActive) {
drawList = drawList.slice(1);
twgl.bindFramebufferInfo(gl, this._mystery.bufferInfo);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
this._drawThese(drawList, ShaderManager.DRAW_MODE.default, this._projection);
if (this._mystery.modeActive) {
// draw all layers except for the bottom layer onto the mystery buffer
twgl.bindFramebufferInfo(gl, null);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
this._drawThese([this._drawList[0]], ShaderManager.DRAW_MODE.default, this._projection);
this._doExitDrawRegion();
const newShader = this._shaderManager.getShader(ShaderManager.DRAW_MODE.mystery, 0);
this._regionId = newShader;
// draw mystery buffer to main buffer
gl.useProgram(newShader.program);
twgl.setBuffersAndAttributes(gl, newShader, this._bufferInfo);
const uniforms = {
u_projectionMatrix: this._projection,
u_modelMatrix: twgl.m4.identity(),
u_skin: this._mystery.bufferInfo.attachments[0],
u_mousePosition: this._mystery.mouseCoords
};
twgl.setTextureParameters(
gl, uniforms.u_skin, {minMag: gl.LINEAR}
);
twgl.setUniforms(newShader, uniforms);
twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES);
}
if (this._snapshotCallbacks.length > 0) {
const snapshot = gl.canvas.toDataURL();
this._snapshotCallbacks.forEach(cb => cb(snapshot));
@@ -2083,6 +2133,32 @@ class RenderWebGL extends EventEmitter {
return dst;
}
setMysteryMode (enableMysteryMode) {
this._mystery.modeActive = enableMysteryMode;
if (enableMysteryMode) {
this._mystery.bufferInfo = twgl.createFramebufferInfo(
this._gl,
[{format: this._gl.RGBA}],
this._gl.drawingBufferWidth,
this._gl.drawingBufferHeight
);
this._mystery.mouseMoveListener = event => {
const rect = this.canvas.getBoundingClientRect();
this._mystery.mouseCoords[0] = (event.clientX - rect.left) / rect.width;
this._mystery.mouseCoords[1] = (event.clientY - rect.top) / rect.height;
};
document.addEventListener('mousemove', this._mystery.mouseMoveListener);
} else {
if (this._mystery.bufferInfo) {
this._gl.deleteFramebuffer(this._mystery.bufferInfo.framebuffer);
}
document.removeEventListener('mousemove', this._mystery.mouseMoveListener);
}
}
/**
* @callback RenderWebGL#snapshotCallback
* @param {string} dataURI Data URI of the snapshot of the renderer

View File

@@ -181,7 +181,9 @@ ShaderManager.DRAW_MODE = {
/**
* Draw the background in a certain color. Must sometimes be used instead of gl.clear.
*/
background: 'background'
background: 'background',
mystery: 'mystery'
};
module.exports = ShaderManager;

View File

@@ -43,6 +43,10 @@ uniform float u_lineLength;
uniform vec4 u_backgroundColor;
#endif // DRAW_MODE_background
#ifdef DRAW_MODE_mystery
uniform vec2 u_mousePosition;
#endif
uniform sampler2D u_skin;
#ifndef DRAW_MODE_background
@@ -118,6 +122,15 @@ void main()
#if !(defined(DRAW_MODE_line) || defined(DRAW_MODE_background))
vec2 texcoord0 = v_texCoord;
#ifdef DRAW_MODE_mystery
vec2 mysteryCoord = texcoord0;
vec2 offset = vec2(u_mousePosition.x, 1.0 - u_mousePosition.y);
mysteryCoord -= offset;
const float SCALE_FACTOR = 0.85;
mysteryCoord *= vec2(SCALE_FACTOR, SCALE_FACTOR);
mysteryCoord += offset;
#endif
#ifdef ENABLE_mosaic
texcoord0 = fract(u_mosaic * texcoord0);
#endif // ENABLE_mosaic
@@ -161,6 +174,21 @@ void main()
gl_FragColor = texture2D(u_skin, texcoord0);
#ifdef DRAW_MODE_mystery
const vec4 SHADOW_COLOR = vec4(0.0, 0.0, 0.0, 0.5);
const float SHADOW_BLUR = 0.0025;
float shadowSample1 = texture2D(u_skin, mysteryCoord + vec2(SHADOW_BLUR, SHADOW_BLUR)).a;
float shadowSample2 = texture2D(u_skin, mysteryCoord + vec2(-SHADOW_BLUR, SHADOW_BLUR)).a;
float shadowSample3 = texture2D(u_skin, mysteryCoord + vec2(SHADOW_BLUR, -SHADOW_BLUR)).a;
float shadowSample4 = texture2D(u_skin, mysteryCoord + vec2(-SHADOW_BLUR, -SHADOW_BLUR)).a;
float shadowAlpha = (shadowSample1 + shadowSample2 + shadowSample3 + shadowSample4) * 0.25;
vec4 shadow = SHADOW_COLOR * shadowAlpha;
gl_FragColor = gl_FragColor + (shadow * (1.0 - gl_FragColor.a));
#endif
#if defined(ENABLE_color) || defined(ENABLE_brightness)
// Divide premultiplied alpha values for proper color processing
// Add epsilon to avoid dividing by 0 for fully transparent pixels

View File

@@ -68,6 +68,9 @@ void main() {
gl_Position = vec4(position, 0, 1);
#elif defined(DRAW_MODE_background)
gl_Position = vec4(a_position * 2.0, 0, 1);
#elif defined(DRAW_MODE_mystery)
gl_Position = vec4(a_position * vec2(-2.0, 2.0), 0.0, 1.0);
v_texCoord = a_texCoord;
#else
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
v_texCoord = a_texCoord;

View File

@@ -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};

View File

@@ -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();
});