cytoscape
Version:
Graph theory (a.k.a. network) library for analysis and visualisation
330 lines (281 loc) • 12.8 kB
JavaScript
/**
* FXAA (Fast Approximate Anti-Aliasing) implementation for Cytoscape.js
* Uses WebGL2 to apply efficient anti-aliasing to canvas content
*/
export class FxaaUpscaler {
/**
* Creates a new FXAA upscaler using WebGL2
* @param {Object} options - Configuration options
* @param {number} [options.subpixelQuality=0.75] - Amount of subpixel aliasing removal (0.0 to 1.0)
* @param {number} [options.edgeThreshold=0.166] - Edge detection threshold (0.0 to 1.0)
* @param {number} [options.edgeThresholdMin=0.0833] - Minimum edge threshold (helps with really fine edges)
* @param {boolean} [options.debug=false] - Enable debug logging
*/
constructor(options = {}) {
this.updateOptions(options);
this.debug = options.debug !== undefined ? options.debug : false;
// WebGL2 shader sources
this.vertexShaderSource = `#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0, 1);
v_texCoord = a_texCoord;
}
`;
this.fragmentShaderSource = `#version 300 es
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_resolution;
uniform float u_subpixelQuality;
uniform float u_edgeThreshold;
uniform float u_edgeThresholdMin;
in vec2 v_texCoord;
out vec4 fragColor;
void main() {
vec2 texelSize = 1.0 / u_resolution;
// Sample source texture
vec3 rgbM = texture(u_image, v_texCoord).rgb;
// Detect edges by sampling neighboring pixels
vec3 rgbNW = texture(u_image, v_texCoord + vec2(-1.0, -1.0) * texelSize).rgb;
vec3 rgbNE = texture(u_image, v_texCoord + vec2(1.0, -1.0) * texelSize).rgb;
vec3 rgbSW = texture(u_image, v_texCoord + vec2(-1.0, 1.0) * texelSize).rgb;
vec3 rgbSE = texture(u_image, v_texCoord + vec2(1.0, 1.0) * texelSize).rgb;
// Calculate luminance using perceptual weights for RGB
const vec3 lumCoeff = vec3(0.299, 0.587, 0.114);
float lumNW = dot(rgbNW, lumCoeff);
float lumNE = dot(rgbNE, lumCoeff);
float lumSW = dot(rgbSW, lumCoeff);
float lumSE = dot(rgbSE, lumCoeff);
float lumM = dot(rgbM, lumCoeff);
// Calculate luminance deltas for edge detection
float lumMin = min(lumM, min(min(lumNW, lumNE), min(lumSW, lumSE)));
float lumMax = max(lumM, max(max(lumNW, lumNE), max(lumSW, lumSE)));
// Calculate edge contrast
float range = lumMax - lumMin;
// Skip processing if contrast is too low (not an edge)
if (range < max(u_edgeThresholdMin, lumMax * u_edgeThreshold)) {
fragColor = vec4(rgbM, 1.0);
return;
}
// Sample additional pixels for the blur direction determination
vec3 rgbN = texture(u_image, v_texCoord + vec2(0.0, -1.0) * texelSize).rgb;
vec3 rgbS = texture(u_image, v_texCoord + vec2(0.0, 1.0) * texelSize).rgb;
vec3 rgbW = texture(u_image, v_texCoord + vec2(-1.0, 0.0) * texelSize).rgb;
vec3 rgbE = texture(u_image, v_texCoord + vec2(1.0, 0.0) * texelSize).rgb;
float lumN = dot(rgbN, lumCoeff);
float lumS = dot(rgbS, lumCoeff);
float lumW = dot(rgbW, lumCoeff);
float lumE = dot(rgbE, lumCoeff);
// Determine the blur direction
float blurH = 2.0 * (lumW + lumE) - (lumNW + lumNE + lumSW + lumSE);
float blurV = 2.0 * (lumN + lumS) - (lumNW + lumNE + lumSW + lumSE);
// Calculate blur direction
vec2 blurDir;
blurDir.x = -((blurH < 0.0) ? -blurH : blurH) / (blurV < 0.0 ? -blurV : blurV + 0.00001);
blurDir.y = 1.0;
// Normalize the blur vector and scale
float dirReduce = max(
(lumNW + lumNE + lumSW + lumSE) * 0.25 * u_subpixelQuality,
1.0/32.0
);
float rcpDirMin = 1.0 / (min(abs(blurDir.x), 1.0) + abs(blurDir.y));
blurDir = min(vec2(8.0, 8.0),
max(vec2(-8.0, -8.0),
blurDir * rcpDirMin)) * texelSize;
// Sample in the calculated direction
vec3 rgbA = 0.5 * (
texture(u_image, v_texCoord + blurDir * (1.0/3.0 - 0.5)).rgb +
texture(u_image, v_texCoord + blurDir * (2.0/3.0 - 0.5)).rgb);
vec3 rgbB = rgbA * 0.5 + 0.25 * (
texture(u_image, v_texCoord + blurDir * -0.5).rgb +
texture(u_image, v_texCoord + blurDir * 0.5).rgb);
// Detect if we are on a highly contrasting edge
float lumB = dot(rgbB, lumCoeff);
// Choose final color based on contrast detection
if (lumB < lumMin || lumB > lumMax) {
fragColor = vec4(rgbA, 1.0);
} else {
fragColor = vec4(rgbB, 1.0);
}
}
`;
// Add cached uniform locations
this.uniformLocations = {};
}
/**
* Update FXAA options
* @param {Object} options - Configuration options
* @param {number} [options.subpixelQuality] - Amount of subpixel aliasing removal (0.0 to 1.0)
* @param {number} [options.edgeThreshold] - Edge detection threshold (0.0 to 1.0)
* @param {number} [options.edgeThresholdMin] - Minimum edge threshold (helps with really fine edges)
* @param {boolean} [options.debug] - Enable debug logging
*/
updateOptions(options = {}) {
if (options.subpixelQuality !== undefined) this.subpixelQuality = options.subpixelQuality;
else if (!this.hasOwnProperty('subpixelQuality')) this.subpixelQuality = 0.75;
if (options.edgeThreshold !== undefined) this.edgeThreshold = options.edgeThreshold;
else if (!this.hasOwnProperty('edgeThreshold')) this.edgeThreshold = 0.166;
if (options.edgeThresholdMin !== undefined) this.edgeThresholdMin = options.edgeThresholdMin;
else if (!this.hasOwnProperty('edgeThresholdMin')) this.edgeThresholdMin = 0.0833;
if (options.debug !== undefined) this.debug = options.debug;
if (this.debug) {
console.log('FXAA options updated:', {
subpixelQuality: this.subpixelQuality,
edgeThreshold: this.edgeThreshold,
edgeThresholdMin: this.edgeThresholdMin
});
}
}
/**
* Compile a shader from source
* @param {WebGL2RenderingContext} gl - The WebGL2 context
* @param {number} type - The shader type
* @param {string} source - The shader source code
* @returns {WebGLShader} - The compiled shader
* @private
*/
compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compilation error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
/**
* Create a WebGL2 program with vertex and fragment shaders
* @param {WebGL2RenderingContext} gl - The WebGL2 context
* @returns {WebGLProgram} - The WebGL2 program
* @private
*/
createProgram(gl) {
const vertexShader = this.compileShader(gl, gl.VERTEX_SHADER, this.vertexShaderSource);
const fragmentShader = this.compileShader(gl, gl.FRAGMENT_SHADER, this.fragmentShaderSource);
if (!vertexShader || !fragmentShader) {
return null;
}
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program linking error:', gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
/**
* Apply FXAA to the input canvas and render to the output canvas
* @param {HTMLCanvasElement} inputCanvas - The source canvas
* @param {HTMLCanvasElement} outputCanvas - The destination canvas
* @returns {boolean} - Whether FXAA was successfully applied
*/
apply(inputCanvas, outputCanvas) {
// Don't modify canvas sizes - use them as they are
// Get WebGL2 context for the output canvas
const gl = outputCanvas.getContext('webgl2', {
alpha: true,
premultipliedAlpha: false,
preserveDrawingBuffer: true
});
if (!gl) {
console.error('WebGL2 is not supported');
// Fall back to 2D copy if WebGL2 is not available
const ctx = outputCanvas.getContext('2d');
ctx.drawImage(inputCanvas, 0, 0, outputCanvas.width, outputCanvas.height);
return false;
}
// Create shader program
const program = this.createProgram(gl);
if (!program) {
return false;
}
gl.useProgram(program);
// Cache uniform locations
this.uniformLocations = {
image: gl.getUniformLocation(program, 'u_image'),
resolution: gl.getUniformLocation(program, 'u_resolution'),
subpixelQuality: gl.getUniformLocation(program, 'u_subpixelQuality'),
edgeThreshold: gl.getUniformLocation(program, 'u_edgeThreshold'),
edgeThresholdMin: gl.getUniformLocation(program, 'u_edgeThresholdMin')
};
// Set up a vertex array object (VAO) - WebGL2 feature
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// Create position buffer with a full-screen quad (2 triangles)
const positionBuffer = gl.createBuffer();
const positionLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1, // bottom-left
1, -1, // bottom-right
-1, 1, // top-left
-1, 1, // top-left
1, -1, // bottom-right
1, 1 // top-right
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// Create texcoord buffer - FIX: flip Y coordinates to correct the upside-down rendering
const texCoordBuffer = gl.createBuffer();
const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(texCoordLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 1, // bottom-left (flipped y)
1, 1, // bottom-right (flipped y)
0, 0, // top-left (flipped y)
0, 0, // top-left (flipped y)
1, 1, // bottom-right (flipped y)
1, 0 // top-right (flipped y)
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Create texture from input canvas
const texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, inputCanvas);
// Set uniforms
gl.uniform1i(this.uniformLocations.image, 0); // Use texture unit 0
gl.uniform2f(this.uniformLocations.resolution, inputCanvas.width, inputCanvas.height);
// Ensure these crucial values are properly set
gl.uniform1f(this.uniformLocations.subpixelQuality, this.subpixelQuality);
gl.uniform1f(this.uniformLocations.edgeThreshold, this.edgeThreshold);
gl.uniform1f(this.uniformLocations.edgeThresholdMin, this.edgeThresholdMin);
if (this.debug) {
console.log('FXAA applying with options:', {
subpixelQuality: this.subpixelQuality,
edgeThreshold: this.edgeThreshold,
edgeThresholdMin: this.edgeThresholdMin,
canvasSize: `${outputCanvas.width}x${outputCanvas.height}`
});
// Check if uniform values were set correctly
const uniforms = {
subpixelQuality: gl.getUniform(program, this.uniformLocations.subpixelQuality),
edgeThreshold: gl.getUniform(program, this.uniformLocations.edgeThreshold),
edgeThresholdMin: gl.getUniform(program, this.uniformLocations.edgeThresholdMin)
};
console.log('Actual uniform values set in shader:', uniforms);
}
// Set viewport to match output canvas size - allows proper upscaling
gl.viewport(0, 0, outputCanvas.width, outputCanvas.height);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Clean up
gl.deleteVertexArray(vao);
gl.deleteBuffer(positionBuffer);
gl.deleteBuffer(texCoordBuffer);
gl.deleteTexture(texture);
gl.deleteProgram(program);
return true;
}
}