Files
MajoorWaldi-ComfyUI-Majoor-…/js/features/viewer/videoProcessorWebGL.js
T
MajoorWaldi dc72be0139 feat: enhance JS testing and linting setup
- Added coverage reporting for Vitest with configuration in vitest.config.mjs
- Updated package.json to include new scripts for JS coverage, linting, and formatting
- Modified run_quality_gate.py to allow skipping ESLint and Prettier checks
- Introduced .prettierrc for consistent code formatting
- Created .prettierignore to exclude specific files and directories from formatting
2026-03-28 14:14:28 +01:00

348 lines
11 KiB
JavaScript

const VS_SOURCE = `
attribute vec2 a_position;
varying vec2 v_uv;
void main() {
// Quad covers -1..1
gl_Position = vec4(a_position, 0, 1);
// Map -1..1 to 0..1
v_uv = a_position * 0.5 + 0.5;
// In WebGL, textures are usually flipped relative to Image/Video elements if not handled.
// We'll flip Y in fragment shader or here.
v_uv.y = 1.0 - v_uv.y;
}
`;
const FS_SOURCE = `
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_image;
uniform float u_exposure_scale;
uniform float u_gamma_inv;
uniform int u_channel; // 0=RGB, 1=R, 2=G, 3=B
uniform int u_analysis; // 0=None, 1=Zebra
uniform float u_zebra_threshold;
uniform vec2 u_resolution;
float getLuma(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
void main() {
vec4 texColor = texture2D(u_image, v_uv);
vec3 color = texColor.rgb;
// Exposure
color *= u_exposure_scale;
// Analysis (Zebra) or Gamma
bool isZebra = false;
if (u_analysis == 1) {
float luma = getLuma(color);
if (luma >= u_zebra_threshold) {
isZebra = true;
// Stripe pattern: (x + y) % 16 < 8
// gl_FragCoord is in window pixels
float stripe = mod(gl_FragCoord.x + gl_FragCoord.y, 32.0);
if (stripe < 16.0) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); // Black
} else {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // White
}
}
}
if (!isZebra) {
// Gamma
// fast pow?
color = pow(clamp(color, 0.0, 1.0), vec3(u_gamma_inv));
// Channel Selector
if (u_channel == 1) color = vec3(color.r);
else if (u_channel == 2) color = vec3(color.g);
else if (u_channel == 3) color = vec3(color.b);
gl_FragColor = vec4(color, texColor.a);
}
}
`;
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.warn("WebGL Shader Error:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vs, fs) {
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.warn("WebGL Program Error:", gl.getProgramInfoLog(program));
try {
gl.deleteProgram(program);
} catch (e) {
console.debug?.(e);
}
return null; // Return null to fallback
}
return program;
}
export function isWebGLAvailable() {
try {
const c = document.createElement("canvas");
return !!(
window.WebGLRenderingContext &&
(c.getContext("webgl") || c.getContext("experimental-webgl"))
);
} catch {
return false;
}
}
export function createWebGLVideoProcessor(opts) {
const { canvas, videoEl, getGradeParams } = opts;
let gl = null;
let resources = null;
let maxTextureSize = 4096;
let _contextLost = false;
const proc = {
type: "webgl",
ready: false,
naturalW: 0,
naturalH: 0,
scale: 1,
_destroyed: false,
};
function acquireContext() {
let ctx = null;
try {
// Must preserve drawing buffer so that frame export (toBlob/toDataURL) works
ctx = canvas.getContext("webgl", { alpha: false, preserveDrawingBuffer: true });
} catch (e) {
console.debug?.(e);
}
if (!ctx) {
try {
ctx = canvas.getContext("experimental-webgl", {
alpha: false,
preserveDrawingBuffer: true,
});
} catch (e) {
console.debug?.(e);
}
}
if (ctx) {
maxTextureSize = ctx.getParameter(ctx.MAX_TEXTURE_SIZE) || 4096;
}
return ctx;
}
const checkGLError = (label = "") => {
if (!gl) return;
const error = gl.getError();
if (error !== gl.NO_ERROR) {
console.error(`WebGL Error [${label}]: ${error}`);
}
};
const cleanupResources = () => {
if (!gl || !resources) return;
const { positionBuffer, texture, program } = resources;
try {
gl.bindTexture(gl.TEXTURE_2D, null);
} catch (e) {
console.debug?.(e);
}
try {
gl.bindBuffer(gl.ARRAY_BUFFER, null);
} catch (e) {
console.debug?.(e);
}
try {
gl.useProgram(null);
} catch (e) {
console.debug?.(e);
}
try {
gl.deleteTexture(texture);
} catch (e) {
console.debug?.(e);
}
try {
gl.deleteBuffer(positionBuffer);
} catch (e) {
console.debug?.(e);
}
try {
gl.deleteProgram(program);
} catch (e) {
console.debug?.(e);
}
resources = null;
};
const setupResources = () => {
if (!gl) return null;
cleanupResources();
const vs = createShader(gl, gl.VERTEX_SHADER, VS_SOURCE);
const fs = createShader(gl, gl.FRAGMENT_SHADER, FS_SOURCE);
if (!vs || !fs) {
if (vs) gl.deleteShader(vs);
if (fs) gl.deleteShader(fs);
return null;
}
const program = createProgram(gl, vs, fs);
gl.deleteShader(vs);
gl.deleteShader(fs);
if (!program) {
return null;
}
checkGLError("setupResources:createProgram");
const loc = {
position: gl.getAttribLocation(program, "a_position"),
u_image: gl.getUniformLocation(program, "u_image"),
u_exposure: gl.getUniformLocation(program, "u_exposure_scale"),
u_gamma: gl.getUniformLocation(program, "u_gamma_inv"),
u_channel: gl.getUniformLocation(program, "u_channel"),
u_analysis: gl.getUniformLocation(program, "u_analysis"),
u_thresh: gl.getUniformLocation(program, "u_zebra_threshold"),
u_res: gl.getUniformLocation(program, "u_resolution"),
};
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
gl.STATIC_DRAW,
);
checkGLError("setupResources:bufferData");
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
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.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
checkGLError("setupResources:texSetup");
return { program, loc, positionBuffer, texture };
};
const ensureResources = () => {
if (_contextLost || proc._destroyed) return false;
if (!gl) gl = acquireContext();
if (!gl) return false;
if (!resources) resources = setupResources();
return !!resources;
};
const ensureSize = () => {
if (!_contextLost && gl && videoEl?.videoWidth) {
let w = videoEl.videoWidth;
let h = videoEl.videoHeight;
const maxSize = maxTextureSize || gl.getParameter(gl.MAX_TEXTURE_SIZE) || 4096;
if (w > maxSize || h > maxSize) {
const scale = Math.min(maxSize / w, maxSize / h);
w = Math.floor(w * scale);
h = Math.floor(h * scale);
}
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
gl.viewport(0, 0, w, h);
}
return true;
}
return false;
};
const render = (overrideParams) => {
if (!_contextLost && ensureResources() && ensureSize()) {
const { program, loc, positionBuffer, texture } = resources;
gl.useProgram(program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
try {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoEl);
} catch (e) {
console.warn("WebGL texImage2D failed", e);
checkGLError("texImage2D");
return;
}
checkGLError("texImage2D");
gl.uniform1i(loc.u_image, 0);
const params = overrideParams || (getGradeParams ? getGradeParams() : {});
const exposureEV = Number(params.exposureEV) || 0;
const gamma = Math.max(0.1, Math.min(3, Number(params.gamma) || 1));
const analysis = params.analysisMode === "zebra" ? 1 : 0;
let ch = 0;
if (params.channel === "r") ch = 1;
if (params.channel === "g") ch = 2;
if (params.channel === "b") ch = 3;
gl.uniform1f(loc.u_exposure, Math.pow(2, exposureEV));
gl.uniform1f(loc.u_gamma, 1.0 / gamma);
gl.uniform1i(loc.u_channel, ch);
gl.uniform1i(loc.u_analysis, analysis);
gl.uniform1f(loc.u_thresh, params.zebraThreshold ?? 0.95);
gl.uniform2f(loc.u_res, canvas.width, canvas.height);
gl.enableVertexAttribArray(loc.position);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(loc.position, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
checkGLError("drawArrays");
}
};
const handleContextLost = (event) => {
event.preventDefault();
_contextLost = true;
cleanupResources();
};
const handleContextRestored = () => {
gl = acquireContext();
_contextLost = false;
resources = null;
ensureResources();
};
canvas.addEventListener("webglcontextlost", handleContextLost);
canvas.addEventListener("webglcontextrestored", handleContextRestored);
return {
update: render,
destroy: () => {
proc._destroyed = true;
proc.ready = false;
cleanupResources();
if (gl) {
try {
gl.getExtension("WEBGL_lose_context")?.loseContext?.();
} catch (e) {
console.debug?.(e);
}
}
canvas.removeEventListener("webglcontextlost", handleContextLost);
canvas.removeEventListener("webglcontextrestored", handleContextRestored);
},
};
}