Skip to content

Instantly share code, notes, and snippets.

@gugray
Created November 9, 2022 20:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gugray/dfdbef073a3efe1abb5f501728ac9d51 to your computer and use it in GitHub Desktop.
Save gugray/dfdbef073a3efe1abb5f501728ac9d51 to your computer and use it in GitHub Desktop.
class WebGLCanvasMasker {
constructor(pixels, w, h, rf, printDiagnostics) {
this.pixels = pixels;
this.w = w;
this.h = h;
this.rf = rf;
this.printDiagnostics = printDiagnostics;
if (this.printDiagnostics)
console.log("Initializing WebGL canvas masker");
this.canvas = document.createElement("canvas");
const gl = this.canvas.getContext("webgl2");
this.gl = gl;
const ext = gl.getExtension('EXT_color_buffer_float');
if (!ext) throw('need EXT_color_buffer_float');
this.program = createProgramFromSources(gl, [vertexShader, fragmentShader]);
gl.useProgram(this.program);
this.lPosition = gl.getAttribLocation(this.program, "a_position");
this.lImg = gl.getUniformLocation(this.program, "u_img");
this.lCoords = gl.getUniformLocation(this.program, "u_coords");
this.lColors = gl.getUniformLocation(this.program, "u_colors");
this.lInputSz = gl.getUniformLocation(this.program, "u_input_sz");
this.lCanvasSz = gl.getUniformLocation(this.program, "u_canvas_sz");
this.lFrame = gl.getUniformLocation(this.program, "u_frame");
this.lSegLen = gl.getUniformLocation(this.program, "u_seg_len");
this.lRF = gl.getUniformLocation(this.program, "u_rf");
// Create position buffer - these will be our vertex attributes
this.bufPosition = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.bufPosition);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1, // first triangle
1, -1,
-1, 1,
-1, 1, // second triangle
1, -1,
1, 1,
]), gl.STATIC_DRAW);
// Upload pixels into image texture
this.txImage = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.txImage);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w * rf, h * rf, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
gl.bindTexture(gl.TEXTURE_2D, null);
}
mask(lines, frame, segLen, maxIters = 32) {
const calcStart = new Date();
if (this.printDiagnostics)
console.log("Calculating " + lines.length + " lines");
const res = [];
let toCheck = [];
for (const ln of lines) {
// No clue why, but if we get too large values here, the shader fails and no lines are returned as visible
const lim = 500_000;
if (ln.pt1.x > lim || ln.pt1.x < -lim || ln.pt1.y > lim || ln.pt1.y < -lim)
continue;
if (ln.pt2.x > lim || ln.pt2.x < -lim || ln.pt2.y > lim || ln.pt2.y < -lim)
continue;
toCheck.push(ln);
}
let iters = 0;
while (toCheck.length > 0 && iters < maxIters) {
let viss = this.maskOnce(toCheck, frame, segLen);
let checkNext = [];
for (const vl of viss) {
res.push([vl[0], vl[1]]);
const edge = toCheck[vl[2]];
checkNext.push({
pt1: vl[1].clone(),
pt2: edge.pt2.clone(),
clr1: edge.clr1,
clr2: edge.clr2,
});
}
toCheck = checkNext;
++iters;
}
if (this.printDiagnostics) {
let elapsed = new Date() - calcStart;
console.log("Kept " + res.length + " lines");
let sec = Math.floor(elapsed / 1000);
let ms = elapsed - sec * 1000;
console.log("Elapsed: " + sec.toString() + "." + ms.toString());
}
return res;
}
maskOnce(lines, frame, segLen) {
const gl = this.gl;
// Input and target textures
let texWidth = Math.ceil(Math.sqrt(lines.length));
let texHeight = Math.ceil(lines.length / texWidth);
this.canvas.width = texWidth;
this.canvas.height = texHeight;
gl.viewport(0, 0, texWidth, texHeight);
if (this.printDiagnostics)
console.log("Data texture: " + texWidth + " x " + texHeight);
// Data for coordinates and colors texture
const coordsData = [], colorsData = [];
for (const ln of lines) {
coordsData.push(ln.pt1.x, ln.pt1.y, ln.pt2.x, ln.pt2.y);
verifyRGBVals([ln.clr1.r, ln.clr1.g, ln.clr1.b, ln.clr2.r, ln.clr2.g, ln.clr2.b]);
const clrX = ln.clr1.r * 256 + ln.clr1.g;
const clrY = ln.clr1.b * 256 + ln.clr2.r;
const clrZ = ln.clr2.g * 256 + ln.clr2.b;
colorsData.push(clrX, clrY, clrZ, 0);
}
while (coordsData.length < texWidth * texHeight * 4) coordsData.push(0);
while (colorsData.length < texWidth * texHeight * 4) colorsData.push(0);
function verifyRGBVals(vals) {
for (const val of vals) {
if (val < 0 || val > 255) throw "RGB value out of 0..255 range";
if (val != Math.round(val)) throw "RGB value not integer";
}
}
// Create textures
const txTarget = createTexture(null);
const txCoords = createTexture(coordsData);
const txColors = createTexture(colorsData);
function createTexture(data) {
const tex = gl.createTexture();
let arr = null;
if (data) arr = new Float32Array(data);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
gl.TEXTURE_2D,
0, // mip level
gl.RGBA32F, // format
texWidth, // width
texHeight, // height
0, // border
gl.RGBA, // format
gl.FLOAT, // type
arr, // data
);
// We don't need any filtering
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Unbind texture!
gl.bindTexture(gl.TEXTURE_2D, null);
// Done
return tex;
}
// Render to target texture
const frameBuf = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuf);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, txTarget, 0);
// Put image texture on texture unit 0; tell shader to use it
gl.activeTexture(gl.TEXTURE0 + 0);
gl.bindTexture(gl.TEXTURE_2D, this.txImage);
gl.uniform1i(this.lImg, 0);
// Put coords texture on texture unit 1; tell shader to use it
gl.activeTexture(gl.TEXTURE0 + 1);
gl.bindTexture(gl.TEXTURE_2D, txCoords);
gl.uniform1i(this.lCoords, 1);
// Put colors texture on texture unit 2; tell shader to use it
gl.activeTexture(gl.TEXTURE0 + 2);
gl.bindTexture(gl.TEXTURE_2D, txColors);
gl.uniform1i(this.lColors, 2);
// Non-texture uniforms
gl.uniform2f(this.lCanvasSz, this.w, this.h);
gl.uniform2f(this.lInputSz, texWidth, texHeight);
if (frame) gl.uniform4f(this.lFrame, ...frame);
else gl.uniform4f(this.lFrame, 0, 0, this.w, this.h);
gl.uniform1f(this.lSegLen, segLen);
gl.uniform1f(this.lRF, this.rf);
// Render a square (two triangles / 6 vertices for full image)
gl.bindVertexArray(null);
gl.enableVertexAttribArray(this.lPosition);
gl.bindBuffer(gl.ARRAY_BUFFER, this.bufPosition);
gl.vertexAttribPointer(this.lPosition, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Fetch target texture data
let targetData = new Float32Array(texWidth * texHeight * 4);
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuf);
gl.readPixels(0, 0, texWidth, texHeight, gl.RGBA, gl.FLOAT, targetData);
// const res = [];
// for (let i = 0; i != lines.length; ++i) {
// const j = i * 4;
// res.push([lines[i].pt1, targetData[j], targetData[j+1], targetData[j+2]])
// }
// return res;
const visibleLines = [];
for (let i = 0; i != lines.length; ++i) {
const j = i * 4;
const pt1 = new paper.Point(targetData[j], targetData[j+1]);
const pt2 = new paper.Point(targetData[j+2], targetData[j+3]);
if (pt1.x == 0 && pt1.y == 0 && pt2.x == 0 && pt2.y == 0)
continue;
visibleLines.push([pt1, pt2, i]);
}
return visibleLines;
}
}
const vertexShader = `#version 300 es
precision highp float;
in vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1.);
}
`;
const fragmentShader = `#version 300 es
precision highp float;
uniform sampler2D u_img;
uniform sampler2D u_coords;
uniform sampler2D u_colors;
uniform vec2 u_canvas_sz;
uniform vec2 u_input_sz;
uniform vec4 u_frame;
uniform float u_seg_len;
uniform float u_rf;
out vec4 outColor;
#define PI 3.1415926538
#define PIFOURTH 0.7853981635
bool clrEq(vec3 a, vec3 b) {
float maxDiff = abs(a.r - b.r);
maxDiff = max(maxDiff, abs(a.g - b.g));
maxDiff = max(maxDiff, abs(a.b - b.b));
return maxDiff < 0.01;
}
vec3 getColor(vec2 pt) {
pt.y = u_canvas_sz.y - pt.y;
return texture(u_img, pt / u_canvas_sz).rgb;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_input_sz;
vec4 txCoords = texture(u_coords, uv);
vec4 txColors = texture(u_colors, uv);
// Colors 1 and 2 in RGB 0..255
float r1 = floor(txColors.x / 256.);
float g1 = txColors.x - r1 * 256.;
float b1 = floor(txColors.y / 256.);
float r2 = txColors.y - b1 * 256.;
float g2 = floor(txColors.z / 256.);
float b2 = txColors.z - g2 * 256.;
vec3 clr1 = vec3(r1, g1, b1);
vec3 clr2 = vec3(r2, g2, b2);
clr1 /= 256.;
clr2 /= 256.;
vec2 pt1 = vec2(txCoords[0], txCoords[1]);
vec2 pt2 = vec2(txCoords[2], txCoords[3]);
vec2 delta = pt2 - pt1;
float len = length(delta);
float nSegs = max(2., floor(len / u_seg_len) + 1.);
// vec2 orto = vec2(-delta.y, delta.x) / len / u_rf;
vec2 orto = vec2(-delta.y, delta.x) / len * 1.8;
mat2 mDiag = mat2(cos(PIFOURTH), -sin(PIFOURTH), sin(PIFOURTH), cos(PIFOURTH));
vec2 diag1 = mDiag * (vec2(-delta.y, delta.x) / len * 1.8);
vec2 diag2 = vec2(-diag1.y, diag1.x);
vec2 pt, firstVisible, lastVisible;
bool gotFirstVisible = false;
for (float i = 0.; i <= nSegs + 0.5; i += 1.) {
pt = pt1 + delta * i / nSegs;
// if (pt.x < 0. || pt.x > u_canvas_sz.x) continue;
// if (pt.y < 0. || pt.y > u_canvas_sz.y) continue;
if (pt.x < u_frame.x || pt.x > u_frame.x + u_frame.z) continue;
if (pt.y < u_frame.y || pt.y > u_frame.y + u_frame.w) continue;
vec3 clrHere = getColor(pt);
bool isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
if (!isVisible) {
clrHere = getColor(pt + orto);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (!isVisible) {
clrHere = getColor(pt - orto);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (!isVisible) {
clrHere = getColor(pt + diag1);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (!isVisible) {
clrHere = getColor(pt - diag1);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (!isVisible) {
clrHere = getColor(pt + diag2);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (!isVisible) {
clrHere = getColor(pt - diag2);
isVisible = clrEq(clrHere, clr1);
if (!isVisible) isVisible = clrEq(clrHere, clr2);
}
if (isVisible) {
lastVisible = pt;
if (!gotFirstVisible) {
firstVisible = pt;
gotFirstVisible = true;
}
}
else if (gotFirstVisible) {
float visibleLen = length(lastVisible - firstVisible);
if (visibleLen > u_seg_len) break;
gotFirstVisible = false;
}
}
// // DBG: keep all inputs
// float fullLen = length(pt2 - pt1);
// if (fullLen > u_seg_len) outColor = vec4(pt1, pt2);
// else outColor = vec4(0.);
// return;
if (!gotFirstVisible) outColor = vec4(0.);
else {
float visibleLen = length(lastVisible - firstVisible);
if (visibleLen > u_seg_len) outColor = vec4(firstVisible, lastVisible);
else outColor = vec4(0.);
}
}
`;
// WebGL shader loader taken from WebGL 2 Fundamentals
// ============================================================================
/*
* Copyright 2021, GFXFundamentals.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of GFXFundamentals. nor the names of his
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Creates a program, attaches shaders, binds attrib locations, links the
* program and calls useProgram.
* @param {WebGLShader[]} shaders The shaders to attach
* @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in
* @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations.
* @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console
* on error. If you want something else pass an callback. It's passed an error message.
* @memberOf module:webgl-utils
*/
function createProgram(
gl, shaders, opt_attribs, opt_locations, opt_errorCallback) {
const errFn = opt_errorCallback || error;
const program = gl.createProgram();
shaders.forEach(function (shader) {
gl.attachShader(program, shader);
});
if (opt_attribs) {
opt_attribs.forEach(function (attrib, ndx) {
gl.bindAttribLocation(
program,
opt_locations ? opt_locations[ndx] : ndx,
attrib);
});
}
gl.linkProgram(program);
// Check the link status
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
// something went wrong with the link
const lastError = gl.getProgramInfoLog(program);
errFn(`Error in program linking: ${lastError}\n${
shaders.map(shader => {
const src = addLineNumbersWithError(gl.getShaderSource(shader));
const type = gl.getShaderParameter(shader, gl.SHADER_TYPE);
return `${glEnumToString(gl, type)}:\n${src}`;
}).join('\n')
}`);
gl.deleteProgram(program);
return null;
}
return program;
}
/**
* Creates a program from 2 sources.
*
* @param {WebGLRenderingContext} gl The WebGLRenderingContext
* to use.
* @param {string[]} shaderSourcess Array of sources for the
* shaders. The first is assumed to be the vertex shader,
* the second the fragment shader.
* @param {string[]} [opt_attribs] An array of attribs names. Locations will be assigned by index if not passed in
* @param {number[]} [opt_locations] The locations for the. A parallel array to opt_attribs letting you assign locations.
* @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors. By default it just prints an error to the console
* on error. If you want something else pass an callback. It's passed an error message.
* @return {WebGLProgram} The created program.
* @memberOf module:webgl-utils
*/
function createProgramFromSources(
gl, shaderSources, opt_attribs, opt_locations, opt_errorCallback) {
const shaders = [];
for (let ii = 0; ii < shaderSources.length; ++ii) {
shaders.push(loadShader(
gl, shaderSources[ii], gl[defaultShaderType[ii]], opt_errorCallback));
}
return createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback);
}
/**
* Loads a shader.
* @param {WebGLRenderingContext} gl The WebGLRenderingContext to use.
* @param {string} shaderSource The shader source.
* @param {number} shaderType The type of shader.
* @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback for errors.
* @return {WebGLShader} The created shader.
*/
function loadShader(gl, shaderSource, shaderType, opt_errorCallback) {
const errFn = opt_errorCallback || error;
// Create the shader object
const shader = gl.createShader(shaderType);
// Load the shader source
gl.shaderSource(shader, shaderSource);
// Compile the shader
gl.compileShader(shader);
// Check the compile status
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
// Something went wrong during compilation; get the error
const lastError = gl.getShaderInfoLog(shader);
errFn(`Error compiling shader: ${lastError}\n${addLineNumbersWithError(shaderSource, lastError)}`);
gl.deleteShader(shader);
return null;
}
return shader;
}
/**
* Wrapped logging function.
* @param {string} msg The message to log.
*/
function error(msg) {
if (window.console) {
if (window.console.error) {
window.console.error(msg);
} else if (window.console.log) {
window.console.log(msg);
}
}
}
const errorRE = /ERROR:\s*\d+:(\d+)/gi;
function addLineNumbersWithError(src, log = '') {
// Note: Error message formats are not defined by any spec so this may or may not work.
const matches = [...log.matchAll(errorRE)];
const lineNoToErrorMap = new Map(matches.map((m, ndx) => {
const lineNo = parseInt(m[1]);
const next = matches[ndx + 1];
const end = next ? next.index : log.length;
const msg = log.substring(m.index, end);
return [lineNo - 1, msg];
}));
return src.split('\n').map((line, lineNo) => {
const err = lineNoToErrorMap.get(lineNo);
return `${lineNo + 1}: ${line}${err ? `\n\n^^^ ${err}` : ''}`;
}).join('\n');
}
const defaultShaderType = [
"VERTEX_SHADER",
"FRAGMENT_SHADER",
];
export {WebGLCanvasMasker};
@jesperkc
Copy link

Hi Gábor
I'm trying to use this but I'm struggling to figure out what to pass to the mask function. Could you give an example?

@gugray
Copy link
Author

gugray commented Jan 23, 2023

Sorry for the slow reply! I have a few plots on my website with the source code included that use the masker. E.g.:
https://jealousmarkup.xyz/plots/044/src/sketch-js/

HTH!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment