diff --git a/CHANGELOG.md b/CHANGELOG.md index 580a32de667..6190f92167e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(TS): migrate filter backends [#8403](https://github.com/fabricjs/fabric.js/pull/8403) - chore(TS): migrate Text classes/mixins [#8408](https://github.com/fabricjs/fabric.js/pull/8408) - chore(TS): migrate Path [#8412](https://github.com/fabricjs/fabric.js/pull/8412) - ci(): remove unwanted build stats (from [#8395](https://github.com/fabricjs/fabric.js/pull/8395)) [#8416](https://github.com/fabricjs/fabric.js/pull/8416) diff --git a/index.js b/index.js index 421d937822d..bc323d6b169 100644 --- a/index.js +++ b/index.js @@ -35,8 +35,7 @@ import './src/shapes/group.class'; import './src/shapes/active_selection.class'; // optional interaction import './src/shapes/image.class'; import './src/mixins/object_straightening.mixin'; // optional objectstraightening -import './src/filters/webgl_backend.class'; // optional image_filters -import './src/filters/2d_backend.class'; // optional image_filters +import './src/filters/WebGLProbe'; // optional image_filters import './src/filters/base_filter.class'; // optional image_filters import './src/filters/colormatrix_filter.class'; // optional image_filters import './src/filters/brightness_filter.class'; // optional image_filters diff --git a/src/filters/2d_backend.class.ts b/src/filters/2d_backend.class.ts index 910cf68fe0e..ecba6db6f14 100644 --- a/src/filters/2d_backend.class.ts +++ b/src/filters/2d_backend.class.ts @@ -1,78 +1,64 @@ -//@ts-nocheck -(function (global) { - var fabric = global.fabric, - noop = function () {}; +/** + * Canvas 2D filter backend. + */ +import { T2DPipelineState } from './typedefs'; - fabric.Canvas2dFilterBackend = Canvas2dFilterBackend; +export class Canvas2dFilterBackend { + /** + * 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 = {}; /** - * Canvas 2D filter backend. + * Apply a set of filters against a source image and draw the filtered output + * to the provided destination canvas. + * + * @param {EnhancedFilter} filters The filter to apply. + * @param {HTMLImageElement|HTMLCanvasElement} sourceElement The source to be filtered. + * @param {Number} sourceWidth The width of the source input. + * @param {Number} sourceHeight The height of the source input. + * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. */ - function Canvas2dFilterBackend() {} - - Canvas2dFilterBackend.prototype = - /** @lends fabric.Canvas2dFilterBackend.prototype */ { - evictCachesForKey: noop, - dispose: noop, - clearWebGLCaches: noop, - - /** - * 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: {}, - - /** - * Apply a set of filters against a source image and draw the filtered output - * to the provided destination canvas. - * - * @param {EnhancedFilter} filters The filter to apply. - * @param {HTMLImageElement|HTMLCanvasElement} sourceElement The source to be filtered. - * @param {Number} sourceWidth The width of the source input. - * @param {Number} sourceHeight The height of the source input. - * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. - */ - applyFilters: function ( - filters, - sourceElement, - sourceWidth, - sourceHeight, - targetCanvas - ) { - var ctx = targetCanvas.getContext('2d'); - ctx.drawImage(sourceElement, 0, 0, sourceWidth, sourceHeight); - var imageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); - var originalImageData = ctx.getImageData( - 0, - 0, - sourceWidth, - sourceHeight - ); - var pipelineState = { - sourceWidth: sourceWidth, - sourceHeight: sourceHeight, - imageData: imageData, - originalEl: sourceElement, - originalImageData: originalImageData, - canvasEl: targetCanvas, - ctx: ctx, - filterBackend: this, - }; - filters.forEach(function (filter) { - filter.applyTo(pipelineState); - }); - if ( - pipelineState.imageData.width !== sourceWidth || - pipelineState.imageData.height !== sourceHeight - ) { - targetCanvas.width = pipelineState.imageData.width; - targetCanvas.height = pipelineState.imageData.height; - } - ctx.putImageData(pipelineState.imageData, 0, 0); - return pipelineState; - }, + applyFilters( + filters: any[], + sourceElement: HTMLImageElement | HTMLCanvasElement, + sourceWidth: number, + sourceHeight: number, + targetCanvas: HTMLCanvasElement + ): T2DPipelineState | void { + const ctx = targetCanvas.getContext('2d'); + if (!ctx) { + return; + } + ctx.drawImage(sourceElement, 0, 0, sourceWidth, sourceHeight); + const imageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + const originalImageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + const pipelineState: T2DPipelineState = { + sourceWidth, + sourceHeight, + imageData, + originalEl: sourceElement, + originalImageData, + canvasEl: targetCanvas, + ctx, + filterBackend: this, }; -})(typeof exports !== 'undefined' ? exports : window); + filters.forEach(function (filter) { + filter.applyTo(pipelineState); + }); + const { imageData: imageDataPostFilter } = pipelineState; + if ( + imageDataPostFilter.width !== sourceWidth || + imageDataPostFilter.height !== sourceHeight + ) { + targetCanvas.width = imageDataPostFilter.width; + targetCanvas.height = imageDataPostFilter.height; + } + ctx.putImageData(imageDataPostFilter, 0, 0); + return pipelineState; + } +} diff --git a/src/filters/WebGLProbe.ts b/src/filters/WebGLProbe.ts index 3b1006cdc2b..f2167ac6505 100644 --- a/src/filters/WebGLProbe.ts +++ b/src/filters/WebGLProbe.ts @@ -1,53 +1,38 @@ -//@ts-nocheck - import { fabric } from '../../HEADER'; +import { config } from '../config'; import { createCanvasElement } from '../util/misc/dom'; +import { Canvas2dFilterBackend } from './2d_backend.class'; +import { WebGLFilterBackend } from './webgl_backend.class'; -export const enum TWebGLPrecision { +export enum WebGLPrecision { low = 'lowp', medium = 'mediump', high = 'highp', } -/** - * @todo remove once rollup supports transforming enums... - * https://github.com/rollup/plugins/issues/463 - */ -const WebGLPrecision = [ - TWebGLPrecision.low, - TWebGLPrecision.medium, - TWebGLPrecision.high, -]; - /** * Lazy initialize WebGL contants */ class WebGLProbe { - private initialized = false; - - private _maxTextureSize?: number; - - private _webGLPrecision?: TWebGLPrecision; - - get maxTextureSize() { - this.queryWebGL(); - return this._maxTextureSize; - } + maxTextureSize?: number; - get webGLPrecision() { - this.queryWebGL(); - return this._webGLPrecision; - } + webGLPrecision: WebGLPrecision | undefined; /** * Tests if webgl supports certain precision * @param {WebGL} Canvas WebGL context to test on - * @param {TWebGLPrecision} Precision to test can be any of following + * @param {WebGLPrecision} Precision to test can be any of following * @returns {Boolean} Whether the user's browser WebGL supports given precision. */ - private testPrecision(gl: WebGLRenderingContext, precision: TWebGLPrecision) { + private testPrecision( + gl: WebGLRenderingContext, + precision: WebGLPrecision + ): boolean { const fragmentSource = `precision ${precision} float;\nvoid main(){}`; const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + if (!fragmentShader) { + return false; + } gl.shaderSource(fragmentShader, fragmentSource); gl.compileShader(fragmentShader); return !!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS); @@ -57,21 +42,19 @@ class WebGLProbe { * query browser for WebGL * @returns config object if true */ - private queryWebGL() { - if (this.initialized || fabric.isLikelyNode) { + queryWebGL() { + if (fabric.isLikelyNode) { return; } const canvas = createCanvasElement(); - const gl = - canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + const gl = canvas.getContext('webgl'); if (gl) { - this._maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); - this._webGLPrecision = WebGLPrecision.find((key) => - this.testPrecision(gl, key) + this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); + this.webGLPrecision = Object.values(WebGLPrecision).find((precision) => + this.testPrecision(gl, precision) ); - console.log(`fabric: max texture size ${this._maxTextureSize}`); + console.log(`fabric: max texture size ${this.maxTextureSize}`); } - this.initialized = true; } isSupported(textureSize: number) { @@ -80,3 +63,18 @@ class WebGLProbe { } export const webGLProbe = new WebGLProbe(); + +export function initFilterBackend(): + | WebGLFilterBackend + | Canvas2dFilterBackend { + webGLProbe.queryWebGL(); + if (config.enableGLFiltering && webGLProbe.isSupported(config.textureSize)) { + return new WebGLFilterBackend({ tileSize: config.textureSize }); + } else { + return new Canvas2dFilterBackend(); + } +} + +fabric.Canvas2dFilterBackend = Canvas2dFilterBackend; +fabric.WebglFilterBackend = WebGLFilterBackend; +fabric.initFilterBackend = initFilterBackend; diff --git a/src/filters/base_filter.class.ts b/src/filters/base_filter.class.ts index bbc27a4ee86..0bac427f713 100644 --- a/src/filters/base_filter.class.ts +++ b/src/filters/base_filter.class.ts @@ -1,393 +1,426 @@ -//@ts-nocheck +import { fabric } from '../../HEADER'; +import type { + TWebGLPipelineState, + T2DPipelineState, + TWebGLUniformLocationMap, + TWebGLAttributeLocationMap, + TWebGLProgramCacheItem, +} from './typedefs'; +import { WebGLPrecision, webGLProbe } from './WebGLProbe'; +import { isWebGLPipelineState } from './typedefs'; +import { createCanvasElement } from '../util/misc/dom'; + +/** + * @namespace fabric.Image.filters + * @memberOf fabric.Image + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#image_filters} + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + */ + +const highPsourceCode = `precision ${WebGLPrecision.high} float`; + +/** + * Root filter class from which all filter classes inherit from + * @class fabric.Image.filters.BaseFilter + * @memberOf fabric.Image.filters + */ +export abstract class BaseFilter { + /** + * Filter type + * @param {String} type + * @default + */ + type = 'BaseFilter'; + + /** + * Array of attributes to send with buffers. do not modify + * @private + */ + vertexSource: string; + fragmentSource: string; -import { TWebGLPrecision, webGLProbe } from './WebGLProbe'; + /** + * Name of the parameter that can be changed in the filter. + * Some filters have more than one paramenter and there is no + * mainParameter + * @private + */ + mainParameter?: keyof this; -(function (global) { - var fabric = global.fabric; /** - * @namespace fabric.Image.filters - * @memberOf fabric.Image - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#image_filters} - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * Constructor + * @param {Object} [options] Options object */ - fabric.Image.filters = fabric.Image.filters || {}; + constructor(options = {}) { + this.setOptions(options); + } /** - * Root filter class from which all filter classes inherit from - * @class fabric.Image.filters.BaseFilter - * @memberOf fabric.Image.filters + * just the compatibility layer until all classes are translated + * @param {Object} [options] Options object */ - fabric.Image.filters.BaseFilter = fabric.util.createClass( - /** @lends fabric.Image.filters.BaseFilter.prototype */ { - /** - * Filter type - * @param {String} type - * @default - */ - type: 'BaseFilter', - - /** - * Array of attributes to send with buffers. do not modify - * @private - */ - - vertexSource: - 'attribute vec2 aPosition;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vTexCoord = aPosition;\n' + - 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + - '}', - - fragmentSource: - 'precision highp float;\n' + - 'varying vec2 vTexCoord;\n' + - 'uniform sampler2D uTexture;\n' + - 'void main() {\n' + - 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + - '}', - - /** - * Constructor - * @param {Object} [options] Options object - */ - initialize: function (options) { - if (options) { - this.setOptions(options); - } - }, - - /** - * Sets filter's properties from options - * @param {Object} [options] Options object - */ - setOptions: function (options) { - for (var prop in options) { - this[prop] = options[prop]; - } - }, - - /** - * Compile this filter's shader program. - * - * @param {WebGLRenderingContext} gl The GL canvas context to use for shader compilation. - * @param {String} fragmentSource fragmentShader source for compilation - * @param {String} vertexSource vertexShader source for compilation - */ - createProgram: function (gl, fragmentSource, vertexSource) { - fragmentSource = fragmentSource || this.fragmentSource; - vertexSource = vertexSource || this.vertexSource; - if (webGLProbe.webGLPrecision !== TWebGLPrecision.high) { - fragmentSource = fragmentSource.replace( - new RegExp(`precision ${TWebGLPrecision.high} float`, 'g'), - `precision ${webGLProbe.webGLPrecision} float` - ); - } - var vertexShader = gl.createShader(gl.VERTEX_SHADER); - gl.shaderSource(vertexShader, vertexSource); - gl.compileShader(vertexShader); - if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { - throw new Error( - // eslint-disable-next-line prefer-template - 'Vertex shader compile error for ' + - this.type + - ': ' + - gl.getShaderInfoLog(vertexShader) - ); - } - - var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fragmentShader, fragmentSource); - gl.compileShader(fragmentShader); - if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { - throw new Error( - // eslint-disable-next-line prefer-template - 'Fragment shader compile error for ' + - this.type + - ': ' + - gl.getShaderInfoLog(fragmentShader) - ); - } - - var program = gl.createProgram(); - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - throw new Error( - // eslint-disable-next-line prefer-template - 'Shader link error for "${this.type}" ' + - gl.getProgramInfoLog(program) - ); - } - - var attributeLocations = this.getAttributeLocations(gl, program); - var uniformLocations = this.getUniformLocations(gl, program) || {}; - uniformLocations.uStepW = gl.getUniformLocation(program, 'uStepW'); - uniformLocations.uStepH = gl.getUniformLocation(program, 'uStepH'); - return { - program: program, - attributeLocations: attributeLocations, - uniformLocations: uniformLocations, - }; - }, - - /** - * Return a map of attribute names to WebGLAttributeLocation objects. - * - * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. - * @param {WebGLShaderProgram} program The shader program from which to take attribute locations. - * @returns {Object} A map of attribute names to attribute locations. - */ - getAttributeLocations: function (gl, program) { - return { - aPosition: gl.getAttribLocation(program, 'aPosition'), - }; - }, - - /** - * Return a map of uniform names to WebGLUniformLocation objects. - * - * Intended to be overridden by subclasses. - * - * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. - * @param {WebGLShaderProgram} program The shader program from which to take uniform locations. - * @returns {Object} A map of uniform names to uniform locations. - */ - getUniformLocations: function (/* gl, program */) { - // in case i do not need any special uniform i need to return an empty object - return {}; - }, - - /** - * Send attribute data from this filter to its shader program on the GPU. - * - * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. - * @param {Object} attributeLocations A map of shader attribute names to their locations. - */ - sendAttributeData: function (gl, attributeLocations, aPositionData) { - var attributeLocation = attributeLocations.aPosition; - var buffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - gl.enableVertexAttribArray(attributeLocation); - gl.vertexAttribPointer(attributeLocation, 2, gl.FLOAT, false, 0, 0); - gl.bufferData(gl.ARRAY_BUFFER, aPositionData, gl.STATIC_DRAW); - }, - - _setupFrameBuffer: function (options) { - var gl = options.context, - width, - height; - if (options.passes > 1) { - width = options.destinationWidth; - height = options.destinationHeight; - if ( - options.sourceWidth !== width || - options.sourceHeight !== height - ) { - gl.deleteTexture(options.targetTexture); - options.targetTexture = options.filterBackend.createTexture( - gl, - width, - height - ); - } - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - options.targetTexture, - 0 - ); - } else { - // draw last filter on canvas and not to framebuffer. - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - gl.finish(); - } - }, - - _swapTextures: function (options) { - options.passes--; - options.pass++; - var temp = options.targetTexture; - options.targetTexture = options.sourceTexture; - options.sourceTexture = temp; - }, - - /** - * Generic isNeutral implementation for one parameter based filters. - * Used only in image applyFilters to discard filters that will not have an effect - * on the image - * Other filters may need their own version ( ColorMatrix, HueRotation, gamma, ComposedFilter ) - * @param {Object} options - **/ - isNeutralState: function (/* options */) { - var main = this.mainParameter, - _class = fabric.Image.filters[this.type].prototype; - if (main) { - if (Array.isArray(_class[main])) { - for (var i = _class[main].length; i--; ) { - if (this[main][i] !== _class[main][i]) { - return false; - } - } - return true; - } else { - return _class[main] === this[main]; - } - } else { - return false; - } - }, - - /** - * Apply this filter to the input image data provided. - * - * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. - * - * @param {Object} options - * @param {Number} options.passes The number of filters remaining to be executed - * @param {Boolean} options.webgl Whether to use webgl to render the filter. - * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. - * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - applyTo: function (options) { - if (options.webgl) { - this._setupFrameBuffer(options); - this.applyToWebGL(options); - this._swapTextures(options); - } else { - this.applyTo2d(options); - } - }, - - /** - * Retrieves the cached shader. - * @param {Object} options - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - retrieveShader: function (options) { - if (!options.programCache.hasOwnProperty(this.type)) { - options.programCache[this.type] = this.createProgram(options.context); - } - return options.programCache[this.type]; - }, - - /** - * Apply this filter using webgl. - * - * @param {Object} options - * @param {Number} options.passes The number of filters remaining to be executed - * @param {Boolean} options.webgl Whether to use webgl to render the filter. - * @param {WebGLTexture} options.originalTexture The texture of the original input image. - * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. - * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - applyToWebGL: function (options) { - var gl = options.context; - var shader = this.retrieveShader(options); - if (options.pass === 0 && options.originalTexture) { - gl.bindTexture(gl.TEXTURE_2D, options.originalTexture); - } else { - gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture); - } - gl.useProgram(shader.program); - this.sendAttributeData( + initialize(options = {}) { + this.setOptions(options); + } + + /** + * Sets filter's properties from options + * @param {Object} [options] Options object + */ + setOptions(options: Record) { + Object.assign(this, options); + } + + /** + * Compile this filter's shader program. + * + * @param {WebGLRenderingContext} gl The GL canvas context to use for shader compilation. + * @param {String} fragmentSource fragmentShader source for compilation + * @param {String} vertexSource vertexShader source for compilation + */ + createProgram( + gl: WebGLRenderingContext, + fragmentSource: string = this.fragmentSource, + vertexSource: string = this.vertexSource + ) { + if ( + webGLProbe.webGLPrecision && + webGLProbe.webGLPrecision !== WebGLPrecision.high + ) { + fragmentSource = fragmentSource.replace( + new RegExp(highPsourceCode, 'g'), + highPsourceCode.replace(WebGLPrecision.high, webGLProbe.webGLPrecision) + ); + } + const vertexShader = gl.createShader(gl.VERTEX_SHADER); + const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + const program = gl.createProgram(); + + if (!vertexShader || !fragmentShader || !program) { + throw new Error('Vertex, fragment shader or program creation error'); + } + gl.shaderSource(vertexShader, vertexSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + throw new Error( + `Vertex shader compile error for ${this.type}: ${gl.getShaderInfoLog( + vertexShader + )}` + ); + } + + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + throw new Error( + `Fragment shader compile error for ${this.type}: ${gl.getShaderInfoLog( + fragmentShader + )}` + ); + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Shader link error for "${this.type}" ' + gl.getProgramInfoLog(program) + ); + } + + const uniformLocations = this.getUniformLocations(gl, program) || {}; + uniformLocations.uStepW = gl.getUniformLocation(program, 'uStepW'); + uniformLocations.uStepH = gl.getUniformLocation(program, 'uStepH'); + return { + program, + attributeLocations: this.getAttributeLocations(gl, program), + uniformLocations, + }; + } + + /** + * Return a map of attribute names to WebGLAttributeLocation objects. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take attribute locations. + * @returns {Object} A map of attribute names to attribute locations. + */ + getAttributeLocations( + gl: WebGLRenderingContext, + program: WebGLProgram + ): TWebGLAttributeLocationMap { + return { + aPosition: gl.getAttribLocation(program, 'aPosition'), + }; + } + + /** + * Return a map of uniform names to WebGLUniformLocation objects. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take uniform locations. + * @returns {Object} A map of uniform names to uniform locations. + */ + abstract getUniformLocations( + gl: WebGLRenderingContext, + program: WebGLProgram + ): TWebGLUniformLocationMap; + + /** + * Send attribute data from this filter to its shader program on the GPU. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} attributeLocations A map of shader attribute names to their locations. + */ + sendAttributeData( + gl: WebGLRenderingContext, + attributeLocations: Record, + aPositionData: Float32Array + ) { + const attributeLocation = attributeLocations.aPosition; + const buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(attributeLocation); + gl.vertexAttribPointer(attributeLocation, 2, gl.FLOAT, false, 0, 0); + gl.bufferData(gl.ARRAY_BUFFER, aPositionData, gl.STATIC_DRAW); + } + + _setupFrameBuffer(options: TWebGLPipelineState) { + const gl = options.context; + if (options.passes > 1) { + const width = options.destinationWidth; + const height = options.destinationHeight; + if (options.sourceWidth !== width || options.sourceHeight !== height) { + gl.deleteTexture(options.targetTexture); + options.targetTexture = options.filterBackend.createTexture( gl, - shader.attributeLocations, - options.aPosition + width, + height + ); + } + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + options.targetTexture, + 0 + ); + } else { + // draw last filter on canvas and not to framebuffer. + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.finish(); + } + } + + _swapTextures(options: TWebGLPipelineState) { + options.passes--; + options.pass++; + const temp = options.targetTexture; + options.targetTexture = options.sourceTexture; + options.sourceTexture = temp; + } + + /** + * Generic isNeutral implementation for one parameter based filters. + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * Other filters may need their own version ( ColorMatrix, HueRotation, gamma, ComposedFilter ) + * @param {Object} options + **/ + isNeutralState(/* options */): boolean { + const main = this.mainParameter, + // @ts-ignore ts you are lying + proto = this.__proto__; + if (main) { + if (Array.isArray(proto[main]) && Array.isArray(this[main])) { + return proto[main].every( + // @ts-ignore requires some kind of dynamic type thing, or delete, or leave it ignored + (value: any, i: number) => value === this[main][i] ); + } else { + return proto[main] === this[main]; + } + } else { + return false; + } + } + + /** + * Apply this filter to the input image data provided. + * + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo(options: TWebGLPipelineState | T2DPipelineState) { + if (isWebGLPipelineState(options)) { + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + } else { + this.applyTo2d(options); + } + } + + abstract applyTo2d(options: T2DPipelineState): void; - gl.uniform1f(shader.uniformLocations.uStepW, 1 / options.sourceWidth); - gl.uniform1f(shader.uniformLocations.uStepH, 1 / options.sourceHeight); - - this.sendUniformData(gl, shader.uniformLocations); - gl.viewport(0, 0, options.destinationWidth, options.destinationHeight); - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); - }, - - bindAdditionalTexture: function (gl, texture, textureUnit) { - gl.activeTexture(textureUnit); - gl.bindTexture(gl.TEXTURE_2D, texture); - // reset active texture to 0 as usual - gl.activeTexture(gl.TEXTURE0); - }, - - unbindAdditionalTexture: function (gl, textureUnit) { - gl.activeTexture(textureUnit); - gl.bindTexture(gl.TEXTURE_2D, null); - gl.activeTexture(gl.TEXTURE0); - }, - - getMainParameter: function () { - return this[this.mainParameter]; - }, - - setMainParameter: function (value) { - this[this.mainParameter] = value; - }, - - /** - * Send uniform data from this filter to its shader program on the GPU. - * - * Intended to be overridden by subclasses. - * - * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. - * @param {Object} uniformLocations A map of shader uniform names to their locations. - */ - sendUniformData: function (/* gl, uniformLocations */) { - // Intentionally left blank. Override me in subclasses. - }, - - /** - * If needed by a 2d filter, this functions can create an helper canvas to be used - * remember that options.targetCanvas is available for use till end of chain. - */ - createHelpLayer: function (options) { - if (!options.helpLayer) { - var helpLayer = document.createElement('canvas'); - helpLayer.width = options.sourceWidth; - helpLayer.height = options.sourceHeight; - options.helpLayer = helpLayer; - } - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function () { - var object = { type: this.type }, - mainP = this.mainParameter; - if (mainP) { - object[mainP] = this[mainP]; - } - return object; - }, - - /** - * Returns a JSON representation of an instance - * @return {Object} JSON - */ - toJSON: function () { - // delegate, not alias - return this.toObject(); - }, + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + * @return {WebGLProgram} the compiled program shader + */ + retrieveShader(options: TWebGLPipelineState): TWebGLProgramCacheItem { + if (!options.programCache[this.type]) { + options.programCache[this.type] = this.createProgram(options.context); } - ); + return options.programCache[this.type]; + } + /** + * Apply this filter using webgl. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.originalTexture The texture of the original input image. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyToWebGL(options: TWebGLPipelineState) { + const gl = options.context; + const shader = this.retrieveShader(options); + if (options.pass === 0 && options.originalTexture) { + gl.bindTexture(gl.TEXTURE_2D, options.originalTexture); + } else { + gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture); + } + gl.useProgram(shader.program); + this.sendAttributeData(gl, shader.attributeLocations, options.aPosition); + + gl.uniform1f(shader.uniformLocations.uStepW, 1 / options.sourceWidth); + gl.uniform1f(shader.uniformLocations.uStepH, 1 / options.sourceHeight); + + this.sendUniformData(gl, shader.uniformLocations); + gl.viewport(0, 0, options.destinationWidth, options.destinationHeight); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + + bindAdditionalTexture( + gl: WebGLRenderingContext, + texture: WebGLTexture, + textureUnit: number + ) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, texture); + // reset active texture to 0 as usual + gl.activeTexture(gl.TEXTURE0); + } + + unbindAdditionalTexture(gl: WebGLRenderingContext, textureUnit: number) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.activeTexture(gl.TEXTURE0); + } + + getMainParameter() { + return this.mainParameter ? this[this.mainParameter] : undefined; + } + + setMainParameter(value: any) { + if (this.mainParameter) { + this[this.mainParameter] = value; + } + } + + /** + * Send uniform data from this filter to its shader program on the GPU. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} uniformLocations A map of shader uniform names to their locations. + */ + abstract sendUniformData( + gl: WebGLRenderingContext, + uniformLocations: TWebGLUniformLocationMap + ): void; + + /** + * If needed by a 2d filter, this functions can create an helper canvas to be used + * remember that options.targetCanvas is available for use till end of chain. + */ + createHelpLayer(options: T2DPipelineState) { + if (!options.helpLayer) { + const helpLayer = createCanvasElement(); + helpLayer.width = options.sourceWidth; + helpLayer.height = options.sourceHeight; + options.helpLayer = helpLayer; + } + } + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject() { + const mainP = this.mainParameter; + return { + type: this.type, + ...(mainP ? { [mainP]: this[mainP] } : {}), + }; + } + + /** + * Returns a JSON representation of an instance + * @return {Object} JSON + */ + toJSON() { + // delegate, not alias + return this.toObject(); + } /** * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @returns {Promise} */ - fabric.Image.filters.BaseFilter.fromObject = function (object) { + static fromObject = function (object: any) { + // todo: the class registry her return Promise.resolve(new fabric.Image.filters[object.type](object)); }; -})(typeof exports !== 'undefined' ? exports : window); +} + +Object.assign(BaseFilter.prototype, { + vertexSource: ` + attribute vec2 aPosition; + varying vec2 vTexCoord; + void main() { + vTexCoord = aPosition; + gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0); + }`, + + fragmentSource: ` + ${highPsourceCode}; + varying vec2 vTexCoord; + uniform sampler2D uTexture; + void main() { + gl_FragColor = texture2D(uTexture, vTexCoord); + }`, +}); + +fabric.Image.filters = { + BaseFilter, +}; diff --git a/src/filters/typedefs.ts b/src/filters/typedefs.ts new file mode 100644 index 00000000000..dfa763f6778 --- /dev/null +++ b/src/filters/typedefs.ts @@ -0,0 +1,59 @@ +import type { WebGLFilterBackend } from './webgl_backend.class'; +import type { Canvas2dFilterBackend } from './2d_backend.class'; + +export type TProgramCache = any; + +export type TTextureCache = any; + +export type TWebGLPipelineState = { + filterBackend: WebGLFilterBackend; + originalWidth: number; + originalHeight: number; + sourceWidth: number; + sourceHeight: number; + destinationWidth: number; + destinationHeight: number; + context: WebGLRenderingContext; + sourceTexture: WebGLTexture | null; + targetTexture: WebGLTexture | null; + originalTexture: WebGLTexture; + passes: number; + webgl: boolean; + aPosition: Float32Array; + programCache: TProgramCache; + pass: number; + targetCanvas: HTMLCanvasElement; +}; + +export type T2DPipelineState = { + sourceWidth: number; + sourceHeight: number; + filterBackend: Canvas2dFilterBackend; + canvasEl: HTMLCanvasElement; + imageData: ImageData; + originalEl: HTMLCanvasElement | HTMLImageElement; + originalImageData?: ImageData; + ctx: CanvasRenderingContext2D; + helpLayer?: HTMLCanvasElement; +}; + +export type TWebGLUniformLocationMap = Record< + string, + WebGLUniformLocation | null +>; + +export type TWebGLAttributeLocationMap = Record; + +export type TWebGLProgramCacheItem = { + program: WebGLProgram; + attributeLocations: TWebGLAttributeLocationMap; + uniformLocations: TWebGLUniformLocationMap; +}; + +export type TApplyFilterArgs = {}; + +export const isWebGLPipelineState = ( + options: TWebGLPipelineState | T2DPipelineState +): options is TWebGLPipelineState => { + return (options as TWebGLPipelineState).webgl !== undefined; +}; diff --git a/src/filters/webgl_backend.class.ts b/src/filters/webgl_backend.class.ts index 022082c5709..6dbdc016bf1 100644 --- a/src/filters/webgl_backend.class.ts +++ b/src/filters/webgl_backend.class.ts @@ -1,352 +1,389 @@ -//@ts-nocheck - +import { fabric } from '../../HEADER'; import { config } from '../config'; -import { webGLProbe } from './WebGLProbe'; +import { createCanvasElement } from '../util/misc/dom'; +import { TWebGLPipelineState, TProgramCache, TTextureCache } from './typedefs'; -(function (global) { - var fabric = global.fabric; +export class WebGLFilterBackend { + tileSize: number; - fabric.initFilterBackend = function () { - if ( - config.enableGLFiltering && - webGLProbe.isSupported(config.textureSize) - ) { - return new fabric.WebglFilterBackend({ tileSize: config.textureSize }); - } else if (fabric.Canvas2dFilterBackend) { - return new fabric.Canvas2dFilterBackend(); - } - }; + /** + * Define ... + **/ + aPosition: Float32Array = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); - fabric.WebglFilterBackend = WebglFilterBackend; + /** + * If GLPut data is the fastest operation, or if forced, this buffer will be used + * to transfer the data back in the 2d logic + **/ + imageBuffer?: ArrayBuffer; + + canvas: HTMLCanvasElement; /** - * WebGL filter backend. - */ - function WebglFilterBackend(options) { - if (options && options.tileSize) { - this.tileSize = options.tileSize; - } - this.setupGLContext(this.tileSize, this.tileSize); - this.captureGPUInfo(); - } + * The Webgl context that will execute the operations for filtering + **/ + gl: WebGLRenderingContext; - WebglFilterBackend.prototype = - /** @lends fabric.WebglFilterBackend.prototype */ { - tileSize: config.textureSize, + /** + * Keyed map for shader cache + **/ + programCache: TProgramCache; - /** - * 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: {}, + /** + * Keyed map for texture cache + **/ + textureCache: TTextureCache; - /** - * Setup a WebGL context suitable for filtering, and bind any needed event handlers. - */ - setupGLContext: function (width, height) { - this.dispose(); - this.createWebGLCanvas(width, height); - // eslint-disable-next-line - this.aPosition = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); - this.chooseFastestCopyGLTo2DMethod(width, height); - }, + /** + * Contains GPU info for debug + **/ + gpuInfo: any; - /** - * Pick a method to copy data from GL context to 2d canvas. In some browsers using - * putImageData is faster than drawImage for that specific operation. - */ - chooseFastestCopyGLTo2DMethod: function (width, height) { - var canMeasurePerf = typeof window.performance !== 'undefined', - canUseImageData; - try { - new ImageData(1, 1); - canUseImageData = true; - } catch (e) { - canUseImageData = false; - } - // eslint-disable-next-line no-undef - var canUseArrayBuffer = typeof ArrayBuffer !== 'undefined'; - // eslint-disable-next-line no-undef - var canUseUint8Clamped = typeof Uint8ClampedArray !== 'undefined'; + /** + * 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 = {}; - if ( - !( - canMeasurePerf && - canUseImageData && - canUseArrayBuffer && - canUseUint8Clamped - ) - ) { - return; - } + constructor({ tileSize = config.textureSize } = {}) { + this.tileSize = tileSize; + this.setupGLContext(tileSize, tileSize); + this.captureGPUInfo(); + } - var targetCanvas = fabric.util.createCanvasElement(); - // eslint-disable-next-line no-undef - var imageBuffer = new ArrayBuffer(width * height * 4); - if (config.forceGLPutImageData) { - this.imageBuffer = imageBuffer; - this.copyGLTo2D = copyGLTo2DPutImageData; - return; - } - var testContext = { - imageBuffer: imageBuffer, - destinationWidth: width, - destinationHeight: height, - targetCanvas: targetCanvas, - }; - var startTime, drawImageTime, putImageDataTime; - targetCanvas.width = width; - targetCanvas.height = height; + /** + * Setup a WebGL context suitable for filtering, and bind any needed event handlers. + */ + setupGLContext(width: number, height: number): void { + this.dispose(); + this.createWebGLCanvas(width, height); + // eslint-disable-next-line + this.chooseFastestCopyGLTo2DMethod(width, height); + } - startTime = window.performance.now(); - copyGLTo2DDrawImage.call(testContext, this.gl, testContext); - drawImageTime = window.performance.now() - startTime; + /** + * Pick a method to copy data from GL context to 2d canvas. In some browsers using + * drawImage should be faster, but is also bugged for a small combination of old hardware + * and drivers. + * putImageData is faster than drawImage for that specific operation. + */ + chooseFastestCopyGLTo2DMethod(width: number, height: number): void { + const targetCanvas = createCanvasElement(); + // eslint-disable-next-line no-undef + const imageBuffer = new ArrayBuffer(width * height * 4); + if (config.forceGLPutImageData) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + return; + } - startTime = window.performance.now(); - copyGLTo2DPutImageData.call(testContext, this.gl, testContext); - putImageDataTime = window.performance.now() - startTime; + const testContext = { + imageBuffer: imageBuffer, + } as unknown as Required; + const testPipelineState = { + destinationWidth: width, + destinationHeight: height, + targetCanvas: targetCanvas, + } as unknown as TWebGLPipelineState; + let startTime; + targetCanvas.width = width; + targetCanvas.height = height; - if (drawImageTime > putImageDataTime) { - this.imageBuffer = imageBuffer; - this.copyGLTo2D = copyGLTo2DPutImageData; - } else { - this.copyGLTo2D = copyGLTo2DDrawImage; - } - }, + startTime = fabric.window.performance.now(); + this.copyGLTo2D.call(testContext, this.gl, testPipelineState); + const drawImageTime = fabric.window.performance.now() - startTime; - /** - * Create a canvas element and associated WebGL context and attaches them as - * class properties to the GLFilterBackend class. - */ - createWebGLCanvas: function (width, height) { - var canvas = fabric.util.createCanvasElement(); - canvas.width = width; - canvas.height = height; - var glOptions = { - alpha: true, - premultipliedAlpha: false, - depth: false, - stencil: false, - antialias: false, - }, - gl = canvas.getContext('webgl', glOptions); - if (!gl) { - gl = canvas.getContext('experimental-webgl', glOptions); - } - if (!gl) { - return; - } - gl.clearColor(0, 0, 0, 0); - // this canvas can fire webglcontextlost and webglcontextrestored - this.canvas = canvas; - this.gl = gl; + startTime = fabric.window.performance.now(); + copyGLTo2DPutImageData.call(testContext, this.gl, testPipelineState); + const putImageDataTime = fabric.window.performance.now() - startTime; + + if (drawImageTime > putImageDataTime) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + } + } + + /** + * Create a canvas element and associated WebGL context and attaches them as + * class properties to the GLFilterBackend class. + */ + createWebGLCanvas(width: number, height: number): void { + const canvas = createCanvasElement(); + canvas.width = width; + canvas.height = height; + const glOptions = { + alpha: true, + premultipliedAlpha: false, + depth: false, + stencil: false, + antialias: false, }, + gl = canvas.getContext('webgl', glOptions) as WebGLRenderingContext; - /** - * 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 {HTMLImageElement|HTMLCanvasElement} 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: function ( - filters, - source, + 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: any[], + source: TexImageSource, + width: number, + height: number, + targetCanvas: HTMLCanvasElement, + cacheKey?: string + ): TWebGLPipelineState | undefined { + const gl = this.gl; + const ctx = targetCanvas.getContext('2d'); + if (!gl || !ctx) { + return; + } + let cachedTexture; + if (cacheKey) { + cachedTexture = this.getCachedTexture(cacheKey, source); + } + const pipelineState: TWebGLPipelineState = { + // @ts-ignore + originalWidth: source.width || source.originalWidth || 0, + // @ts-ignore + originalHeight: source.height || source.originalHeight || 0, + sourceWidth: width, + sourceHeight: height, + destinationWidth: width, + destinationHeight: height, + context: gl, + sourceTexture: this.createTexture( + gl, width, height, - targetCanvas, - cacheKey - ) { - var gl = this.gl; - var cachedTexture; - if (cacheKey) { - cachedTexture = this.getCachedTexture(cacheKey, source); - } - var pipelineState = { - originalWidth: source.width || source.originalWidth, - originalHeight: source.height || source.originalHeight, - sourceWidth: width, - sourceHeight: height, - destinationWidth: width, - destinationHeight: height, - context: gl, - sourceTexture: this.createTexture( - gl, - width, - height, - !cachedTexture && source - ), - targetTexture: this.createTexture(gl, width, height), - originalTexture: - cachedTexture || - this.createTexture(gl, width, height, !cachedTexture && source), - passes: filters.length, - webgl: true, - aPosition: this.aPosition, - programCache: this.programCache, - pass: 0, - filterBackend: this, - targetCanvas: targetCanvas, - }; - var tempFbo = gl.createFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo); - filters.forEach(function (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); - targetCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); - return pipelineState; - }, + !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: any) => { + 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: function () { - if (this.canvas) { - this.canvas = null; - this.gl = null; - } - this.clearWebGLCaches(); - }, + /** + * 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-ignore + this.canvas = null; + // @ts-ignore + this.gl = null; + } + this.clearWebGLCaches(); + } - /** - * Wipe out WebGL-related caches. - */ - clearWebGLCaches: function () { - this.programCache = {}; - this.textureCache = {}; - }, + /** + * 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 {HTMLImageElement|HTMLCanvasElement} textureImageSource A source for the texture data. - * @returns {WebGLTexture} - */ - createTexture: function (gl, width, height, textureImageSource) { - var texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - 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); - if (textureImageSource) { - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - textureImageSource - ); - } else { - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - width, - height, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - null - ); - } - return texture; - }, + /** + * 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 {HTMLImageElement|HTMLCanvasElement} textureImageSource A source for the texture data. + * @returns {WebGLTexture} + */ + createTexture( + gl: WebGLRenderingContext, + width: number, + height: number, + textureImageSource?: TexImageSource + ) { + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + 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); + if (textureImageSource) { + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + gl.RGBA, + gl.UNSIGNED_BYTE, + textureImageSource + ); + } else { + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + width, + height, + 0, + gl.RGBA, + gl.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: function (uniqueId, textureImageSource) { - if (this.textureCache[uniqueId]) { - return this.textureCache[uniqueId]; - } else { - var texture = this.createTexture( - this.gl, - textureImageSource.width, - textureImageSource.height, - textureImageSource - ); - this.textureCache[uniqueId] = texture; - 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: string, textureImageSource: TexImageSource) { + if (this.textureCache[uniqueId]) { + return this.textureCache[uniqueId]; + } else { + const texture = this.createTexture( + this.gl, + textureImageSource.width, + textureImageSource.height, + textureImageSource + ); + this.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: function (cacheKey) { - if (this.textureCache[cacheKey]) { - this.gl.deleteTexture(this.textureCache[cacheKey]); - delete this.textureCache[cacheKey]; - } - }, + /** + * 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: string) { + if (this.textureCache[cacheKey]) { + this.gl.deleteTexture(this.textureCache[cacheKey]); + delete this.textureCache[cacheKey]; + } + } - copyGLTo2D: copyGLTo2DDrawImage, + /** + * 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: WebGLRenderingContext, pipelineState: TWebGLPipelineState) { + 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 + ); + } - /** - * 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: function () { - if (this.gpuInfo) { - return this.gpuInfo; - } - var gl = this.gl, - gpuInfo = { renderer: '', vendor: '' }; - if (!gl) { - return gpuInfo; - } - var ext = gl.getExtension('WEBGL_debug_renderer_info'); - if (ext) { - var renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); - var vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL); - if (renderer) { - gpuInfo.renderer = renderer.toLowerCase(); - } - if (vendor) { - gpuInfo.vendor = vendor.toLowerCase(); - } - } - this.gpuInfo = gpuInfo; - return gpuInfo; - }, - }; -})(typeof exports !== 'undefined' ? exports : window); + /** + * 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) { - var targetCanvas = pipelineState.targetCanvas, +function resizeCanvasIfNeeded(pipelineState: TWebGLPipelineState): void { + const targetCanvas = pipelineState.targetCanvas, width = targetCanvas.width, height = targetCanvas.height, dWidth = pipelineState.destinationWidth, @@ -358,37 +395,6 @@ function resizeCanvasIfNeeded(pipelineState) { } } -/** - * 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 {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. - * @param {Object} pipelineState The 2D target canvas to copy on to. - */ -function copyGLTo2DDrawImage(gl, pipelineState) { - var glCanvas = gl.canvas, - targetCanvas = pipelineState.targetCanvas, - ctx = targetCanvas.getContext('2d'); - ctx.translate(0, targetCanvas.height); // move it down again - ctx.scale(1, -1); // vertical flip - // where is my image on the big glcanvas? - var 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). @@ -397,19 +403,23 @@ function copyGLTo2DDrawImage(gl, pipelineState) { * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. * @param {Object} pipelineState The 2D target canvas to copy on to. */ -function copyGLTo2DPutImageData(gl, pipelineState) { - var targetCanvas = pipelineState.targetCanvas, +function copyGLTo2DPutImageData( + this: Required, + gl: WebGLRenderingContext, + pipelineState: TWebGLPipelineState +) { + const targetCanvas = pipelineState.targetCanvas, ctx = targetCanvas.getContext('2d'), dWidth = pipelineState.destinationWidth, dHeight = pipelineState.destinationHeight, numBytes = dWidth * dHeight * 4; - - // eslint-disable-next-line no-undef - var u8 = new Uint8Array(this.imageBuffer, 0, numBytes); - // eslint-disable-next-line no-undef - var u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes); + 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); - var imgData = new ImageData(u8Clamped, dWidth, dHeight); + const imgData = new ImageData(u8Clamped, dWidth, dHeight); ctx.putImageData(imgData, 0, 0); } diff --git a/src/shapes/image.class.ts b/src/shapes/image.class.ts index 010d8a56540..457e9a63740 100644 --- a/src/shapes/image.class.ts +++ b/src/shapes/image.class.ts @@ -1,5 +1,6 @@ //@ts-nocheck import { FabricObject } from './fabricObject.class'; +import { initFilterBackend } from '../filters/WebGLProbe'; (function (global) { var fabric = global.fabric, @@ -452,7 +453,7 @@ import { FabricObject } from './fabricObject.class'; return; } if (!fabric.filterBackend) { - fabric.filterBackend = fabric.initFilterBackend(); + fabric.filterBackend = initFilterBackend(); } var canvasEl = fabric.util.createCanvasElement(), cacheKey = this._filteredEl @@ -526,7 +527,7 @@ import { FabricObject } from './fabricObject.class'; this._lastScaleY = 1; } if (!fabric.filterBackend) { - fabric.filterBackend = fabric.initFilterBackend(); + fabric.filterBackend = initFilterBackend(); } fabric.filterBackend.applyFilters( filters,