From a700bd1a2bc13c3318506b400554a15d08f1ce86 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sat, 4 Nov 2017 11:57:05 +0100 Subject: [PATCH] Ab webgl filter for resize (#4426) * test another version * a working webgl resize * broke blur" * broke blur" * a working webgl resize * better cache shader * no dist * better * better2 * fixed tests * linting --- src/filters/base_filter.class.js | 53 +++--- src/filters/colormatrix_filter.class.js | 14 ++ src/filters/pixelate_filter.class.js | 7 + src/filters/resize_filter.class.js | 226 ++++++++++++++++-------- src/filters/webgl_backend.class.js | 73 +++++--- src/shapes/image.class.js | 23 ++- test/unit/image.js | 8 +- 7 files changed, 269 insertions(+), 135 deletions(-) diff --git a/src/filters/base_filter.class.js b/src/filters/base_filter.class.js index 89ecea0e0e5..d71f05ac134 100644 --- a/src/filters/base_filter.class.js +++ b/src/filters/base_filter.class.js @@ -20,11 +20,15 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag */ type: 'BaseFilter', + /** + * Array of attributes to send with buffers. do not modify + * @private + */ + vertexSource: 'attribute vec2 aPosition;\n' + - 'attribute vec2 aTexCoord;\n' + 'varying vec2 vTexCoord;\n' + 'void main() {\n' + - 'vTexCoord = aTexCoord;\n' + + 'vTexCoord = aPosition;\n' + 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + '}', @@ -63,13 +67,12 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @param {String} vertexSource vertexShader source for compilation */ createProgram: function(gl, fragmentSource, vertexSource) { - if (!this.vertexSource || !this.fragmentSource) { - return; - } - if(fabric.webGlPrecision !== 'highp'){ - fragmentSource = fragmentSource.replace(/precision highp float/g, 'precision ' + fabric.webGlPrecision + ' float'); + if (fabric.webGlPrecision !== 'highp'){ + fragmentSource = fragmentSource.replace( + /precision highp float/g, + 'precision ' + fabric.webGlPrecision + ' float' + ); } - var vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexSource || this.vertexSource); gl.compileShader(vertexShader); @@ -125,7 +128,6 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag getAttributeLocations: function(gl, program) { return { aPosition: gl.getAttribLocation(program, 'aPosition'), - aTexCoord: gl.getAttribLocation(program, 'aTexCoord'), }; }, @@ -139,7 +141,8 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @returns {Object} A map of uniform names to uniform locations. */ getUniformLocations: function (/* gl, program */) { - // Intentionally left blank, override me in subclasses. + // in case i do not need any special uniform i need to return an empty object + return { }; }, /** @@ -148,20 +151,24 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag * @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, squareVertices) { - ['aPosition', 'aTexCoord'].forEach(function(attribute) { - var attributeLocation = attributeLocations[attribute]; - 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, squareVertices, gl.STATIC_DRAW); - }); + sendAttributeData: function(gl, attributeLocations, aPositionData) { + var attributeLocation = attributeLocations.aPostion; + 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; + 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); } @@ -211,7 +218,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag this.applyToWebGL(options); this._swapTextures(options); } - else { + else if (!this.isNeutralState()) { this.applyTo2d(options); } }, @@ -251,13 +258,13 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture); } gl.useProgram(shader.program); - this.sendAttributeData(gl, shader.attributeLocations, options.squareVertices); + 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.sourceWidth, options.sourceHeight); + gl.viewport(0, 0, options.destinationWidth, options.destinationHeight); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }, diff --git a/src/filters/colormatrix_filter.class.js b/src/filters/colormatrix_filter.class.js index a3295c35c57..d65d6c461d0 100644 --- a/src/filters/colormatrix_filter.class.js +++ b/src/filters/colormatrix_filter.class.js @@ -81,6 +81,20 @@ this.matrix = this.matrix.slice(0); }, + /** + * Intentionally left blank, to be overridden in custom filters + * @param {Object} options + **/ + isNeutralState: function(/* options */) { + var _class = filters.ColorMatrix; + for (var i = 20; i--;) { + if (this.matrix[i] !== _class.prototype.matrix[i]) { + return false; + } + } + return true; + }, + /** * Apply the ColorMatrix operation to a Uint8Array representing the pixels of an image. * diff --git a/src/filters/pixelate_filter.class.js b/src/filters/pixelate_filter.class.js index 5a14b966c61..a9c7dbab612 100644 --- a/src/filters/pixelate_filter.class.js +++ b/src/filters/pixelate_filter.class.js @@ -96,6 +96,13 @@ } }, + /** + * Indicate when the filter is not gonna apply changes to the image + **/ + isNeutralState: function() { + return this.blocksize === 1; + }, + /** * Return WebGL uniform locations for this filter's shader. * diff --git a/src/filters/resize_filter.class.js b/src/filters/resize_filter.class.js index 42ff066f223..966f85df26e 100644 --- a/src/filters/resize_filter.class.js +++ b/src/filters/resize_filter.class.js @@ -56,59 +56,158 @@ */ lanczosLobes: 3, - // vertexSource: 'attribute vec2 aPosition;\n' + - // 'attribute vec2 aTexCoord;\n' + - // 'uniform float uStepW;\n' + - // 'uniform float uStepH;\n' + - // 'varying vec2 centerTextureCoordinate;\n' + - // 'varying vec2 oneStepLeftTextureCoordinate;\n' + - // 'varying vec2 twoStepsLeftTextureCoordinate;\n' + - // 'varying vec2 threeStepsLeftTextureCoordinate;\n' + - // 'varying vec2 fourStepsLeftTextureCoordinate;\n' + - // 'varying vec2 oneStepRightTextureCoordinate;\n' + - // 'varying vec2 twoStepsRightTextureCoordinate;\n' + - // 'varying vec2 threeStepsRightTextureCoordinate;\n' + - // 'varying vec2 fourStepsRightTextureCoordinate;\n' + - // 'void main() {\n' + - // 'vec2 firstOffset = vec2(uStepW, uStepH);\n' + - // 'vec2 secondOffset = vec2(2.0 * uStepW, 2.0 * uStepH);\n' + - // 'vec2 thirdOffset = vec2(3.0 * uStepW, 3.0 * uStepH);\n' + - // 'vec2 fourthOffset = vec2(4.0 * uStepW, 4.0 * uStepH);\n' + - // 'centerTextureCoordinate = aTexCoord;\n' + - // 'oneStepLeftTextureCoordinate = aTexCoord - firstOffset;\n' + - // 'twoStepsLeftTextureCoordinate = aTexCoord - secondOffset;\n' + - // 'threeStepsLeftTextureCoordinate = aTexCoord - thirdOffset;\n' + - // 'fourStepsLeftTextureCoordinate = aTexCoord - fourthOffset;\n' + - // 'oneStepRightTextureCoordinate = aTexCoord + firstOffset;\n' + - // 'twoStepsRightTextureCoordinate = aTexCoord + secondOffset;\n' + - // 'threeStepsRightTextureCoordinate = aTexCoord + thirdOffset;\n' + - // 'fourStepsRightTextureCoordinate = aTexCoord + fourthOffset;\n' + - // 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + - // '}', - // - // fragmentSource: 'precision highp float;\n' + - // 'varying vec2 centerTextureCoordinate;\n' + - // 'varying vec2 oneStepLeftTextureCoordinate;\n' + - // 'varying vec2 twoStepsLeftTextureCoordinate;\n' + - // 'varying vec2 threeStepsLeftTextureCoordinate;\n' + - // 'varying vec2 fourStepsLeftTextureCoordinate;\n' + - // 'varying vec2 oneStepRightTextureCoordinate;\n' + - // 'varying vec2 twoStepsRightTextureCoordinate;\n' + - // 'varying vec2 threeStepsRightTextureCoordinate;\n' + - // 'varying vec2 fourStepsRightTextureCoordinate;\n' + - // 'uniform sampler2D uTexture;\n' + - // 'void main() {\n' + - // 'vec4 color = texture2D(uTexture, centerTextureCoordinate) * 0.38026;\n' + - // 'color += texture2D(uTexture, oneStepLeftTextureCoordinate) * 0.27667;\n' + - // 'color += texture2D(uTexture, oneStepRightTextureCoordinate) * 0.27667;\n' + - // 'color += texture2D(uTexture, twoStepsLeftTextureCoordinate) * 0.08074;\n' + - // 'color += texture2D(uTexture, twoStepsRightTextureCoordinate) * 0.08074;\n' + - // 'color += texture2D(uTexture, threeStepsLeftTextureCoordinate) * -0.02612;\n' + - // 'color += texture2D(uTexture, threeStepsRightTextureCoordinate) * -0.02612;\n' + - // 'color += texture2D(uTexture, fourStepsLeftTextureCoordinate) * -0.02143;\n' + - // 'color += texture2D(uTexture, fourStepsRightTextureCoordinate) * -0.02143;\n' + - // 'gl_FragColor = color;\n' + - // '}', + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uDelta: gl.getUniformLocation(program, 'uDelta'), + uTaps: gl.getUniformLocation(program, 'uTaps'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform2fv(uniformLocations.uDelta, this.horizontal ? [1 / this.width, 0] : [0, 1 / this.height]); + gl.uniform1fv(uniformLocations.uTaps, this.taps); + }, + + /** + * 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) { + var filterWindow = this.getFilterWindow(), cacheKey = this.type + '_' + filterWindow; + if (!options.programCache.hasOwnProperty(cacheKey)) { + var fragmentShader = this.generateShader(filterWindow); + options.programCache[cacheKey] = this.createProgram(options.context, fragmentShader); + } + return options.programCache[cacheKey]; + }, + + getFilterWindow: function() { + var scale = this.tempScale; + return Math.ceil(this.lanczosLobes / scale); + }, + + getTaps: function() { + var lobeFunction = this.lanczosCreate(this.lanczosLobes), scale = this.tempScale, + filterWindow = this.getFilterWindow(), taps = new Array(filterWindow); + for (var i = 1; i <= filterWindow; i++) { + taps[i - 1] = lobeFunction(i * scale); + } + return taps; + }, + + /** + * Generate vertex and shader sources from the necessary steps numbers + * @param {Number} filterWindow + */ + generateShader: function(filterWindow) { + var offsets = new Array(filterWindow), + fragmentShader = this.fragmentSourceTOP, filterWindow; + + for (var i = 1; i <= filterWindow; i++) { + offsets[i - 1] = i + '.0 * uDelta'; + } + + fragmentShader += 'uniform float uTaps[' + filterWindow + '];\n'; + fragmentShader += 'void main() {\n'; + fragmentShader += ' vec4 color = texture2D(uTexture, vTexCoord);\n'; + fragmentShader += ' float sum = 1.0;\n'; + + offsets.forEach(function(offset, i) { + fragmentShader += ' color += texture2D(uTexture, vTexCoord + ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' color += texture2D(uTexture, vTexCoord - ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' sum += 2.0 * uTaps[' + i + '];\n'; + }); + fragmentShader += ' gl_FragColor = color / sum;\n'; + fragmentShader += '}'; + return fragmentShader; + }, + + fragmentSourceTOP: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec2 uDelta;\n' + + 'varying vec2 vTexCoord;\n', + + /** + * Apply the resize filter to the image + * 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) { + if (options.passes > 1 && this.isNeutralState(options)) { + // avoid doing something that we do not need + return; + } + options.passes++; + this.width = options.sourceWidth; + this.horizontal = true; + this.dW = Math.round(this.width * this.scaleX); + this.dH = options.sourceHeight; + this.tempScale = this.dW / this.width; + this.taps = this.getTaps(); + options.destinationWidth = this.dW; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceWidth = options.destinationWidth; + + this.height = options.sourceHeight; + this.horizontal = false; + this.dH = Math.round(this.height * this.scaleY); + this.tempScale = this.dH / this.height; + this.taps = this.getTaps(); + options.destinationHeight = this.dH; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceHeight = options.destinationHeight; + } + else if (!this.isNeutralState(options)) { + this.applyTo2d(options); + } + }, + + isNeutralState: function(options) { + var scaleX = options.scaleX || this.scaleX, + scaleY = options.scaleY || this.scaleY; + return scaleX === 1 && scaleY === 1; + }, + + lanczosCreate: function(lobes) { + return function(x) { + if (x >= lobes || x <= -lobes) { + return 0.0; + } + if (x < 1.19209290E-07 && x > -1.19209290E-07) { + return 1.0; + } + x *= Math.PI; + var xx = x / lobes; + return (sin(x) / x) * sin(xx) / xx; + }; + }, /** * Applies filter to canvas element @@ -121,9 +220,6 @@ var imageData = options.imageData, scaleX = options.scaleX || this.scaleX, scaleY = options.scaleY || this.scaleY; - if (scaleX === 1 && scaleY === 1) { - return; - } this.rcpScaleX = 1 / scaleX; this.rcpScaleY = 1 / scaleY; @@ -212,20 +308,6 @@ */ lanczosResize: function(options, oW, oH, dW, dH) { - function lanczosCreate(lobes) { - return function(x) { - if (x > lobes) { - return 0; - } - x *= Math.PI; - if (abs(x) < 1e-16) { - return 1; - } - var xx = x / lobes; - return sin(x) * sin(xx) / x / xx; - }; - } - function process(u) { var v, i, weight, idx, a, red, green, blue, alpha, fX, fY; @@ -278,9 +360,9 @@ } var srcData = options.imageData.data, - destImg = options.ctx.creteImageData(dW, dH), + destImg = options.ctx.createImageData(dW, dH), destData = destImg.data, - lanczos = lanczosCreate(this.lanczosLobes), + lanczos = this.lanczosCreate(this.lanczosLobes), ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, rcpRatioX = 2 / this.rcpScaleX, rcpRatioY = 2 / this.rcpScaleY, range2X = ceil(ratioX * this.lanczosLobes / 2), diff --git a/src/filters/webgl_backend.class.js b/src/filters/webgl_backend.class.js index 07938c750dd..aa2d78cf486 100644 --- a/src/filters/webgl_backend.class.js +++ b/src/filters/webgl_backend.class.js @@ -13,7 +13,7 @@ var fragmentSource = 'precision ' + precision + ' float;\nvoid main(){}'; var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentSource); - gl.compileShader(fragmentShader); + gl.compileShader(fragmentShader); if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { return false; } @@ -38,8 +38,8 @@ fabric.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); isSupported = fabric.maxTextureSize >= tileSize; var precisions = ['highp', 'mediump', 'lowp']; - for(var i = 0; i < 3; i++){ - if(testPrecision(gl, precisions[i])){ + for (var i = 0; i < 3; i++){ + if (testPrecision(gl, precisions[i])){ fabric.webGlPrecision = precisions[i]; break; }; @@ -84,7 +84,7 @@ this.dispose(); this.createWebGLCanvas(width, height); // eslint-disable-next-line - this.squareVertices = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); + this.aPosition = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); this.chooseFastestCopyGLTo2DMethod(width, height); }, @@ -114,17 +114,22 @@ var targetCanvas = fabric.util.createCanvasElement(); // eslint-disable-next-line no-undef var imageBuffer = new ArrayBuffer(width * height * 4); - var testContext = { imageBuffer: imageBuffer }; + var testContext = { + imageBuffer: imageBuffer, + destinationWidth: width, + destinationHeight: height, + targetCanvas: targetCanvas + }; var startTime, drawImageTime, putImageDataTime; targetCanvas.width = width; targetCanvas.height = height; startTime = window.performance.now(); - copyGLTo2DDrawImage.call(testContext, this.gl, targetCanvas); + copyGLTo2DDrawImage.call(testContext, this.gl, testContext); drawImageTime = window.performance.now() - startTime; startTime = window.performance.now(); - copyGLTo2DPutImageData.call(testContext, this.gl, targetCanvas); + copyGLTo2DPutImageData.call(testContext, this.gl, testContext); putImageDataTime = window.performance.now() - startTime; if (drawImageTime > putImageDataTime) { @@ -181,6 +186,8 @@ 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), @@ -188,15 +195,17 @@ this.createTexture(gl, width, height, !cachedTexture && source), passes: filters.length, webgl: true, - squareVertices: this.squareVertices, + aPosition: this.aPosition, programCache: this.programCache, pass: 0, - filterBackend: this + filterBackend: this, + targetCanvas: targetCanvas }; var tempFbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo); filters.forEach(function(filter) { filter && filter.applyTo(pipelineState); }); - this.copyGLTo2D(gl, targetCanvas); + resizeCanvasIfNeeded(pipelineState); + this.copyGLTo2D(gl, pipelineState); gl.bindTexture(gl.TEXTURE_2D, null); gl.deleteTexture(pipelineState.sourceTexture); gl.deleteTexture(pipelineState.targetTexture); @@ -283,8 +292,8 @@ 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.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + 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) { @@ -362,6 +371,18 @@ }; })(); +function resizeCanvasIfNeeded(pipelineState) { + var 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; + } +} + /** * Copy an input WebGL canvas on to an output 2D canvas. * @@ -370,15 +391,16 @@ * * @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, targetCanvas) { - var sourceCanvas = gl.canvas; - var ctx = targetCanvas.getContext('2d'); +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 = sourceCanvas.height - targetCanvas.height; - ctx.drawImage(sourceCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, + var sourceY = glCanvas.height - targetCanvas.height; + ctx.drawImage(glCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, targetCanvas.width, targetCanvas.height); } @@ -388,17 +410,20 @@ function copyGLTo2DDrawImage(gl, targetCanvas) { * * @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 copyGLTo2DPutImageData(gl, targetCanvas) { - var ctx = targetCanvas.getContext('2d'); - var width = targetCanvas.width; - var height = targetCanvas.height; - var numBytes = width * height * 4; +function copyGLTo2DPutImageData(gl, pipelineState) { + var 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); - gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, u8); - var imgData = new ImageData(u8Clamped, width); + + gl.readPixels(0, 0, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8); + var imgData = new ImageData(u8Clamped, dWidth, dHeight); ctx.putImageData(imgData, 0, 0); } diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index e7b8a58c854..8d325521e3e 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -267,9 +267,6 @@ if (this.resizeFilter) { object.resizeFilter = this.resizeFilter.toObject(); } - object.width /= this._filterScalingX; - object.height /= this._filterScalingY; - return object; }, @@ -443,11 +440,10 @@ } fabric.filterBackend.applyFilters( filters, this._originalElement, sourceWidth, sourceHeight, this._element, this.cacheKey); - if (this.width !== this._element.width || this.height !== this._element.height) { - this._filterScalingX = this._element.width / this.width; - this._filterScalingY = this._element.height / this.height; - this.width = this._element.width; - this.height = this._element.height; + if (this._originalElement.width !== this._element.width || + this._originalElement.height !== this._element.height) { + this._filterScalingX = this._element.width / this._originalElement.width; + this._filterScalingY = this._element.height / this._originalElement.height; } return this; }, @@ -467,11 +463,14 @@ }, _renderFill: function(ctx) { - var x = -this.width / 2, y = -this.height / 2, elementToDraw; - elementToDraw = this._element; + var w = this.width, h = this.height, sW = w * this._filterScalingX, sH = h * this._filterScalingY, + x = -w / 2, y = -h / 2, elementToDraw = this._element; elementToDraw && ctx.drawImage(elementToDraw, - this.cropX, this.cropY, this.width, this.height, - x, y, this.width, this.height); + this.cropX * this._filterScalingX, + this.cropY * this._filterScalingY, + sW, + sH, + x, y, w, h); }, /** diff --git a/test/unit/image.js b/test/unit/image.js index eae23ec67e6..fbaca1c97e5 100644 --- a/test/unit/image.js +++ b/test/unit/image.js @@ -185,8 +185,10 @@ var width = image.width, height = image.height; assert.ok(image.filters[0] instanceof fabric.Image.filters.Resize, 'should inherit from fabric.Image.filters.Resize'); image.applyFilters(); - assert.equal(image.width, Math.floor(width / 5), 'width should be a fifth'); - assert.equal(image.height, Math.floor(height / 5), 'height should a fifth'); + assert.equal(image.width, Math.floor(width), 'width is not changed'); + assert.equal(image.height, Math.floor(height), 'height is not changed'); + assert.equal(image._filterScalingX.toFixed(1), 0.2, 'a new scaling factor is made for x'); + assert.equal(image._filterScalingY.toFixed(1), 0.2, 'a new scaling factor is made for y'); var toObject = image.toObject(); assert.deepEqual(toObject.filters[0], filter.toObject()); assert.equal(toObject.width, width, 'width is stored as before filters'); @@ -196,8 +198,6 @@ assert.ok(filterFromObj instanceof fabric.Image.filters.Resize, 'should inherit from fabric.Image.filters.Resize'); assert.equal(filterFromObj.scaleY, 0.2); assert.equal(filterFromObj.scaleX, 0.2); - assert.equal(_imageFromObject.width, Math.floor(width / 5), 'on image reload width is halved again'); - assert.equal(_imageFromObject.height, Math.floor(height / 5), 'on image reload width is halved again'); done(); }); });