fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
260 lines (259 loc) • 9.52 kB
JavaScript
import { _defineProperty } from "../../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs";
import { config } from "../config.mjs";
import { createCanvasElementFor } from "../util/misc/dom.mjs";
//#region src/filters/WebGLFilterBackend.ts
var WebGLFilterBackend = class {
constructor({ tileSize = config.textureSize } = {}) {
_defineProperty(
this,
/**
* Define ...
**/
"aPosition",
new Float32Array([
0,
0,
0,
1,
1,
0,
1,
1
])
);
_defineProperty(
this,
/**
* Experimental. This object is a sort of repository of help layers used to avoid
* of recreating them during frequent filtering. If you are previewing a filter with
* a slider you probably do not want to create help layers every filter step.
* in this object there will be appended some canvases, created once, resized sometimes
* cleared never. Clearing is left to the developer.
**/
"resources",
{}
);
this.tileSize = tileSize;
this.setupGLContext(tileSize, tileSize);
this.captureGPUInfo();
}
/**
* Setup a WebGL context suitable for filtering, and bind any needed event handlers.
*/
setupGLContext(width, height) {
this.dispose();
this.createWebGLCanvas(width, height);
}
/**
* Create a canvas element and associated WebGL context and attaches them as
* class properties to the GLFilterBackend class.
*/
createWebGLCanvas(width, height) {
const canvas = createCanvasElementFor({
width,
height
});
const gl = canvas.getContext("webgl", {
alpha: true,
premultipliedAlpha: false,
depth: false,
stencil: false,
antialias: false
});
if (!gl) return;
gl.clearColor(0, 0, 0, 0);
this.canvas = canvas;
this.gl = gl;
}
/**
* Attempts to apply the requested filters to the source provided, drawing the filtered output
* to the provided target canvas.
*
* @param {Array} filters The filters to apply.
* @param {TexImageSource} source The source to be filtered.
* @param {Number} width The width of the source input.
* @param {Number} height The height of the source input.
* @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn.
* @param {String|undefined} cacheKey A key used to cache resources related to the source. If
* omitted, caching will be skipped.
*/
applyFilters(filters, source, width, height, targetCanvas, cacheKey) {
const gl = this.gl;
const ctx = targetCanvas.getContext("2d");
if (!gl || !ctx) return;
let cachedTexture;
if (cacheKey) cachedTexture = this.getCachedTexture(cacheKey, source);
const pipelineState = {
originalWidth: source.width || source.naturalWidth || 0,
originalHeight: source.height || source.naturalHeight || 0,
sourceWidth: width,
sourceHeight: height,
destinationWidth: width,
destinationHeight: height,
context: gl,
sourceTexture: this.createTexture(gl, width, height, !cachedTexture ? source : void 0),
targetTexture: this.createTexture(gl, width, height),
originalTexture: cachedTexture || this.createTexture(gl, width, height, !cachedTexture ? source : void 0),
passes: filters.length,
webgl: true,
aPosition: this.aPosition,
programCache: this.programCache,
pass: 0,
filterBackend: this,
targetCanvas
};
const tempFbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo);
filters.forEach((filter) => {
filter && filter.applyTo(pipelineState);
});
resizeCanvasIfNeeded(pipelineState);
this.copyGLTo2D(gl, pipelineState);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.deleteTexture(pipelineState.sourceTexture);
gl.deleteTexture(pipelineState.targetTexture);
gl.deleteFramebuffer(tempFbo);
ctx.setTransform(1, 0, 0, 1, 0, 0);
return pipelineState;
}
/**
* Detach event listeners, remove references, and clean up caches.
*/
dispose() {
if (this.canvas) {
this.canvas = null;
this.gl = null;
}
this.clearWebGLCaches();
}
/**
* Wipe out WebGL-related caches.
*/
clearWebGLCaches() {
this.programCache = {};
this.textureCache = {};
}
/**
* Create a WebGL texture object.
*
* Accepts specific dimensions to initialize the texture to or a source image.
*
* @param {WebGLRenderingContext} gl The GL context to use for creating the texture.
* @param {number} width The width to initialize the texture at.
* @param {number} height The height to initialize the texture.
* @param {TexImageSource} textureImageSource A source for the texture data.
* @param {number} filter gl.NEAREST default or gl.LINEAR filters for the texture.
* This filter is very useful for LUTs filters. If you need interpolation use gl.LINEAR
* @returns {WebGLTexture}
*/
createTexture(gl, width, height, textureImageSource, filter) {
const { NEAREST, TEXTURE_2D, RGBA, UNSIGNED_BYTE, CLAMP_TO_EDGE, TEXTURE_MAG_FILTER, TEXTURE_MIN_FILTER, TEXTURE_WRAP_S, TEXTURE_WRAP_T } = gl;
const texture = gl.createTexture();
gl.bindTexture(TEXTURE_2D, texture);
gl.texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, filter || NEAREST);
gl.texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, filter || NEAREST);
gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE);
gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE);
if (textureImageSource) gl.texImage2D(TEXTURE_2D, 0, RGBA, RGBA, UNSIGNED_BYTE, textureImageSource);
else gl.texImage2D(TEXTURE_2D, 0, RGBA, width, height, 0, RGBA, UNSIGNED_BYTE, null);
return texture;
}
/**
* Can be optionally used to get a texture from the cache array
*
* If an existing texture is not found, a new texture is created and cached.
*
* @param {String} uniqueId A cache key to use to find an existing texture.
* @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source to use to create the
* texture cache entry if one does not already exist.
*/
getCachedTexture(uniqueId, textureImageSource, filter) {
const { textureCache } = this;
if (textureCache[uniqueId]) return textureCache[uniqueId];
else {
const texture = this.createTexture(this.gl, textureImageSource.width, textureImageSource.height, textureImageSource, filter);
if (texture) textureCache[uniqueId] = texture;
return texture;
}
}
/**
* Clear out cached resources related to a source image that has been
* filtered previously.
*
* @param {String} cacheKey The cache key provided when the source image was filtered.
*/
evictCachesForKey(cacheKey) {
if (this.textureCache[cacheKey]) {
this.gl.deleteTexture(this.textureCache[cacheKey]);
delete this.textureCache[cacheKey];
}
}
/**
* Copy an input WebGL canvas on to an output 2D canvas.
*
* The WebGL canvas is assumed to be upside down, with the top-left pixel of the
* desired output image appearing in the bottom-left corner of the WebGL canvas.
*
* @param {WebGLRenderingContext} sourceContext The WebGL context to copy from.
* @param {Object} pipelineState The 2D target canvas to copy on to.
*/
copyGLTo2D(gl, pipelineState) {
const glCanvas = gl.canvas, targetCanvas = pipelineState.targetCanvas, ctx = targetCanvas.getContext("2d");
if (!ctx) return;
ctx.translate(0, targetCanvas.height);
ctx.scale(1, -1);
const sourceY = glCanvas.height - targetCanvas.height;
ctx.drawImage(glCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, targetCanvas.width, targetCanvas.height);
}
/**
* Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData
* API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra).
*
* @param {WebGLRenderingContext} sourceContext The WebGL context to copy from.
* @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to.
* @param {Object} pipelineState The 2D target canvas to copy on to.
*/
copyGLTo2DPutImageData(gl, pipelineState) {
const ctx = pipelineState.targetCanvas.getContext("2d"), dWidth = pipelineState.destinationWidth, dHeight = pipelineState.destinationHeight, numBytes = dWidth * dHeight * 4;
if (!ctx) return;
const u8 = new Uint8Array(this.imageBuffer, 0, numBytes);
const u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes);
gl.readPixels(0, 0, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8);
const imgData = new ImageData(u8Clamped, dWidth, dHeight);
ctx.putImageData(imgData, 0, 0);
}
/**
* Attempt to extract GPU information strings from a WebGL context.
*
* Useful information when debugging or blacklisting specific GPUs.
*
* @returns {Object} A GPU info object with renderer and vendor strings.
*/
captureGPUInfo() {
if (this.gpuInfo) return this.gpuInfo;
const gl = this.gl, gpuInfo = {
renderer: "",
vendor: ""
};
if (!gl) return gpuInfo;
const ext = gl.getExtension("WEBGL_debug_renderer_info");
if (ext) {
const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
const vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL);
if (renderer) gpuInfo.renderer = renderer.toLowerCase();
if (vendor) gpuInfo.vendor = vendor.toLowerCase();
}
this.gpuInfo = gpuInfo;
return gpuInfo;
}
};
function resizeCanvasIfNeeded(pipelineState) {
const targetCanvas = pipelineState.targetCanvas, width = targetCanvas.width, height = targetCanvas.height, dWidth = pipelineState.destinationWidth, dHeight = pipelineState.destinationHeight;
if (width !== dWidth || height !== dHeight) {
targetCanvas.width = dWidth;
targetCanvas.height = dHeight;
}
}
//#endregion
export { WebGLFilterBackend };
//# sourceMappingURL=WebGLFilterBackend.mjs.map