fabric-pure-browser
Version:
Fabric.js package with no node-specific dependencies (node-canvas, jsdom). The project is published once a day (in case if a new version appears) from 'master' branch of https://github.com/fabricjs/fabric.js repository. You can keep original imports in
396 lines (364 loc) • 14.3 kB
JavaScript
(function() {
'use strict';
/**
* Tests if webgl supports certain precision
* @param {WebGL} Canvas WebGL context to test on
* @param {String} Precision to test can be any of following: 'lowp', 'mediump', 'highp'
* @returns {Boolean} Whether the user's browser WebGL supports given precision.
*/
function testPrecision(gl, precision){
var fragmentSource = 'precision ' + precision + ' float;\nvoid main(){}';
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
return false;
}
return true;
}
/**
* Indicate whether this filtering backend is supported by the user's browser.
* @param {Number} tileSize check if the tileSize is supported
* @returns {Boolean} Whether the user's browser supports WebGL.
*/
fabric.isWebglSupported = function(tileSize) {
if (fabric.isLikelyNode) {
return false;
}
tileSize = tileSize || fabric.WebglFilterBackend.prototype.tileSize;
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
var isSupported = false;
// eslint-disable-next-line
if (gl) {
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])){
fabric.webGlPrecision = precisions[i];
break;
};
}
}
this.isSupported = isSupported;
return isSupported;
};
fabric.WebglFilterBackend = WebglFilterBackend;
/**
* WebGL filter backend.
*/
function WebglFilterBackend(options) {
if (options && options.tileSize) {
this.tileSize = options.tileSize;
}
this.setupGLContext(this.tileSize, this.tileSize);
this.captureGPUInfo();
};
WebglFilterBackend.prototype = /** @lends fabric.WebglFilterBackend.prototype */ {
tileSize: 2048,
/**
* 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 problably 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: {
},
/**
* 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);
},
/**
* 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';
if (!(canMeasurePerf && canUseImageData && canUseArrayBuffer && canUseUint8Clamped)) {
return;
}
var targetCanvas = fabric.util.createCanvasElement();
// eslint-disable-next-line no-undef
var imageBuffer = new ArrayBuffer(width * height * 4);
if (fabric.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;
startTime = window.performance.now();
copyGLTo2DDrawImage.call(testContext, this.gl, testContext);
drawImageTime = window.performance.now() - startTime;
startTime = window.performance.now();
copyGLTo2DPutImageData.call(testContext, this.gl, testContext);
putImageDataTime = window.performance.now() - startTime;
if (drawImageTime > putImageDataTime) {
this.imageBuffer = imageBuffer;
this.copyGLTo2D = copyGLTo2DPutImageData;
}
else {
this.copyGLTo2D = copyGLTo2DDrawImage;
}
},
/**
* 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;
},
/**
* 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, 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;
},
/**
* Detach event listeners, remove references, and clean up caches.
*/
dispose: function() {
if (this.canvas) {
this.canvas = null;
this.gl = null;
}
this.clearWebGLCaches();
},
/**
* Wipe out WebGL-related caches.
*/
clearWebGLCaches: function() {
this.programCache = {};
this.textureCache = {};
},
/**
* Create a WebGL texture object.
*
* Accepts specific dimensions to initialize the textuer 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;
},
/**
* 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;
}
},
/**
* 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];
}
},
copyGLTo2D: copyGLTo2DDrawImage,
/**
* 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;
},
};
})();
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.
*
* 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).
*
* @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, 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, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8);
var imgData = new ImageData(u8Clamped, dWidth, dHeight);
ctx.putImageData(imgData, 0, 0);
}