UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

311 lines (297 loc) 10.7 kB
import { defineProperty as _defineProperty } from '../../_virtual/_rollupPluginBabelHelpers.mjs'; import { config } from '../config.mjs'; import { createCanvasElementFor } from '../util/misc/dom.mjs'; class WebGLFilterBackend { constructor() { let { tileSize = config.textureSize } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; /** * Define ... **/ _defineProperty(this, "aPosition", new Float32Array([0, 0, 0, 1, 1, 0, 1, 1])); /** * 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. **/ _defineProperty(this, "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 glOptions = { alpha: true, premultipliedAlpha: false, depth: false, stencil: false, antialias: false }, gl = canvas.getContext('webgl', glOptions); if (!gl) { return; } gl.clearColor(0, 0, 0, 0); // this canvas can fire webglcontextlost and webglcontextrestored 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 : undefined), targetTexture: this.createTexture(gl, width, height), originalTexture: cachedTexture || this.createTexture(gl, width, height, !cachedTexture ? source : undefined), passes: filters.length, webgl: true, aPosition: this.aPosition, programCache: this.programCache, pass: 0, filterBackend: this, targetCanvas: 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) { // we are disposing, we don't care about the fact // that the canvas shouldn't be null. // @ts-expect-error disposing this.canvas = null; // @ts-expect-error disposing 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); // move it down again ctx.scale(1, -1); // vertical flip // where is my image on the big glcanvas? 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 targetCanvas = pipelineState.targetCanvas, ctx = 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; } } export { WebGLFilterBackend }; //# sourceMappingURL=WebGLFilterBackend.mjs.map