';
- },
-
- applyResizeFilters: function() {
- var filter = this.resizeFilter,
- retinaScaling = this.canvas ? this.canvas.getRetinaScaling() : 1,
- minimumScale = this.minimumScaleTrigger,
- scaleX = this.scaleX * retinaScaling,
- scaleY = this.scaleY * retinaScaling,
- elementToFilter = this._filteredEl || this._originalElement;
- if (!filter || (scaleX > minimumScale && scaleY > minimumScale)) {
- this._element = elementToFilter;
- this._filterScalingX = 1;
- this._filterScalingY = 1;
- return;
- }
- if (!fabric.filterBackend) {
- fabric.filterBackend = fabric.initFilterBackend();
- }
- var canvasEl = fabric.util.createCanvasElement(),
- cacheKey = this._filteredEl ? this.cacheKey : (this.cacheKey + '_filtered'),
- sourceWidth = elementToFilter.width, sourceHeight = elementToFilter.height;
- canvasEl.width = sourceWidth;
- canvasEl.height = sourceHeight;
- this._element = canvasEl;
- filter.scaleX = scaleX;
- filter.scaleY = scaleY;
- fabric.filterBackend.applyFilters(
- [filter], elementToFilter, sourceWidth, sourceHeight, this._element, cacheKey);
- this._filterScalingX = canvasEl.width / this._originalElement.width;
- this._filterScalingY = canvasEl.height / this._originalElement.height;
- },
-
- /**
- * Applies filters assigned to this image (from "filters" array) or from filter param
- * @method applyFilters
- * @param {Array} filters to be applied
- * @param {Boolean} forResizing specify if the filter operation is a resize operation
- * @return {thisArg} return the fabric.Image object
- * @chainable
- */
- applyFilters: function(filters) {
-
- filters = filters || this.filters || [];
- filters = filters.filter(function(filter) { return filter; });
- if (filters.length === 0) {
- this._element = this._originalElement;
- this._filteredEl = null;
- this._filterScalingX = 1;
- this._filterScalingY = 1;
- return this;
- }
-
- var imgElement = this._originalElement,
- sourceWidth = imgElement.naturalWidth || imgElement.width,
- sourceHeight = imgElement.naturalHeight || imgElement.height;
-
- if (this._element === this._originalElement) {
- // if the element is the same we need to create a new element
- var canvasEl = fabric.util.createCanvasElement();
- canvasEl.width = sourceWidth;
- canvasEl.height = sourceHeight;
- this._element = canvasEl;
- this._filteredEl = canvasEl;
- }
- else {
- // clear the existing element to get new filter data
- this._element.getContext('2d').clearRect(0, 0, sourceWidth, sourceHeight);
- }
- if (!fabric.filterBackend) {
- fabric.filterBackend = fabric.initFilterBackend();
- }
- fabric.filterBackend.applyFilters(
- filters, this._originalElement, sourceWidth, sourceHeight, this._element, this.cacheKey);
- 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;
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _render: function(ctx) {
- if (this.isMoving === false && this.resizeFilter && this._needsResize()) {
- this._lastScaleX = this.scaleX;
- this._lastScaleY = this.scaleY;
- this.applyResizeFilters();
- }
- this._stroke(ctx);
- this._renderPaintInOrder(ctx);
- },
-
- _renderFill: function(ctx) {
- 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._filterScalingX,
- this.cropY * this._filterScalingY,
- sW,
- sH,
- x, y, w, h);
- },
-
- /**
- * @private, needed to check if image needs resize
- */
- _needsResize: function() {
- return (this.scaleX !== this._lastScaleX || this.scaleY !== this._lastScaleY);
- },
-
- /**
- * @private
- */
- _resetWidthHeight: function() {
- var element = this.getElement();
-
- this.set('width', element.width);
- this.set('height', element.height);
- },
-
- /**
- * The Image class's initialization method. This method is automatically
- * called by the constructor.
- * @private
- * @param {HTMLImageElement|String} element The element representing the image
- * @param {Object} [options] Options object
- */
- _initElement: function(element, options) {
- this.setElement(fabric.util.getById(element), options);
- fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS);
- },
-
- /**
- * @private
- * @param {Object} [options] Options object
- */
- _initConfig: function(options) {
- options || (options = { });
- this.setOptions(options);
- this._setWidthHeight(options);
- if (this._element && this.crossOrigin) {
- this._element.crossOrigin = this.crossOrigin;
- }
- },
-
- /**
- * @private
- * @param {Array} filters to be initialized
- * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created
- */
- _initFilters: function(filters, callback) {
- if (filters && filters.length) {
- fabric.util.enlivenObjects(filters, function(enlivenedObjects) {
- callback && callback(enlivenedObjects);
- }, 'fabric.Image.filters');
- }
- else {
- callback && callback();
- }
- },
-
- /**
- * @private
- * @param {Object} [options] Object with width/height properties
- */
- _setWidthHeight: function(options) {
- this.width = 'width' in options
- ? options.width
- : (this.getElement()
- ? this.getElement().width || 0
- : 0);
-
- this.height = 'height' in options
- ? options.height
- : (this.getElement()
- ? this.getElement().height || 0
- : 0);
- },
-
- /**
- * Calculate offset for center and scale factor for the image in order to respect
- * the preserveAspectRatio attribute
- * @private
- * @return {Object}
- */
- parsePreserveAspectRatioAttribute: function() {
- var pAR = fabric.util.parsePreserveAspectRatioAttribute(this.preserveAspectRatio || ''),
- rWidth = this._element.width, rHeight = this._element.height,
- scaleX = 1, scaleY = 1, offsetLeft = 0, offsetTop = 0, cropX = 0, cropY = 0,
- offset, pWidth = this.width, pHeight = this.height, parsedAttributes = { width: pWidth, height: pHeight };
- if (pAR && (pAR.alignX !== 'none' || pAR.alignY !== 'none')) {
- if (pAR.meetOrSlice === 'meet') {
- scaleX = scaleY = fabric.util.findScaleToFit(this._element, parsedAttributes);
- offset = (pWidth - rWidth * scaleX) / 2;
- if (pAR.alignX === 'Min') {
- offsetLeft = -offset;
- }
- if (pAR.alignX === 'Max') {
- offsetLeft = offset;
- }
- offset = (pHeight - rHeight * scaleY) / 2;
- if (pAR.alignY === 'Min') {
- offsetTop = -offset;
- }
- if (pAR.alignY === 'Max') {
- offsetTop = offset;
- }
- }
- if (pAR.meetOrSlice === 'slice') {
- scaleX = scaleY = fabric.util.findScaleToCover(this._element, parsedAttributes);
- offset = rWidth - pWidth / scaleX;
- if (pAR.alignX === 'Mid') {
- cropX = offset / 2;
- }
- if (pAR.alignX === 'Max') {
- cropX = offset;
- }
- offset = rHeight - pHeight / scaleY;
- if (pAR.alignY === 'Mid') {
- cropY = offset / 2;
- }
- if (pAR.alignY === 'Max') {
- cropY = offset;
- }
- rWidth = pWidth / scaleX;
- rHeight = pHeight / scaleY;
- }
- }
- else {
- scaleX = pWidth / rWidth;
- scaleY = pHeight / rHeight;
- }
- return {
- width: rWidth,
- height: rHeight,
- scaleX: scaleX,
- scaleY: scaleY,
- offsetLeft: offsetLeft,
- offsetTop: offsetTop,
- cropX: cropX,
- cropY: cropY
- };
- }
- });
-
- /**
- * Default CSS class name for canvas
- * @static
- * @type String
- * @default
- */
- fabric.Image.CSS_CANVAS = 'canvas-img';
-
- /**
- * Alias for getSrc
- * @static
- */
- fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc;
-
- /**
- * Creates an instance of fabric.Image from its object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {Function} callback Callback to invoke when an image instance is created
- */
- fabric.Image.fromObject = function(_object, callback) {
- var object = fabric.util.object.clone(_object);
- fabric.util.loadImage(object.src, function(img, error) {
- if (error) {
- callback && callback(null, error);
- return;
- }
- fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) {
- object.filters = filters || [];
- fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) {
- object.resizeFilter = resizeFilters[0];
- var image = new fabric.Image(img, object);
- callback(image);
- });
- });
- }, null, object.crossOrigin);
- };
-
- /**
- * Creates an instance of fabric.Image from an URL string
- * @static
- * @param {String} url URL to create an image from
- * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument)
- * @param {Object} [imgOptions] Options object
- */
- fabric.Image.fromURL = function(url, callback, imgOptions) {
- fabric.util.loadImage(url, function(img) {
- callback && callback(new fabric.Image(img, imgOptions));
- }, null, imgOptions && imgOptions.crossOrigin);
- };
-
- /* _FROM_SVG_START_ */
- /**
- * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement})
- * @static
- * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement}
- */
- fabric.Image.ATTRIBUTE_NAMES =
- fabric.SHARED_ATTRIBUTES.concat('x y width height preserveAspectRatio xlink:href crossOrigin'.split(' '));
-
- /**
- * Returns {@link fabric.Image} instance from an SVG element
- * @static
- * @param {SVGElement} element Element to parse
- * @param {Object} [options] Options object
- * @param {Function} callback Callback to execute when fabric.Image object is created
- * @return {fabric.Image} Instance of fabric.Image
- */
- fabric.Image.fromElement = function(element, callback, options) {
- var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES);
- fabric.Image.fromURL(parsedAttributes['xlink:href'], callback,
- extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes));
- };
- /* _FROM_SVG_END_ */
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
-
- /**
- * @private
- * @return {Number} angle value
- */
- _getAngleValueForStraighten: function() {
- var angle = this.angle % 360;
- if (angle > 0) {
- return Math.round((angle - 1) / 90) * 90;
- }
- return Math.round(angle / 90) * 90;
- },
-
- /**
- * Straightens an object (rotating it from current angle to one of 0, 90, 180, 270, etc. depending on which is closer)
- * @return {fabric.Object} thisArg
- * @chainable
- */
- straighten: function() {
- this.rotate(this._getAngleValueForStraighten());
- return this;
- },
-
- /**
- * Same as {@link fabric.Object.prototype.straighten} but with animation
- * @param {Object} callbacks Object with callback functions
- * @param {Function} [callbacks.onComplete] Invoked on completion
- * @param {Function} [callbacks.onChange] Invoked on every step of animation
- * @return {fabric.Object} thisArg
- * @chainable
- */
- fxStraighten: function(callbacks) {
- callbacks = callbacks || { };
-
- var empty = function() { },
- onComplete = callbacks.onComplete || empty,
- onChange = callbacks.onChange || empty,
- _this = this;
-
- fabric.util.animate({
- startValue: this.get('angle'),
- endValue: this._getAngleValueForStraighten(),
- duration: this.FX_DURATION,
- onChange: function(value) {
- _this.rotate(value);
- onChange();
- },
- onComplete: function() {
- _this.setCoords();
- onComplete();
- },
- });
-
- return this;
- }
-});
-
-fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ {
-
- /**
- * Straightens object, then rerenders canvas
- * @param {fabric.Object} object Object to straighten
- * @return {fabric.Canvas} thisArg
- * @chainable
- */
- straightenObject: function (object) {
- object.straighten();
- this.requestRenderAll();
- return this;
- },
-
- /**
- * Same as {@link fabric.Canvas.prototype.straightenObject}, but animated
- * @param {fabric.Object} object Object to straighten
- * @return {fabric.Canvas} thisArg
- * @chainable
- */
- fxStraightenObject: function (object) {
- object.fxStraighten({
- onChange: this.requestRenderAllBound
- });
- return this;
- }
-});
-
-
-(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';
- var 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);
- 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 = { premultipliedAlpha: 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;
- },
-
- /**
- * The same as the applyFilter method but with additional logging of WebGL
- * errors.
- */
- applyFiltersDebug: function(filters, source, width, height, targetCanvas, cacheKey) {
- // The following code is useful when debugging a specific issue but adds ~10x slowdown.
- var gl = this.gl;
- var ret = this.applyFilters(filters, source, width, height, targetCanvas, cacheKey);
- var glError = gl.getError();
- if (glError !== gl.NO_ERROR) {
- var errorString = this.glErrorToString(gl, glError);
- var error = new Error('WebGL Error ' + errorString);
- error.glErrorCode = glError;
- throw error;
- }
- return ret;
- },
-
- glErrorToString: function(context, errorCode) {
- if (!context) {
- return 'Context undefined for error code: ' + errorCode;
- }
- else if (typeof errorCode !== 'number') {
- return 'Error code is not a number';
- }
- switch (errorCode) {
- case context.NO_ERROR:
- return 'NO_ERROR';
- case context.INVALID_ENUM:
- return 'INVALID_ENUM';
- case context.INVALID_VALUE:
- return 'INVALID_VALUE';
- case context.INVALID_OPERATION:
- return 'INVALID_OPERATION';
- case context.INVALID_FRAMEBUFFER_OPERATION:
- return 'INVALID_FRAMEBUFFER_OPERATION';
- case context.OUT_OF_MEMORY:
- return 'OUT_OF_MEMORY';
- case context.CONTEXT_LOST_WEBGL:
- return 'CONTEXT_LOST_WEBGL';
- default:
- return 'UNKNOWN_ERROR';
- }
- },
-
- /**
- * 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;
- var ext = gl.getExtension('WEBGL_debug_renderer_info');
- var gpuInfo = { renderer: '', vendor: '' };
- 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);
-}
-
-
-(function() {
-
- 'use strict';
-
- var noop = function() {};
-
- fabric.Canvas2dFilterBackend = Canvas2dFilterBackend;
-
- /**
- * Canvas 2D filter backend.
- */
- 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;
- },
-
- };
-})();
-
-
-/**
- * @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}
- */
-fabric.Image.filters = fabric.Image.filters || { };
-
-/**
- * Root filter class from which all filter classes inherit from
- * @class fabric.Image.filters.BaseFilter
- * @memberOf fabric.Image.filters
- */
-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 (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);
- 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.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, 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;
- },
-
- /**
- * Intentionally left blank, to be overridden in custom filters
- * @param {Object} options
- **/
- isNeutralState: function(/* options */) {
- 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) {
- if (options.passes > 1 && this.isNeutralState(options)) {
- // avoid doing something that we do not need
- return;
- }
- this._setupFrameBuffer(options);
- this.applyToWebGL(options);
- this._swapTextures(options);
- }
- else if (!this.isNeutralState()) {
- 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(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: 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();
- }
-});
-
-fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
- var filter = new fabric.Image.filters[object.type](object);
- callback && callback(filter);
- return filter;
-};
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Color Matrix filter class
- * @class fabric.Image.filters.ColorMatrix
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.ColorMatrix#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @see {@Link http://www.webwasp.co.uk/tutorials/219/Color_Matrix_Filter.php}
- * @see {@Link http://phoboslab.org/log/2013/11/fast-image-filters-with-webgl}
- * @example Kodachrome filter
- * var filter = new fabric.Image.filters.ColorMatrix({
- * matrix: [
- 1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502,
- -0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203,
- -0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946,
- 0, 0, 0, 1, 0
- ]
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- */
- filters.ColorMatrix = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.ColorMatrix.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'ColorMatrix',
-
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'uniform mat4 uColorMatrix;\n' +
- 'uniform vec4 uConstants;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'color *= uColorMatrix;\n' +
- 'color += uConstants;\n' +
- 'gl_FragColor = color;\n' +
- '}',
-
- /**
- * Colormatrix for pixels.
- * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning
- * outside the -1, 1 range.
- * 0.0039215686 is the part of 1 that get translated to 1 in 2d
- * @param {Array} matrix array of 20 numbers.
- * @default
- */
- matrix: [
- 1, 0, 0, 0, 0,
- 0, 1, 0, 0, 0,
- 0, 0, 1, 0, 0,
- 0, 0, 0, 1, 0
- ],
-
- mainParameter: 'matrix',
-
- /**
- * Lock the colormatrix on the color part, skipping alpha, manly for non webgl scenario
- * to save some calculation
- */
- colorsOnly: true,
-
- /**
- * Constructor
- * @param {Object} [options] Options object
- */
- initialize: function(options) {
- this.callSuper('initialize', options);
- // create a new array instead mutating the prototype with push
- 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.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8Array to be filtered.
- */
- applyTo2d: function(options) {
- var imageData = options.imageData,
- data = imageData.data,
- iLen = data.length,
- m = this.matrix,
- r, g, b, a, i, colorsOnly = this.colorsOnly;
-
- for (i = 0; i < iLen; i += 4) {
- r = data[i];
- g = data[i + 1];
- b = data[i + 2];
- if (colorsOnly) {
- data[i] = r * m[0] + g * m[1] + b * m[2] + m[4] * 255;
- data[i + 1] = r * m[5] + g * m[6] + b * m[7] + m[9] * 255;
- data[i + 2] = r * m[10] + g * m[11] + b * m[12] + m[14] * 255;
- }
- else {
- a = data[i + 3];
- data[i] = r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255;
- data[i + 1] = r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255;
- data[i + 2] = r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255;
- data[i + 3] = r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255;
- }
- }
- },
-
- /**
- * 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 {
- uColorMatrix: gl.getUniformLocation(program, 'uColorMatrix'),
- uConstants: gl.getUniformLocation(program, 'uConstants'),
- };
- },
-
- /**
- * 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) {
- var m = this.matrix,
- matrix = [
- m[0], m[1], m[2], m[3],
- m[5], m[6], m[7], m[8],
- m[10], m[11], m[12], m[13],
- m[15], m[16], m[17], m[18]
- ],
- constants = [m[4], m[9], m[14], m[19]];
- gl.uniformMatrix4fv(uniformLocations.uColorMatrix, false, matrix);
- gl.uniform4fv(uniformLocations.uConstants, constants);
- },
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] function to invoke after filter creation
- * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix
- */
- fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Brightness filter class
- * @class fabric.Image.filters.Brightness
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Brightness({
- * brightness: 0.05
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- */
- filters.Brightness = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Brightness',
-
- /**
- * Fragment source for the brightness program
- */
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uBrightness;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'color.rgb += uBrightness;\n' +
- 'gl_FragColor = color;\n' +
- '}',
-
- /**
- * Brightness value, from -1 to 1.
- * translated to -255 to 255 for 2d
- * 0.0039215686 is the part of 1 that get translated to 1 in 2d
- * @param {Number} brightness
- * @default
- */
- brightness: 0,
-
- /**
- * Describe the property that is the filter parameter
- * @param {String} m
- * @default
- */
- mainParameter: 'brightness',
-
- /**
- * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered.
- */
- applyTo2d: function(options) {
- if (this.brightness === 0) {
- return;
- }
- var imageData = options.imageData,
- data = imageData.data, i, len = data.length,
- brightness = Math.round(this.brightness * 255);
- for (i = 0; i < len; i += 4) {
- data[i] = data[i] + brightness;
- data[i + 1] = data[i + 1] + brightness;
- data[i + 2] = data[i + 2] + brightness;
- }
- },
-
- /**
- * 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 {
- uBrightness: gl.getUniformLocation(program, 'uBrightness'),
- };
- },
-
- /**
- * 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.uniform1f(uniformLocations.uBrightness, this.brightness);
- },
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness
- */
- fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- extend = fabric.util.object.extend,
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Adapted from html5rocks article
- * @class fabric.Image.filters.Convolute
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example Sharpen filter
- * var filter = new fabric.Image.filters.Convolute({
- * matrix: [ 0, -1, 0,
- * -1, 5, -1,
- * 0, -1, 0 ]
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- * canvas.renderAll();
- * @example Blur filter
- * var filter = new fabric.Image.filters.Convolute({
- * matrix: [ 1/9, 1/9, 1/9,
- * 1/9, 1/9, 1/9,
- * 1/9, 1/9, 1/9 ]
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- * canvas.renderAll();
- * @example Emboss filter
- * var filter = new fabric.Image.filters.Convolute({
- * matrix: [ 1, 1, 1,
- * 1, 0.7, -1,
- * -1, -1, -1 ]
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- * canvas.renderAll();
- * @example Emboss filter with opaqueness
- * var filter = new fabric.Image.filters.Convolute({
- * opaque: true,
- * matrix: [ 1, 1, 1,
- * 1, 0.7, -1,
- * -1, -1, -1 ]
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- * canvas.renderAll();
- */
- filters.Convolute = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Convolute',
-
- /*
- * Opaque value (true/false)
- */
- opaque: false,
-
- /*
- * matrix for the filter, max 9x9
- */
- matrix: [0, 0, 0, 0, 1, 0, 0, 0, 0],
-
- /**
- * Fragment source for the brightness program
- */
- fragmentSource: {
- Convolute_3_1: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uMatrix[9];\n' +
- 'uniform float uStepW;\n' +
- 'uniform float uStepH;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = vec4(0, 0, 0, 0);\n' +
- 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' +
- 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' +
- 'vec2 matrixPos = vec2(uStepW * (w - 1), uStepH * (h - 1));\n' +
- 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 3.0 + w)];\n' +
- '}\n' +
- '}\n' +
- 'gl_FragColor = color;\n' +
- '}',
- Convolute_3_0: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uMatrix[9];\n' +
- 'uniform float uStepW;\n' +
- 'uniform float uStepH;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = vec4(0, 0, 0, 1);\n' +
- 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' +
- 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' +
- 'vec2 matrixPos = vec2(uStepW * (w - 1.0), uStepH * (h - 1.0));\n' +
- 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 3.0 + w)];\n' +
- '}\n' +
- '}\n' +
- 'float alpha = texture2D(uTexture, vTexCoord).a;\n' +
- 'gl_FragColor = color;\n' +
- 'gl_FragColor.a = alpha;\n' +
- '}',
- Convolute_5_1: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uMatrix[25];\n' +
- 'uniform float uStepW;\n' +
- 'uniform float uStepH;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = vec4(0, 0, 0, 0);\n' +
- 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' +
- 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' +
- 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' +
- 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 5.0 + w)];\n' +
- '}\n' +
- '}\n' +
- 'gl_FragColor = color;\n' +
- '}',
- Convolute_5_0: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uMatrix[25];\n' +
- 'uniform float uStepW;\n' +
- 'uniform float uStepH;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = vec4(0, 0, 0, 1);\n' +
- 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' +
- 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' +
- 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' +
- 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 5.0 + w)];\n' +
- '}\n' +
- '}\n' +
- 'float alpha = texture2D(uTexture, vTexCoord).a;\n' +
- 'gl_FragColor = color;\n' +
- 'gl_FragColor.a = alpha;\n' +
- '}',
- Convolute_7_1: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uMatrix[49];\n' +
- 'uniform float uStepW;\n' +
- 'uniform float uStepH;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = vec4(0, 0, 0, 0);\n' +
- 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' +
- 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' +
- 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' +
- 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 7.0 + w)];\n' +
- '}\n' +
- '}\n' +
- 'gl_FragColor = color;\n' +
- '}',
- Convolute_7_0: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uMatrix[49];\n' +
- 'uniform float uStepW;\n' +
- 'uniform float uStepH;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = vec4(0, 0, 0, 1);\n' +
- 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' +
- 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' +
- 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' +
- 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 7.0 + w)];\n' +
- '}\n' +
- '}\n' +
- 'float alpha = texture2D(uTexture, vTexCoord).a;\n' +
- 'gl_FragColor = color;\n' +
- 'gl_FragColor.a = alpha;\n' +
- '}',
- Convolute_9_1: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uMatrix[81];\n' +
- 'uniform float uStepW;\n' +
- 'uniform float uStepH;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = vec4(0, 0, 0, 0);\n' +
- 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' +
- 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' +
- 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' +
- 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 9.0 + w)];\n' +
- '}\n' +
- '}\n' +
- 'gl_FragColor = color;\n' +
- '}',
- Convolute_9_0: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uMatrix[81];\n' +
- 'uniform float uStepW;\n' +
- 'uniform float uStepH;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = vec4(0, 0, 0, 1);\n' +
- 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' +
- 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' +
- 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' +
- 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 9.0 + w)];\n' +
- '}\n' +
- '}\n' +
- 'float alpha = texture2D(uTexture, vTexCoord).a;\n' +
- 'gl_FragColor = color;\n' +
- 'gl_FragColor.a = alpha;\n' +
- '}',
- },
-
- /**
- * Constructor
- * @memberOf fabric.Image.filters.Convolute.prototype
- * @param {Object} [options] Options object
- * @param {Boolean} [options.opaque=false] Opaque value (true/false)
- * @param {Array} [options.matrix] Filter matrix
- */
-
-
- /**
- * 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 size = Math.sqrt(this.matrix.length);
- var cacheKey = this.type + '_' + size + '_' + (this.opaque ? 1 : 0);
- var shaderSource = this.fragmentSource[cacheKey];
- if (!options.programCache.hasOwnProperty(cacheKey)) {
- options.programCache[cacheKey] = this.createProgram(options.context, shaderSource);
- }
- return options.programCache[cacheKey];
- },
-
- /**
- * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered.
- */
- applyTo2d: function(options) {
- var imageData = options.imageData,
- data = imageData.data,
- weights = this.matrix,
- side = Math.round(Math.sqrt(weights.length)),
- halfSide = Math.floor(side / 2),
- sw = imageData.width,
- sh = imageData.height,
- output = options.ctx.createImageData(sw, sh),
- dst = output.data,
- // go through the destination image pixels
- alphaFac = this.opaque ? 1 : 0,
- r, g, b, a, dstOff,
- scx, scy, srcOff, wt,
- x, y, cx, cy;
-
- for (y = 0; y < sh; y++) {
- for (x = 0; x < sw; x++) {
- dstOff = (y * sw + x) * 4;
- // calculate the weighed sum of the source image pixels that
- // fall under the convolution matrix
- r = 0; g = 0; b = 0; a = 0;
-
- for (cy = 0; cy < side; cy++) {
- for (cx = 0; cx < side; cx++) {
- scy = y + cy - halfSide;
- scx = x + cx - halfSide;
-
- // eslint-disable-next-line max-depth
- if (scy < 0 || scy > sh || scx < 0 || scx > sw) {
- continue;
- }
-
- srcOff = (scy * sw + scx) * 4;
- wt = weights[cy * side + cx];
-
- r += data[srcOff] * wt;
- g += data[srcOff + 1] * wt;
- b += data[srcOff + 2] * wt;
- // eslint-disable-next-line max-depth
- if (!alphaFac) {
- a += data[srcOff + 3] * wt;
- }
- }
- }
- dst[dstOff] = r;
- dst[dstOff + 1] = g;
- dst[dstOff + 2] = b;
- if (!alphaFac) {
- dst[dstOff + 3] = a;
- }
- else {
- dst[dstOff + 3] = data[dstOff + 3];
- }
- }
- }
- options.imageData = output;
- },
-
- /**
- * 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 {
- uMatrix: gl.getUniformLocation(program, 'uMatrix'),
- uOpaque: gl.getUniformLocation(program, 'uOpaque'),
- uHalfSize: gl.getUniformLocation(program, 'uHalfSize'),
- uSize: gl.getUniformLocation(program, 'uSize'),
- };
- },
-
- /**
- * 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.uniform1fv(uniformLocations.uMatrix, this.matrix);
- },
-
- /**
- * Returns object representation of an instance
- * @return {Object} Object representation of an instance
- */
- toObject: function() {
- return extend(this.callSuper('toObject'), {
- opaque: this.opaque,
- matrix: this.matrix
- });
- }
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute
- */
- fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Grayscale image filter class
- * @class fabric.Image.filters.Grayscale
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Grayscale();
- * object.filters.push(filter);
- * object.applyFilters();
- */
- filters.Grayscale = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Grayscale',
-
- fragmentSource: {
- average: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'float average = (color.r + color.b + color.g) / 3.0;\n' +
- 'gl_FragColor = vec4(average, average, average, color.a);\n' +
- '}',
- lightness: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform int uMode;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 col = texture2D(uTexture, vTexCoord);\n' +
- 'float average = (max(max(col.r, col.g),col.b) + min(min(col.r, col.g),col.b)) / 2.0;\n' +
- 'gl_FragColor = vec4(average, average, average, col.a);\n' +
- '}',
- luminosity: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform int uMode;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 col = texture2D(uTexture, vTexCoord);\n' +
- 'float average = 0.21 * col.r + 0.72 * col.g + 0.07 * col.b;\n' +
- 'gl_FragColor = vec4(average, average, average, col.a);\n' +
- '}',
- },
-
-
- /**
- * Grayscale mode, between 'average', 'lightness', 'luminosity'
- * @param {String} type
- * @default
- */
- mode: 'average',
-
- mainParameter: 'mode',
-
- /**
- * Apply the Grayscale operation to a Uint8Array representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8Array to be filtered.
- */
- applyTo2d: function(options) {
- var imageData = options.imageData,
- data = imageData.data, i,
- len = data.length, value,
- mode = this.mode;
- for (i = 0; i < len; i += 4) {
- if (mode === 'average') {
- value = (data[i] + data[i + 1] + data[i + 2]) / 3;
- }
- else if (mode === 'lightness') {
- value = (Math.min(data[i], data[i + 1], data[i + 2]) +
- Math.max(data[i], data[i + 1], data[i + 2])) / 2;
- }
- else if (mode === 'luminosity') {
- value = 0.21 * data[i] + 0.72 * data[i + 1] + 0.07 * data[i + 2];
- }
- data[i] = value;
- data[i + 1] = value;
- data[i + 2] = value;
- }
- },
-
- /**
- * 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 cacheKey = this.type + '_' + this.mode;
- if (!options.programCache.hasOwnProperty(cacheKey)) {
- var shaderSource = this.fragmentSource[this.mode];
- options.programCache[cacheKey] = this.createProgram(options.context, shaderSource);
- }
- return options.programCache[cacheKey];
- },
-
- /**
- * 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 {
- uMode: gl.getUniformLocation(program, 'uMode'),
- };
- },
-
- /**
- * 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) {
- // default average mode.
- var mode = 1;
- gl.uniform1i(uniformLocations.uMode, mode);
- },
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale
- */
- fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Invert filter class
- * @class fabric.Image.filters.Invert
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Invert();
- * object.filters.push(filter);
- * object.applyFilters(canvas.renderAll.bind(canvas));
- */
- filters.Invert = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Invert',
-
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform int uInvert;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'if (uInvert == 1) {\n' +
- 'gl_FragColor = vec4(1.0 - color.r,1.0 -color.g,1.0 -color.b,color.a);\n' +
- '} else {\n' +
- 'gl_FragColor = color;\n' +
- '}\n' +
- '}',
-
- /**
- * Filter invert. if false, does nothing
- * @param {Boolean} invert
- * @default
- */
- invert: true,
-
- mainParameter: 'invert',
-
- /**
- * Apply the Invert operation to a Uint8Array representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8Array to be filtered.
- */
- applyTo2d: function(options) {
- if (!this.invert) {
- return;
- }
- var imageData = options.imageData,
- data = imageData.data, i,
- len = data.length;
- for (i = 0; i < len; i += 4) {
- data[i] = 255 - data[i];
- data[i + 1] = 255 - data[i + 1];
- data[i + 2] = 255 - data[i + 2];
- }
- },
-
- /**
- * 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 {
- uInvert: gl.getUniformLocation(program, 'uInvert'),
- };
- },
-
- /**
- * 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.uniform1i(uniformLocations.uInvert, this.invert);
- },
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert
- */
- fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- extend = fabric.util.object.extend,
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Noise filter class
- * @class fabric.Image.filters.Noise
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Noise({
- * noise: 700
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- * canvas.renderAll();
- */
- filters.Noise = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Noise',
-
- /**
- * Fragment source for the noise program
- */
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uStepH;\n' +
- 'uniform float uNoise;\n' +
- 'uniform float uSeed;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'float rand(vec2 co, float seed, float vScale) {\n' +
- 'return fract(sin(dot(co.xy * vScale ,vec2(12.9898 , 78.233))) * 43758.5453 * (seed + 0.01) / 2.0);\n' +
- '}\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'color.rgb += (0.5 - rand(vTexCoord, uSeed, 0.1 / uStepH)) * uNoise;\n' +
- 'gl_FragColor = color;\n' +
- '}',
-
- /**
- * Describe the property that is the filter parameter
- * @param {String} m
- * @default
- */
- mainParameter: 'noise',
-
- /**
- * Noise value, from
- * @param {Number} noise
- * @default
- */
- noise: 0,
-
- /**
- * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered.
- */
- applyTo2d: function(options) {
- if (this.noise === 0) {
- return;
- }
- var imageData = options.imageData,
- data = imageData.data, i, len = data.length,
- noise = this.noise, rand;
-
- for (i = 0, len = data.length; i < len; i += 4) {
-
- rand = (0.5 - Math.random()) * noise;
-
- data[i] += rand;
- data[i + 1] += rand;
- data[i + 2] += rand;
- }
- },
-
- /**
- * 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 {
- uNoise: gl.getUniformLocation(program, 'uNoise'),
- uSeed: gl.getUniformLocation(program, 'uSeed'),
- };
- },
-
- /**
- * 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.uniform1f(uniformLocations.uNoise, this.noise / 255);
- gl.uniform1f(uniformLocations.uSeed, Math.random());
- },
-
- /**
- * Returns object representation of an instance
- * @return {Object} Object representation of an instance
- */
- toObject: function() {
- return extend(this.callSuper('toObject'), {
- noise: this.noise
- });
- }
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise
- */
- fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Pixelate filter class
- * @class fabric.Image.filters.Pixelate
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Pixelate({
- * blocksize: 8
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- */
- filters.Pixelate = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Pixelate',
-
- blocksize: 4,
-
- mainParameter: 'blocksize',
-
- /**
- * Fragment source for the Pixelate program
- */
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uBlocksize;\n' +
- 'uniform float uStepW;\n' +
- 'uniform float uStepH;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'float blockW = uBlocksize * uStepW;\n' +
- 'float blockH = uBlocksize * uStepW;\n' +
- 'int posX = int(vTexCoord.x / blockW);\n' +
- 'int posY = int(vTexCoord.y / blockH);\n' +
- 'float fposX = float(posX);\n' +
- 'float fposY = float(posY);\n' +
- 'vec2 squareCoords = vec2(fposX * blockW, fposY * blockH);\n' +
- 'vec4 color = texture2D(uTexture, squareCoords);\n' +
- 'gl_FragColor = color;\n' +
- '}',
-
- /**
- * Apply the Pixelate operation to a Uint8ClampedArray representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered.
- */
- applyTo2d: function(options) {
- if (this.blocksize === 1) {
- return;
- }
- var imageData = options.imageData,
- data = imageData.data,
- iLen = imageData.height,
- jLen = imageData.width,
- index, i, j, r, g, b, a,
- _i, _j, _iLen, _jLen;
-
- for (i = 0; i < iLen; i += this.blocksize) {
- for (j = 0; j < jLen; j += this.blocksize) {
-
- index = (i * 4) * jLen + (j * 4);
-
- r = data[index];
- g = data[index + 1];
- b = data[index + 2];
- a = data[index + 3];
-
- _iLen = Math.min(i + this.blocksize, iLen);
- _jLen = Math.min(j + this.blocksize, jLen);
- for (_i = i; _i < _iLen; _i++) {
- for (_j = j; _j < _jLen; _j++) {
- index = (_i * 4) * jLen + (_j * 4);
- data[index] = r;
- data[index + 1] = g;
- data[index + 2] = b;
- data[index + 3] = a;
- }
- }
- }
- }
- },
-
- /**
- * 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.
- *
- * @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 {
- uBlocksize: gl.getUniformLocation(program, 'uBlocksize'),
- uStepW: gl.getUniformLocation(program, 'uStepW'),
- uStepH: gl.getUniformLocation(program, 'uStepH'),
- };
- },
-
- /**
- * 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.uniform1f(uniformLocations.uBlocksize, this.blocksize);
- },
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate
- */
- fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- extend = fabric.util.object.extend,
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Remove white filter class
- * @class fabric.Image.filters.RemoveColor
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.RemoveColor#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.RemoveColor({
- * threshold: 0.2,
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- * canvas.renderAll();
- */
- filters.RemoveColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.RemoveColor.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'RemoveColor',
-
- /**
- * Color to remove, in any format understood by fabric.Color.
- * @param {String} type
- * @default
- */
- color: '#FFFFFF',
-
- /**
- * Fragment source for the brightness program
- */
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uLow;\n' +
- 'uniform vec4 uHigh;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' +
- 'if(all(greaterThan(gl_FragColor.rgb,uLow.rgb)) && all(greaterThan(uHigh.rgb,gl_FragColor.rgb))) {\n' +
- 'gl_FragColor.a = 0.0;\n' +
- '}\n' +
- '}',
-
- /**
- * distance to actual color, as value up or down from each r,g,b
- * between 0 and 1
- **/
- distance: 0.02,
-
- /**
- * For color to remove inside distance, use alpha channel for a smoother deletion
- * NOT IMPLEMENTED YET
- **/
- useAlpha: false,
-
- /**
- * Constructor
- * @memberOf fabric.Image.filters.RemoveWhite.prototype
- * @param {Object} [options] Options object
- * @param {Number} [options.color=#RRGGBB] Threshold value
- * @param {Number} [options.distance=10] Distance value
- */
-
- /**
- * Applies filter to canvas element
- * @param {Object} canvasEl Canvas element to apply filter to
- */
- applyTo2d: function(options) {
- var imageData = options.imageData,
- data = imageData.data, i,
- distance = this.distance * 255,
- r, g, b,
- source = new fabric.Color(this.color).getSource(),
- lowC = [
- source[0] - distance,
- source[1] - distance,
- source[2] - distance,
- ],
- highC = [
- source[0] + distance,
- source[1] + distance,
- source[2] + distance,
- ];
-
-
- for (i = 0; i < data.length; i += 4) {
- r = data[i];
- g = data[i + 1];
- b = data[i + 2];
-
- if (r > lowC[0] &&
- g > lowC[1] &&
- b > lowC[2] &&
- r < highC[0] &&
- g < highC[1] &&
- b < highC[2]) {
- data[i + 3] = 0;
- }
- }
- },
-
- /**
- * 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 {
- uLow: gl.getUniformLocation(program, 'uLow'),
- uHigh: gl.getUniformLocation(program, 'uHigh'),
- };
- },
-
- /**
- * 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) {
- var source = new fabric.Color(this.color).getSource(),
- distance = parseFloat(this.distance),
- lowC = [
- 0 + source[0] / 255 - distance,
- 0 + source[1] / 255 - distance,
- 0 + source[2] / 255 - distance,
- 1
- ],
- highC = [
- source[0] / 255 + distance,
- source[1] / 255 + distance,
- source[2] / 255 + distance,
- 1
- ];
- gl.uniform4fv(uniformLocations.uLow, lowC);
- gl.uniform4fv(uniformLocations.uHigh, highC);
- },
-
- /**
- * Returns object representation of an instance
- * @return {Object} Object representation of an instance
- */
- toObject: function() {
- return extend(this.callSuper('toObject'), {
- color: this.color,
- distance: this.distance
- });
- }
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite
- */
- fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- var matrices = {
- Brownie: [
- 0.59970,0.34553,-0.27082,0,0.186,
- -0.03770,0.86095,0.15059,0,-0.1449,
- 0.24113,-0.07441,0.44972,0,-0.02965,
- 0,0,0,1,0
- ],
- Vintage: [
- 0.62793,0.32021,-0.03965,0,0.03784,
- 0.02578,0.64411,0.03259,0,0.02926,
- 0.04660,-0.08512,0.52416,0,0.02023,
- 0,0,0,1,0
- ],
- Kodachrome: [
- 1.12855,-0.39673,-0.03992,0,0.24991,
- -0.16404,1.08352,-0.05498,0,0.09698,
- -0.16786,-0.56034,1.60148,0,0.13972,
- 0,0,0,1,0
- ],
- Technicolor: [
- 1.91252,-0.85453,-0.09155,0,0.04624,
- -0.30878,1.76589,-0.10601,0,-0.27589,
- -0.23110,-0.75018,1.84759,0,0.12137,
- 0,0,0,1,0
- ],
- Polaroid: [
- 1.438,-0.062,-0.062,0,0,
- -0.122,1.378,-0.122,0,0,
- -0.016,-0.016,1.483,0,0,
- 0,0,0,1,0
- ],
- Sepia: [
- 0.393, 0.769, 0.189, 0, 0,
- 0.349, 0.686, 0.168, 0, 0,
- 0.272, 0.534, 0.131, 0, 0,
- 0, 0, 0, 1, 0
- ],
- BlackWhite: [
- 1.5, 1.5, 1.5, 0, -1,
- 1.5, 1.5, 1.5, 0, -1,
- 1.5, 1.5, 1.5, 0, -1,
- 0, 0, 0, 1, 0,
- ]
- };
-
- for (var key in matrices) {
- filters[key] = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.Sepia.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: key,
-
- /**
- * Colormatrix for the effect
- * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning
- * outside the -1, 1 range.
- * @param {Array} matrix array of 20 numbers.
- * @default
- */
- matrix: matrices[key],
-
- /**
- * Lock the matrix export for this kind of static, parameter less filters.
- */
- mainParameter: false,
- /**
- * Lock the colormatrix on the color part, skipping alpha
- */
- colorsOnly: true,
-
- });
- fabric.Image.filters[key].fromObject = fabric.Image.filters.BaseFilter.fromObject;
- }
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
- 'use strict';
-
- var fabric = global.fabric,
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Color Blend filter class
- * @class fabric.Image.filter.BlendColor
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @example
- * var filter = new fabric.Image.filters.BlendColor({
- * color: '#000',
- * mode: 'multiply'
- * });
- *
- * var filter = new fabric.Image.filters.BlendImage({
- * image: fabricImageObject,
- * mode: 'multiply',
- * alpha: 0.5
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- * canvas.renderAll();
- */
-
- filters.BlendColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blend.prototype */ {
- type: 'BlendColor',
-
- /**
- * Color to make the blend operation with. default to a reddish color since black or white
- * gives always strong result.
- **/
- color: '#F95C63',
-
- /**
- * Blend mode for the filter: one of multiply, add, diff, screen, subtract,
- * darken, lighten, overlay, exclusion, tint.
- **/
- mode: 'multiply',
-
- /**
- * alpha value. represent the strength of the blend color operation.
- **/
- alpha: 1,
-
- /**
- * Fragment source for the Multiply program
- */
- fragmentSource: {
- multiply: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'color.rgb *= uColor.rgb;\n' +
- 'gl_FragColor = color;\n' +
- '}',
- screen: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'color.rgb = 1.0 - (1.0 - color.rgb) * (1.0 - uColor.rgb);\n' +
- 'gl_FragColor = color;\n' +
- '}',
- add: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' +
- 'gl_FragColor.rgb += uColor.rgb;\n' +
- '}',
- diff: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' +
- 'gl_FragColor.rgb = abs(gl_FragColor.rgb - uColor.rgb);\n' +
- '}',
- subtract: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' +
- 'gl_FragColor.rgb -= uColor.rgb;\n' +
- '}',
- lighten: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' +
- 'gl_FragColor.rgb = max(gl_FragColor.rgb, uColor.rgb);\n' +
- '}',
- darken: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' +
- 'gl_FragColor.rgb = min(gl_FragColor.rgb, uColor.rgb);\n' +
- '}',
- exclusion: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' +
- 'gl_FragColor.rgb += uColor.rgb - 2.0 * (uColor.rgb * gl_FragColor.rgb);\n' +
- '}',
- overlay: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' +
- 'if (uColor.r < 0.5) {\n' +
- 'gl_FragColor.r *= 2.0 * uColor.r;\n' +
- '} else {\n' +
- 'gl_FragColor.r = 1.0 - 2.0 * (1.0 - gl_FragColor.r) * (1.0 - uColor.r);\n' +
- '}\n' +
- 'if (uColor.g < 0.5) {\n' +
- 'gl_FragColor.g *= 2.0 * uColor.g;\n' +
- '} else {\n' +
- 'gl_FragColor.g = 1.0 - 2.0 * (1.0 - gl_FragColor.g) * (1.0 - uColor.g);\n' +
- '}\n' +
- 'if (uColor.b < 0.5) {\n' +
- 'gl_FragColor.b *= 2.0 * uColor.b;\n' +
- '} else {\n' +
- 'gl_FragColor.b = 1.0 - 2.0 * (1.0 - gl_FragColor.b) * (1.0 - uColor.b);\n' +
- '}\n' +
- '}',
- tint: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' +
- 'gl_FragColor.rgb *= (1.0 - uColor.a);\n' +
- 'gl_FragColor.rgb += uColor.rgb;\n' +
- '}'
- },
-
- /**
- * 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 cacheKey = this.type + '_' + this.mode;
- var shaderSource = this.fragmentSource[this.mode];
- if (!options.programCache.hasOwnProperty(cacheKey)) {
- options.programCache[cacheKey] = this.createProgram(options.context, shaderSource);
- }
- return options.programCache[cacheKey];
- },
-
- /**
- * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered.
- */
- applyTo2d: function(options) {
- var imageData = options.imageData,
- data = imageData.data, iLen = data.length,
- tr, tg, tb,
- r, g, b,
- source, alpha1 = 1 - this.alpha;
-
- source = new fabric.Color(this.color).getSource();
- tr = source[0] * this.alpha;
- tg = source[1] * this.alpha;
- tb = source[2] * this.alpha;
-
- for (var i = 0; i < iLen; i += 4) {
-
- r = data[i];
- g = data[i + 1];
- b = data[i + 2];
-
- switch (this.mode) {
- case 'multiply':
- data[i] = r * tr / 255;
- data[i + 1] = g * tg / 255;
- data[i + 2] = b * tb / 255;
- break;
- case 'screen':
- data[i] = 255 - (255 - r) * (255 - tr) / 255;
- data[i + 1] = 255 - (255 - g) * (255 - tg) / 255;
- data[i + 2] = 255 - (255 - b) * (255 - tb) / 255;
- break;
- case 'add':
- data[i] = r + tr;
- data[i + 1] = g + tg;
- data[i + 2] = b + tb;
- break;
- case 'diff':
- case 'difference':
- data[i] = Math.abs(r - tr);
- data[i + 1] = Math.abs(g - tg);
- data[i + 2] = Math.abs(b - tb);
- break;
- case 'subtract':
- data[i] = r - tr;
- data[i + 1] = g - tg;
- data[i + 2] = b - tb;
- break;
- case 'darken':
- data[i] = Math.min(r, tr);
- data[i + 1] = Math.min(g, tg);
- data[i + 2] = Math.min(b, tb);
- break;
- case 'lighten':
- data[i] = Math.max(r, tr);
- data[i + 1] = Math.max(g, tg);
- data[i + 2] = Math.max(b, tb);
- break;
- case 'overlay':
- data[i] = tr < 128 ? (2 * r * tr / 255) : (255 - 2 * (255 - r) * (255 - tr) / 255);
- data[i + 1] = tg < 128 ? (2 * g * tg / 255) : (255 - 2 * (255 - g) * (255 - tg) / 255);
- data[i + 2] = tb < 128 ? (2 * b * tb / 255) : (255 - 2 * (255 - b) * (255 - tb) / 255);
- break;
- case 'exclusion':
- data[i] = tr + r - ((2 * tr * r) / 255);
- data[i + 1] = tg + g - ((2 * tg * g) / 255);
- data[i + 2] = tb + b - ((2 * tb * b) / 255);
- break;
- case 'tint':
- data[i] = tr + r * alpha1;
- data[i + 1] = tg + g * alpha1;
- data[i + 2] = tb + b * alpha1;
- }
- }
- },
-
- /**
- * 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 {
- uColor: gl.getUniformLocation(program, 'uColor'),
- };
- },
-
- /**
- * 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) {
- var source = new fabric.Color(this.color).getSource();
- source[0] = this.alpha * source[0] / 255;
- source[1] = this.alpha * source[1] / 255;
- source[2] = this.alpha * source[2] / 255;
- source[3] = this.alpha;
- gl.uniform4fv(uniformLocations.uColor, source);
- },
-
- /**
- * Returns object representation of an instance
- * @return {Object} Object representation of an instance
- */
- toObject: function() {
- return {
- type: this.type,
- color: this.color,
- mode: this.mode,
- alpha: this.alpha
- };
- }
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor
- */
- fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
- 'use strict';
-
- var fabric = global.fabric,
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Image Blend filter class
- * @class fabric.Image.filter.BlendImage
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @example
- * var filter = new fabric.Image.filters.BlendColor({
- * color: '#000',
- * mode: 'multiply'
- * });
- *
- * var filter = new fabric.Image.filters.BlendImage({
- * image: fabricImageObject,
- * mode: 'multiply',
- * alpha: 0.5
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- * canvas.renderAll();
- */
-
- filters.BlendImage = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.BlendImage.prototype */ {
- type: 'BlendImage',
-
- /**
- * Color to make the blend operation with. default to a reddish color since black or white
- * gives always strong result.
- **/
- image: null,
-
- /**
- * Blend mode for the filter: one of multiply, add, diff, screen, subtract,
- * darken, lighten, overlay, exclusion, tint.
- **/
- mode: 'multiply',
-
- /**
- * alpha value. represent the strength of the blend color operation.
- **/
- alpha: 1,
-
- vertexSource: 'attribute vec2 aPosition;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'varying vec2 vTexCoord2;\n' +
- 'uniform mat3 uTransformMatrix;\n' +
- 'void main() {\n' +
- 'vTexCoord = aPosition;\n' +
- 'vTexCoord2 = (uTransformMatrix * vec3(aPosition, 1.0)).xy;\n' +
- 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' +
- '}',
-
- /**
- * Fragment source for the Multiply program
- */
- fragmentSource: {
- multiply: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform sampler2D uImage;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'varying vec2 vTexCoord2;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' +
- 'color.rgba *= color2.rgba;\n' +
- 'gl_FragColor = color;\n' +
- '}',
- mask: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform sampler2D uImage;\n' +
- 'uniform vec4 uColor;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'varying vec2 vTexCoord2;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' +
- 'color.a = color2.a;\n' +
- 'gl_FragColor = color;\n' +
- '}',
- },
-
- /**
- * 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 cacheKey = this.type + '_' + this.mode;
- var shaderSource = this.fragmentSource[this.mode];
- if (!options.programCache.hasOwnProperty(cacheKey)) {
- options.programCache[cacheKey] = this.createProgram(options.context, shaderSource);
- }
- return options.programCache[cacheKey];
- },
-
- applyToWebGL: function(options) {
- // load texture to blend.
- var gl = options.context,
- texture = this.createTexture(options.filterBackend, this.image);
- this.bindAdditionalTexture(gl, texture, gl.TEXTURE1);
- this.callSuper('applyToWebGL', options);
- this.unbindAdditionalTexture(gl, gl.TEXTURE1);
- },
-
- createTexture: function(backend, image) {
- return backend.getCachedTexture(image.cacheKey, image._element);
- },
-
- /**
- * Calculate a transformMatrix to adapt the image to blend over
- * @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.
- */
- calculateMatrix: function() {
- var image = this.image,
- width = image._element.width,
- height = image._element.height;
- return [
- 1 / image.scaleX, 0, 0,
- 0, 1 / image.scaleY, 0,
- -image.left / width, -image.top / height, 1
- ];
- },
-
- /**
- * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered.
- */
- applyTo2d: function(options) {
- var imageData = options.imageData,
- resources = options.filterBackend.resources,
- data = imageData.data, iLen = data.length,
- width = options.imageData.width,
- height = options.imageData.height,
- tr, tg, tb, ta,
- r, g, b, a,
- canvas1, context, image = this.image, blendData;
-
- if (!resources.blendImage) {
- resources.blendImage = document.createElement('canvas');
- }
- canvas1 = resources.blendImage;
- if (canvas1.width !== width || canvas1.height !== height) {
- canvas1.width = width;
- canvas1.height = height;
- }
- context = canvas1.getContext('2d');
- context.setTransform(image.scaleX, 0, 0, image.scaleY, image.left, image.top);
- context.drawImage(image._element, 0, 0, width, height);
- blendData = context.getImageData(0, 0, width, height).data;
- for (var i = 0; i < iLen; i += 4) {
-
- r = data[i];
- g = data[i + 1];
- b = data[i + 2];
- a = data[i + 3];
-
- tr = blendData[i];
- tg = blendData[i + 1];
- tb = blendData[i + 2];
- ta = blendData[i + 3];
-
- switch (this.mode) {
- case 'multiply':
- data[i] = r * tr / 255;
- data[i + 1] = g * tg / 255;
- data[i + 2] = b * tb / 255;
- data[i + 3] = a * ta / 255;
- break;
- case 'mask':
- data[i + 3] = ta;
- break;
- }
- }
- },
-
- /**
- * 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 {
- uTransformMatrix: gl.getUniformLocation(program, 'uTransformMatrix'),
- uImage: gl.getUniformLocation(program, 'uImage'),
- };
- },
-
- /**
- * 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) {
- var matrix = this.calculateMatrix();
- gl.uniform1i(uniformLocations.uImage, 1); // texture unit 1.
- gl.uniformMatrix3fv(uniformLocations.uTransformMatrix, false, matrix);
- },
-
- /**
- * Returns object representation of an instance
- * @return {Object} Object representation of an instance
- */
- toObject: function() {
- return {
- type: this.type,
- image: this.image && this.image.toObject(),
- mode: this.mode,
- alpha: this.alpha
- };
- }
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} callback to be invoked after filter creation
- * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage
- */
- fabric.Image.filters.BlendImage.fromObject = function(object, callback) {
- fabric.Image.fromObject(object.image, function(image) {
- var options = fabric.util.object.clone(object);
- options.image = image;
- callback(new fabric.Image.filters.BlendImage(options));
- });
- };
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }), pow = Math.pow, floor = Math.floor,
- sqrt = Math.sqrt, abs = Math.abs, round = Math.round, sin = Math.sin,
- ceil = Math.ceil,
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Resize image filter class
- * @class fabric.Image.filters.Resize
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Resize();
- * object.filters.push(filter);
- * object.applyFilters(canvas.renderAll.bind(canvas));
- */
- filters.Resize = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Resize.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Resize',
-
- /**
- * Resize type
- * @param {String} resizeType
- * @default
- */
- resizeType: 'hermite',
-
- /**
- * Scale factor for resizing, x axis
- * @param {Number} scaleX
- * @default
- */
- scaleX: 0,
-
- /**
- * Scale factor for resizing, y axis
- * @param {Number} scaleY
- * @default
- */
- scaleY: 0,
-
- /**
- * LanczosLobes parameter for lanczos filter
- * @param {Number} lanczosLobes
- * @default
- */
- lanczosLobes: 3,
-
-
- /**
- * 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
- * @memberOf fabric.Image.filters.Resize.prototype
- * @param {Object} canvasEl Canvas element to apply filter to
- * @param {Number} scaleX
- * @param {Number} scaleY
- */
- applyTo2d: function(options) {
- var imageData = options.imageData,
- scaleX = this.scaleX,
- scaleY = this.scaleY;
-
- this.rcpScaleX = 1 / scaleX;
- this.rcpScaleY = 1 / scaleY;
-
- var oW = imageData.width, oH = imageData.height,
- dW = round(oW * scaleX), dH = round(oH * scaleY),
- newData;
-
- if (this.resizeType === 'sliceHack') {
- newData = this.sliceByTwo(options, oW, oH, dW, dH);
- }
- else if (this.resizeType === 'hermite') {
- newData = this.hermiteFastResize(options, oW, oH, dW, dH);
- }
- else if (this.resizeType === 'bilinear') {
- newData = this.bilinearFiltering(options, oW, oH, dW, dH);
- }
- else if (this.resizeType === 'lanczos') {
- newData = this.lanczosResize(options, oW, oH, dW, dH);
- }
- options.imageData = newData;
- },
-
- /**
- * Filter sliceByTwo
- * @param {Object} canvasEl Canvas element to apply filter to
- * @param {Number} oW Original Width
- * @param {Number} oH Original Height
- * @param {Number} dW Destination Width
- * @param {Number} dH Destination Height
- * @returns {ImageData}
- */
- sliceByTwo: function(options, oW, oH, dW, dH) {
- var imageData = options.imageData,
- mult = 0.5, doneW = false, doneH = false, stepW = oW * mult,
- stepH = oH * mult, resources = fabric.filterBackend.resources,
- tmpCanvas, ctx, sX = 0, sY = 0, dX = oW, dY = 0;
- if (!resources.sliceByTwo) {
- resources.sliceByTwo = document.createElement('canvas');
- }
- tmpCanvas = resources.sliceByTwo;
- if (tmpCanvas.width < oW * 1.5 || tmpCanvas.height < oH) {
- tmpCanvas.width = oW * 1.5;
- tmpCanvas.height = oH;
- }
- ctx = tmpCanvas.getContext('2d');
- ctx.clearRect(0, 0, oW * 1.5, oH);
- ctx.putImageData(imageData, 0, 0);
-
- dW = floor(dW);
- dH = floor(dH);
-
- while (!doneW || !doneH) {
- oW = stepW;
- oH = stepH;
- if (dW < floor(stepW * mult)) {
- stepW = floor(stepW * mult);
- }
- else {
- stepW = dW;
- doneW = true;
- }
- if (dH < floor(stepH * mult)) {
- stepH = floor(stepH * mult);
- }
- else {
- stepH = dH;
- doneH = true;
- }
- ctx.drawImage(tmpCanvas, sX, sY, oW, oH, dX, dY, stepW, stepH);
- sX = dX;
- sY = dY;
- dY += stepH;
- }
- return ctx.getImageData(sX, sY, dW, dH);
- },
-
- /**
- * Filter lanczosResize
- * @param {Object} canvasEl Canvas element to apply filter to
- * @param {Number} oW Original Width
- * @param {Number} oH Original Height
- * @param {Number} dW Destination Width
- * @param {Number} dH Destination Height
- * @returns {ImageData}
- */
- lanczosResize: function(options, oW, oH, dW, dH) {
-
- function process(u) {
- var v, i, weight, idx, a, red, green,
- blue, alpha, fX, fY;
- center.x = (u + 0.5) * ratioX;
- icenter.x = floor(center.x);
- for (v = 0; v < dH; v++) {
- center.y = (v + 0.5) * ratioY;
- icenter.y = floor(center.y);
- a = 0; red = 0; green = 0; blue = 0; alpha = 0;
- for (i = icenter.x - range2X; i <= icenter.x + range2X; i++) {
- if (i < 0 || i >= oW) {
- continue;
- }
- fX = floor(1000 * abs(i - center.x));
- if (!cacheLanc[fX]) {
- cacheLanc[fX] = { };
- }
- for (var j = icenter.y - range2Y; j <= icenter.y + range2Y; j++) {
- if (j < 0 || j >= oH) {
- continue;
- }
- fY = floor(1000 * abs(j - center.y));
- if (!cacheLanc[fX][fY]) {
- cacheLanc[fX][fY] = lanczos(sqrt(pow(fX * rcpRatioX, 2) + pow(fY * rcpRatioY, 2)) / 1000);
- }
- weight = cacheLanc[fX][fY];
- if (weight > 0) {
- idx = (j * oW + i) * 4;
- a += weight;
- red += weight * srcData[idx];
- green += weight * srcData[idx + 1];
- blue += weight * srcData[idx + 2];
- alpha += weight * srcData[idx + 3];
- }
- }
- }
- idx = (v * dW + u) * 4;
- destData[idx] = red / a;
- destData[idx + 1] = green / a;
- destData[idx + 2] = blue / a;
- destData[idx + 3] = alpha / a;
- }
-
- if (++u < dW) {
- return process(u);
- }
- else {
- return destImg;
- }
- }
-
- var srcData = options.imageData.data,
- destImg = options.ctx.createImageData(dW, dH),
- destData = destImg.data,
- 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),
- range2Y = ceil(ratioY * this.lanczosLobes / 2),
- cacheLanc = { }, center = { }, icenter = { };
-
- return process(0);
- },
-
- /**
- * bilinearFiltering
- * @param {Object} canvasEl Canvas element to apply filter to
- * @param {Number} oW Original Width
- * @param {Number} oH Original Height
- * @param {Number} dW Destination Width
- * @param {Number} dH Destination Height
- * @returns {ImageData}
- */
- bilinearFiltering: function(options, oW, oH, dW, dH) {
- var a, b, c, d, x, y, i, j, xDiff, yDiff, chnl,
- color, offset = 0, origPix, ratioX = this.rcpScaleX,
- ratioY = this.rcpScaleY,
- w4 = 4 * (oW - 1), img = options.imageData,
- pixels = img.data, destImage = options.ctx.createImageData(dW, dH),
- destPixels = destImage.data;
- for (i = 0; i < dH; i++) {
- for (j = 0; j < dW; j++) {
- x = floor(ratioX * j);
- y = floor(ratioY * i);
- xDiff = ratioX * j - x;
- yDiff = ratioY * i - y;
- origPix = 4 * (y * oW + x);
-
- for (chnl = 0; chnl < 4; chnl++) {
- a = pixels[origPix + chnl];
- b = pixels[origPix + 4 + chnl];
- c = pixels[origPix + w4 + chnl];
- d = pixels[origPix + w4 + 4 + chnl];
- color = a * (1 - xDiff) * (1 - yDiff) + b * xDiff * (1 - yDiff) +
- c * yDiff * (1 - xDiff) + d * xDiff * yDiff;
- destPixels[offset++] = color;
- }
- }
- }
- return destImage;
- },
-
- /**
- * hermiteFastResize
- * @param {Object} canvasEl Canvas element to apply filter to
- * @param {Number} oW Original Width
- * @param {Number} oH Original Height
- * @param {Number} dW Destination Width
- * @param {Number} dH Destination Height
- * @returns {ImageData}
- */
- hermiteFastResize: function(options, oW, oH, dW, dH) {
- var ratioW = this.rcpScaleX, ratioH = this.rcpScaleY,
- ratioWHalf = ceil(ratioW / 2),
- ratioHHalf = ceil(ratioH / 2),
- img = options.imageData, data = img.data,
- img2 = options.ctx.createImageData(dW, dH), data2 = img2.data;
- for (var j = 0; j < dH; j++) {
- for (var i = 0; i < dW; i++) {
- var x2 = (i + j * dW) * 4, weight = 0, weights = 0, weightsAlpha = 0,
- gxR = 0, gxG = 0, gxB = 0, gxA = 0, centerY = (j + 0.5) * ratioH;
- for (var yy = floor(j * ratioH); yy < (j + 1) * ratioH; yy++) {
- var dy = abs(centerY - (yy + 0.5)) / ratioHHalf,
- centerX = (i + 0.5) * ratioW, w0 = dy * dy;
- for (var xx = floor(i * ratioW); xx < (i + 1) * ratioW; xx++) {
- var dx = abs(centerX - (xx + 0.5)) / ratioWHalf,
- w = sqrt(w0 + dx * dx);
- /* eslint-disable max-depth */
- if (w > 1 && w < -1) {
- continue;
- }
- //hermite filter
- weight = 2 * w * w * w - 3 * w * w + 1;
- if (weight > 0) {
- dx = 4 * (xx + yy * oW);
- //alpha
- gxA += weight * data[dx + 3];
- weightsAlpha += weight;
- //colors
- if (data[dx + 3] < 255) {
- weight = weight * data[dx + 3] / 250;
- }
- gxR += weight * data[dx];
- gxG += weight * data[dx + 1];
- gxB += weight * data[dx + 2];
- weights += weight;
- }
- /* eslint-enable max-depth */
- }
- }
- data2[x2] = gxR / weights;
- data2[x2 + 1] = gxG / weights;
- data2[x2 + 2] = gxB / weights;
- data2[x2 + 3] = gxA / weightsAlpha;
- }
- }
- return img2;
- },
-
- /**
- * Returns object representation of an instance
- * @return {Object} Object representation of an instance
- */
- toObject: function() {
- return {
- type: this.type,
- scaleX: this.scaleX,
- scaleY: this.scaleY,
- resizeType: this.resizeType,
- lanczosLobes: this.lanczosLobes
- };
- }
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize
- */
- fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Contrast filter class
- * @class fabric.Image.filters.Contrast
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.Contrast#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Contrast({
- * contrast: 40
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- */
- filters.Contrast = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Contrast.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Contrast',
-
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uContrast;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'float contrastF = 1.015 * (uContrast + 1.0) / (1.0 * (1.015 - uContrast));\n' +
- 'color.rgb = contrastF * (color.rgb - 0.5) + 0.5;\n' +
- 'gl_FragColor = color;\n' +
- '}',
-
- contrast: 0,
-
- mainParameter: 'contrast',
-
- /**
- * Constructor
- * @memberOf fabric.Image.filters.Contrast.prototype
- * @param {Object} [options] Options object
- * @param {Number} [options.contrast=0] Value to contrast the image up (-1...1)
- */
-
- /**
- * Apply the Contrast operation to a Uint8Array representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8Array to be filtered.
- */
- applyTo2d: function(options) {
- if (this.contrast === 0) {
- return;
- }
- var imageData = options.imageData, i, len,
- data = imageData.data, len = data.length,
- contrast = Math.floor(this.contrast * 255),
- contrastF = 259 * (contrast + 255) / (255 * (259 - contrast));
-
- for (i = 0; i < len; i += 4) {
- data[i] = contrastF * (data[i] - 128) + 128;
- data[i + 1] = contrastF * (data[i + 1] - 128) + 128;
- data[i + 2] = contrastF * (data[i + 2] - 128) + 128;
- }
- },
-
- /**
- * 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 {
- uContrast: gl.getUniformLocation(program, 'uContrast'),
- };
- },
-
- /**
- * 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.uniform1f(uniformLocations.uContrast, this.contrast);
- },
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast
- */
- fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Saturate filter class
- * @class fabric.Image.filters.Saturation
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.Saturation#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Saturation({
- * saturation: 100
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- */
- filters.Saturation = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Saturation.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Saturation',
-
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform float uSaturation;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'float rgMax = max(color.r, color.g);\n' +
- 'float rgbMax = max(rgMax, color.b);\n' +
- 'color.r += rgbMax != color.r ? (rgbMax - color.r) * uSaturation : 0.00;\n' +
- 'color.g += rgbMax != color.g ? (rgbMax - color.g) * uSaturation : 0.00;\n' +
- 'color.b += rgbMax != color.b ? (rgbMax - color.b) * uSaturation : 0.00;\n' +
- 'gl_FragColor = color;\n' +
- '}',
-
- saturation: 0,
-
- mainParameter: 'saturation',
-
- /**
- * Constructor
- * @memberOf fabric.Image.filters.Saturate.prototype
- * @param {Object} [options] Options object
- * @param {Number} [options.saturate=0] Value to saturate the image (-1...1)
- */
-
- /**
- * Apply the Saturation operation to a Uint8ClampedArray representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered.
- */
- applyTo2d: function(options) {
- if (this.saturation === 0) {
- return;
- }
- var imageData = options.imageData,
- data = imageData.data, len = data.length,
- adjust = -this.saturation, i, max;
-
- for (i = 0; i < len; i += 4) {
- max = Math.max(data[i], data[i + 1], data[i + 2]);
- data[i] += max !== data[i] ? (max - data[i]) * adjust : 0;
- data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * adjust : 0;
- data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * adjust : 0;
- }
- },
-
- /**
- * 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 {
- uSaturation: gl.getUniformLocation(program, 'uSaturation'),
- };
- },
-
- /**
- * 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.uniform1f(uniformLocations.uSaturation, -this.saturation);
- },
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate
- */
- fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Blur filter class
- * @class fabric.Image.filters.Blur
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.Blur#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Blur({
- * blur: 0.5
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- * canvas.renderAll();
- */
- filters.Blur = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blur.prototype */ {
-
- type: 'Blur',
-
- /*
-'gl_FragColor = vec4(0.0);',
-'gl_FragColor += texture2D(texture, vTexCoord + -7 * uDelta)*0.0044299121055113265;',
-'gl_FragColor += texture2D(texture, vTexCoord + -6 * uDelta)*0.00895781211794;',
-'gl_FragColor += texture2D(texture, vTexCoord + -5 * uDelta)*0.0215963866053;',
-'gl_FragColor += texture2D(texture, vTexCoord + -4 * uDelta)*0.0443683338718;',
-'gl_FragColor += texture2D(texture, vTexCoord + -3 * uDelta)*0.0776744219933;',
-'gl_FragColor += texture2D(texture, vTexCoord + -2 * uDelta)*0.115876621105;',
-'gl_FragColor += texture2D(texture, vTexCoord + -1 * uDelta)*0.147308056121;',
-'gl_FragColor += texture2D(texture, vTexCoord )*0.159576912161;',
-'gl_FragColor += texture2D(texture, vTexCoord + 1 * uDelta)*0.147308056121;',
-'gl_FragColor += texture2D(texture, vTexCoord + 2 * uDelta)*0.115876621105;',
-'gl_FragColor += texture2D(texture, vTexCoord + 3 * uDelta)*0.0776744219933;',
-'gl_FragColor += texture2D(texture, vTexCoord + 4 * uDelta)*0.0443683338718;',
-'gl_FragColor += texture2D(texture, vTexCoord + 5 * uDelta)*0.0215963866053;',
-'gl_FragColor += texture2D(texture, vTexCoord + 6 * uDelta)*0.00895781211794;',
-'gl_FragColor += texture2D(texture, vTexCoord + 7 * uDelta)*0.0044299121055113265;',
-*/
-
- /* eslint-disable max-len */
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec2 uDelta;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'const float nSamples = 15.0;\n' +
- 'vec3 v3offset = vec3(12.9898, 78.233, 151.7182);\n' +
- 'float random(vec3 scale) {\n' +
- /* use the fragment position for a different seed per-pixel */
- 'return fract(sin(dot(gl_FragCoord.xyz, scale)) * 43758.5453);\n' +
- '}\n' +
- 'void main() {\n' +
- 'vec4 color = vec4(0.0);\n' +
- 'float total = 0.0;\n' +
- 'float offset = random(v3offset);\n' +
- 'for (float t = -nSamples; t <= nSamples; t++) {\n' +
- 'float percent = (t + offset - 0.5) / nSamples;\n' +
- 'float weight = 1.0 - abs(percent);\n' +
- 'color += texture2D(uTexture, vTexCoord + uDelta * percent) * weight;\n' +
- 'total += weight;\n' +
- '}\n' +
- 'gl_FragColor = color / total;\n' +
- '}',
- /* eslint-enable max-len */
-
- /**
- * blur value, in percentage of image dimensions.
- * specific to keep the image blur constant at different resolutions
- * range bewteen 0 and 1.
- */
- blur: 0,
-
- mainParameter: 'blur',
-
- applyTo: function(options) {
- if (options.webgl) {
- // this aspectRatio is used to give the same blur to vertical and horizontal
- this.aspectRatio = options.sourceWidth / options.sourceHeight;
- options.passes++;
- this._setupFrameBuffer(options);
- this.horizontal = true;
- this.applyToWebGL(options);
- this._swapTextures(options);
- this._setupFrameBuffer(options);
- this.horizontal = false;
- this.applyToWebGL(options);
- this._swapTextures(options);
- }
- else {
- this.applyTo2d(options);
- }
- },
-
- applyTo2d: function(options) {
- // paint canvasEl with current image data.
- //options.ctx.putImageData(options.imageData, 0, 0);
- options.imageData = this.simpleBlur(options);
- },
-
- simpleBlur: function(options) {
- var resources = options.filterBackend.resources, canvas1, canvas2,
- width = options.imageData.width,
- height = options.imageData.height;
-
- if (!resources.blurLayer1) {
- resources.blurLayer1 = document.createElement('canvas');
- resources.blurLayer2 = document.createElement('canvas');
- }
- canvas1 = resources.blurLayer1;
- canvas2 = resources.blurLayer2;
- if (canvas1.width !== width || canvas1.height !== height) {
- canvas2.width = canvas1.width = width;
- canvas2.height = canvas1.height = height;
- }
- var ctx1 = canvas1.getContext('2d'),
- ctx2 = canvas2.getContext('2d'),
- nSamples = 15,
- random, percent, j, i,
- blur = this.blur * 0.06 * 0.5;
-
- // load first canvas
- ctx1.putImageData(options.imageData, 0, 0);
- ctx2.clearRect(0, 0, width, height);
-
- for (i = -nSamples; i <= nSamples; i++) {
- random = (Math.random() - 0.5) / 4;
- percent = i / nSamples;
- j = blur * percent * width + random;
- ctx2.globalAlpha = 1 - Math.abs(percent);
- ctx2.drawImage(canvas1, j, random);
- ctx1.drawImage(canvas2, 0, 0);
- ctx2.globalAlpha = 1;
- ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
- }
- for (i = -nSamples; i <= nSamples; i++) {
- random = (Math.random() - 0.5) / 4;
- percent = i / nSamples;
- j = blur * percent * height + random;
- ctx2.globalAlpha = 1 - Math.abs(percent);
- ctx2.drawImage(canvas1, random, j);
- ctx1.drawImage(canvas2, 0, 0);
- ctx2.globalAlpha = 1;
- ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
- }
- options.ctx.drawImage(canvas1, 0, 0);
- var newImageData = options.ctx.getImageData(0, 0, canvas1.width, canvas1.height);
- ctx1.globalAlpha = 1;
- ctx1.clearRect(0, 0, canvas1.width, canvas1.height);
- return newImageData;
- },
-
- /**
- * 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 {
- delta: gl.getUniformLocation(program, 'uDelta'),
- };
- },
-
- /**
- * 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) {
- var delta = this.chooseRightDelta();
- gl.uniform2fv(uniformLocations.delta, delta);
- },
-
- /**
- * choose right value of image percentage to blur with
- * @returns {Array} a numeric array with delta values
- */
- chooseRightDelta: function() {
- var blurScale = 1, delta = [0, 0], blur;
- if (this.horizontal) {
- if (this.aspectRatio > 1) {
- // image is wide, i want to shrink radius horizontal
- blurScale = 1 / this.aspectRatio;
- }
- }
- else {
- if (this.aspectRatio < 1) {
- // image is tall, i want to shrink radius vertical
- blurScale = this.aspectRatio;
- }
- }
- blur = blurScale * this.blur * 0.12;
- if (this.horizontal) {
- delta[0] = blur;
- }
- else {
- delta[1] = blur;
- }
- return delta;
- },
- });
-
- /**
- * Deserialize a JSON definition of a BlurFilter into a concrete instance.
- */
- filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * Gamma filter class
- * @class fabric.Image.filters.Gamma
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.Gamma#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.Gamma({
- * brightness: 200
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- */
- filters.Gamma = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Gamma.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'Gamma',
-
- fragmentSource: 'precision highp float;\n' +
- 'uniform sampler2D uTexture;\n' +
- 'uniform vec3 uGamma;\n' +
- 'varying vec2 vTexCoord;\n' +
- 'void main() {\n' +
- 'vec4 color = texture2D(uTexture, vTexCoord);\n' +
- 'vec3 correction = (1.0 / uGamma);\n' +
- 'color.r = pow(color.r, correction.r);\n' +
- 'color.g = pow(color.g, correction.g);\n' +
- 'color.b = pow(color.b, correction.b);\n' +
- 'gl_FragColor = color;\n' +
- 'gl_FragColor.rgb *= color.a;\n' +
- '}',
-
- /**
- * Gamma array value, from 0.01 to 2.2.
- * @param {Array} gamma
- * @default
- */
- gamma: [1, 1, 1],
-
- /**
- * Describe the property that is the filter parameter
- * @param {String} m
- * @default
- */
- mainParameter: 'gamma',
-
- /**
- * Apply the Gamma operation to a Uint8Array representing the pixels of an image.
- *
- * @param {Object} options
- * @param {ImageData} options.imageData The Uint8Array to be filtered.
- */
- applyTo2d: function(options) {
- var imageData = options.imageData, data = imageData.data,
- gamma = this.gamma, len = data.length,
- rInv = 1 / gamma[0], gInv = 1 / gamma[1],
- bInv = 1 / gamma[2], i;
-
- if (!this.rVals) {
- // eslint-disable-next-line
- this.rVals = new Uint8Array(256);
- // eslint-disable-next-line
- this.gVals = new Uint8Array(256);
- // eslint-disable-next-line
- this.bVals = new Uint8Array(256);
- }
-
- // This is an optimization - pre-compute a look-up table for each color channel
- // instead of performing these pow calls for each pixel in the image.
- for (i = 0, len = 256; i < len; i++) {
- this.rVals[i] = Math.pow(i / 255, rInv) * 255;
- this.gVals[i] = Math.pow(i / 255, gInv) * 255;
- this.bVals[i] = Math.pow(i / 255, bInv) * 255;
- }
- for (i = 0, len = data.length; i < len; i += 4) {
- data[i] = this.rVals[data[i]];
- data[i + 1] = this.gVals[data[i + 1]];
- data[i + 2] = this.bVals[data[i + 2]];
- }
- },
-
- /**
- * 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 {
- uGamma: gl.getUniformLocation(program, 'uGamma'),
- };
- },
-
- /**
- * 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.uniform3fv(uniformLocations.uGamma, this.gamma);
- },
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma
- */
- fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * A container class that knows how to apply a sequence of filters to an input image.
- */
- filters.Composed = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Composed.prototype */ {
-
- type: 'Composed',
-
- /**
- * A non sparse array of filters to apply
- */
- subFilters: [],
-
- /**
- * Constructor
- * @param {Object} [options] Options object
- */
- initialize: function(options) {
- this.callSuper('initialize', options);
- // create a new array instead mutating the prototype with push
- this.subFilters = this.subFilters.slice(0);
- },
-
- /**
- * Apply this container's filters to the input image provided.
- *
- * @param {Object} options
- * @param {Number} options.passes The number of filters remaining to be applied.
- */
- applyTo: function(options) {
- options.passes += this.subFilters.length - 1;
- this.subFilters.forEach(function(filter) {
- filter.applyTo(options);
- });
- },
-
- /**
- * Serialize this filter into JSON.
- *
- * @returns {Object} A JSON representation of this filter.
- */
- toObject: function() {
- return fabric.util.object.extend(this.callSuper('toObject'), {
- subFilters: this.subFilters.map(function(filter) { return filter.toObject(); }),
- });
- },
- });
-
- /**
- * Deserialize a JSON definition of a ComposedFilter into a concrete instance.
- */
- fabric.Image.filters.Composed.fromObject = function(object, callback) {
- var filters = object.subFilters || [],
- subFilters = filters.map(function(filter) {
- return new fabric.Image.filters[filter.type](filter);
- }),
- instance = new fabric.Image.filters.Composed({ subFilters: subFilters });
- callback && callback(instance);
- return instance;
- };
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- filters = fabric.Image.filters,
- createClass = fabric.util.createClass;
-
- /**
- * HueRotation filter class
- * @class fabric.Image.filters.HueRotation
- * @memberOf fabric.Image.filters
- * @extends fabric.Image.filters.BaseFilter
- * @see {@link fabric.Image.filters.HueRotation#initialize} for constructor definition
- * @see {@link http://fabricjs.com/image-filters|ImageFilters demo}
- * @example
- * var filter = new fabric.Image.filters.HueRotation({
- * rotation: -0.5
- * });
- * object.filters.push(filter);
- * object.applyFilters();
- */
- filters.HueRotation = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.HueRotation.prototype */ {
-
- /**
- * Filter type
- * @param {String} type
- * @default
- */
- type: 'HueRotation',
-
- /**
- * HueRotation value, from -1 to 1.
- * the unit is radians
- * @param {Number} myParameter
- * @default
- */
- rotation: 0,
-
- /**
- * Describe the property that is the filter parameter
- * @param {String} m
- * @default
- */
- mainParameter: 'rotation',
-
- calculateMatrix: function() {
- var rad = this.rotation * Math.PI, cos = fabric.util.cos(rad), sin = fabric.util.sin(rad),
- aThird = 1 / 3, aThirdSqtSin = Math.sqrt(aThird) * sin, OneMinusCos = 1 - cos;
- this.matrix = [
- 1, 0, 0, 0, 0,
- 0, 1, 0, 0, 0,
- 0, 0, 1, 0, 0,
- 0, 0, 0, 1, 0
- ];
- this.matrix[0] = cos + OneMinusCos / 3;
- this.matrix[1] = aThird * OneMinusCos - aThirdSqtSin;
- this.matrix[2] = aThird * OneMinusCos + aThirdSqtSin;
- this.matrix[5] = aThird * OneMinusCos + aThirdSqtSin;
- this.matrix[6] = cos + aThird * OneMinusCos;
- this.matrix[7] = aThird * OneMinusCos - aThirdSqtSin;
- this.matrix[10] = aThird * OneMinusCos - aThirdSqtSin;
- this.matrix[11] = aThird * OneMinusCos + aThirdSqtSin;
- this.matrix[12] = cos + aThird * OneMinusCos;
- },
-
- /**
- * 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) {
- this.calculateMatrix();
- fabric.Image.filters.BaseFilter.prototype.applyTo.call(this, options);
- },
-
- });
-
- /**
- * Returns filter instance from an object representation
- * @static
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] to be invoked after filter creation
- * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation
- */
- fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject;
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = { }),
- clone = fabric.util.object.clone,
- MIN_TEXT_WIDTH = 2,
- CACHE_FONT_SIZE = 200;
-
- if (fabric.Text) {
- fabric.warn('fabric.Text is already defined');
- return;
- }
-
- /**
- * Text class
- * @class fabric.Text
- * @extends fabric.Object
- * @return {fabric.Text} thisArg
- * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text}
- * @see {@link fabric.Text#initialize} for constructor definition
- */
- fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ {
-
- /**
- * Properties which when set cause object to change dimensions
- * @type Object
- * @private
- */
- _dimensionAffectingProps: [
- 'fontSize',
- 'fontWeight',
- 'fontFamily',
- 'fontStyle',
- 'lineHeight',
- 'text',
- 'charSpacing',
- 'textAlign',
- 'styles',
- ],
-
- /**
- * @private
- */
- _reNewline: /\r?\n/,
-
- /**
- * Use this regular expression to filter for whitespaces that is not a new line.
- * Mostly used when text is 'justify' aligned.
- * @private
- */
- _reSpacesAndTabs: /[ \t\r]/g,
-
- /**
- * Use this regular expression to filter for whitespace that is not a new line.
- * Mostly used when text is 'justify' aligned.
- * @private
- */
- _reSpaceAndTab: /[ \t\r]/,
-
- /**
- * Use this regular expression to filter consecutive groups of non spaces.
- * Mostly used when text is 'justify' aligned.
- * @private
- */
- _reWords: /\S+/g,
-
- /**
- * Type of an object
- * @type String
- * @default
- */
- type: 'text',
-
- /**
- * Font size (in pixels)
- * @type Number
- * @default
- */
- fontSize: 40,
-
- /**
- * Font weight (e.g. bold, normal, 400, 600, 800)
- * @type {(Number|String)}
- * @default
- */
- fontWeight: 'normal',
-
- /**
- * Font family
- * @type String
- * @default
- */
- fontFamily: 'Times New Roman',
-
- /**
- * Text decoration underline.
- * @type String
- * @default
- */
- underline: false,
-
- /**
- * Text decoration overline.
- * @type String
- * @default
- */
- overline: false,
-
- /**
- * Text decoration linethrough.
- * @type String
- * @default
- */
- linethrough: false,
-
- /**
- * Text alignment. Possible values: "left", "center", "right", "justify",
- * "justify-left", "justify-center" or "justify-right".
- * @type String
- * @default
- */
- textAlign: 'left',
-
- /**
- * Font style . Possible values: "", "normal", "italic" or "oblique".
- * @type String
- * @default
- */
- fontStyle: 'normal',
-
- /**
- * Line height
- * @type Number
- * @default
- */
- lineHeight: 1.16,
-
- /**
- * Background color of text lines
- * @type String
- * @default
- */
- textBackgroundColor: '',
-
- /**
- * List of properties to consider when checking if
- * state of an object is changed ({@link fabric.Object#hasStateChanged})
- * as well as for history (undo/redo) purposes
- * @type Array
- */
- stateProperties: fabric.Object.prototype.stateProperties.concat('fontFamily',
- 'fontWeight',
- 'fontSize',
- 'text',
- 'underline',
- 'overline',
- 'linethrough',
- 'textAlign',
- 'fontStyle',
- 'lineHeight',
- 'textBackgroundColor',
- 'charSpacing',
- 'styles'),
-
- /**
- * List of properties to consider when checking if cache needs refresh
- * @type Array
- */
- cacheProperties: fabric.Object.prototype.cacheProperties.concat('fontFamily',
- 'fontWeight',
- 'fontSize',
- 'text',
- 'underline',
- 'overline',
- 'linethrough',
- 'textAlign',
- 'fontStyle',
- 'lineHeight',
- 'textBackgroundColor',
- 'charSpacing',
- 'styles'),
-
- /**
- * When defined, an object is rendered via stroke and this property specifies its color.
- * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6
- * @type String
- * @default
- */
- stroke: null,
-
- /**
- * Shadow object representing shadow of this shape.
- * Backwards incompatibility note: This property was named "textShadow" (String) until v1.2.11
- * @type fabric.Shadow
- * @default
- */
- shadow: null,
-
- /**
- * @private
- */
- _fontSizeFraction: 0.222,
-
- /**
- * @private
- */
- offsets: {
- underline: 0.10,
- linethrough: -0.315,
- overline: -0.88
- },
-
- /**
- * Text Line proportion to font Size (in pixels)
- * @type Number
- * @default
- */
- _fontSizeMult: 1.13,
-
- /**
- * additional space between characters
- * expressed in thousands of em unit
- * @type Number
- * @default
- */
- charSpacing: 0,
-
- /**
- * Object containing character styles
- * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line)
- * @type Object
- * @default
- */
- styles: null,
-
- /**
- * Reference to a context to measure text char or couple of chars
- * the cacheContext of the canvas will be used or a freshly created one if the object is not on canvas
- * once created it will be referenced on fabric._measuringContext to avoide creating a canvas for every
- * text object created.
- * @type {CanvasRenderingContext2D}
- * @default
- */
- _measuringContext: null,
-
- /**
- * Array of properties that define a style unit.
- * @type {Array}
- * @default
- */
- _styleProperties: [
- 'stroke',
- 'strokeWidth',
- 'fill',
- 'fontFamily',
- 'fontSize',
- 'fontWeight',
- 'fontStyle',
- 'underline',
- 'overline',
- 'linethrough',
- 'textBackgroundColor',
- ],
-
- /**
- * contains characters bounding boxes
- */
- __charBounds: [],
-
- /**
- * Constructor
- * @param {String} text Text string
- * @param {Object} [options] Options object
- * @return {fabric.Text} thisArg
- */
- initialize: function(text, options) {
- this.styles = options ? (options.styles || { }) : { };
- this.text = text;
- this.__skipDimension = true;
- this.callSuper('initialize', options);
- this.__skipDimension = false;
- this.initDimensions();
- this.setCoords();
- this.setupState({ propertySet: '_dimensionAffectingProps' });
- },
-
- /**
- * Return a contex for measurement of text string.
- * if created it gets stored for reuse
- * @param {String} text Text string
- * @param {Object} [options] Options object
- * @return {fabric.Text} thisArg
- */
- getMeasuringContext: function() {
- // if we did not return we have to measure something.
- if (!fabric._measuringContext) {
- fabric._measuringContext = this.canvas && this.canvas.contextCache ||
- fabric.util.createCanvasElement().getContext('2d');
- }
- return fabric._measuringContext;
- },
-
- /**
- * @private
- * Divides text into lines of text and lines of graphemes.
- */
- _splitText: function() {
- var newLines = this._splitTextIntoLines(this.text);
- this.textLines = newLines.lines;
- this._textLines = newLines.graphemeLines;
- this._unwrappedTextLines = newLines._unwrappedLines;
- this._text = newLines.graphemeText;
- return newLines;
- },
-
- /**
- * Initialize or update text dimensions.
- * Updates this.width and this.height with the proper values.
- * Does not return dimensions.
- */
- initDimensions: function() {
- if (this.__skipDimension) {
- return;
- }
- this._splitText();
- this._clearCache();
- this.width = this.calcTextWidth() || this.cursorWidth || MIN_TEXT_WIDTH;
- if (this.textAlign.indexOf('justify') !== -1) {
- // once text is measured we need to make space fatter to make justified text.
- this.enlargeSpaces();
- }
- this.height = this.calcTextHeight();
- this.saveState({ propertySet: '_dimensionAffectingProps' });
- },
-
- /**
- * Enlarge space boxes and shift the others
- */
- enlargeSpaces: function() {
- var diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) {
- continue;
- }
- accumulatedSpace = 0;
- line = this._textLines[i];
- currentLineWidth = this.getLineWidth(i);
- if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
- numberOfSpaces = spaces.length;
- diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
- for (var j = 0, jlen = line.length; j <= jlen; j++) {
- charBound = this.__charBounds[i][j];
- if (this._reSpaceAndTab.test(line[j])) {
- charBound.width += diffSpace;
- charBound.kernedWidth += diffSpace;
- charBound.left += accumulatedSpace;
- accumulatedSpace += diffSpace;
- }
- else {
- charBound.left += accumulatedSpace;
- }
- }
- }
- }
- },
-
- /**
- * Detect if the text line is ended with an hard break
- * text and itext do not have wrapping, return false
- * @return {Boolean}
- */
- isEndOfWrapping: function(lineIndex) {
- return lineIndex === this._textLines.length - 1;
- },
-
- /**
- * Returns string representation of an instance
- * @return {String} String representation of text object
- */
- toString: function() {
- return '#';
- },
-
- /**
- * Return the dimension and the zoom level needed to create a cache canvas
- * big enough to host the object to be cached.
- * @private
- * @param {Object} dim.x width of object to be cached
- * @param {Object} dim.y height of object to be cached
- * @return {Object}.width width of canvas
- * @return {Object}.height height of canvas
- * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache
- * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache
- */
- _getCacheCanvasDimensions: function() {
- var dims = this.callSuper('_getCacheCanvasDimensions');
- var fontSize = this.fontSize;
- dims.width += fontSize * dims.zoomX;
- dims.height += fontSize * dims.zoomY;
- return dims;
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _render: function(ctx) {
- this._setTextStyles(ctx);
- this._renderTextLinesBackground(ctx);
- this._renderTextDecoration(ctx, 'underline');
- this._renderText(ctx);
- this._renderTextDecoration(ctx, 'overline');
- this._renderTextDecoration(ctx, 'linethrough');
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderText: function(ctx) {
- if (this.paintFirst === 'stroke') {
- this._renderTextStroke(ctx);
- this._renderTextFill(ctx);
- }
- else {
- this._renderTextFill(ctx);
- this._renderTextStroke(ctx);
- }
- },
-
- /**
- * Set the font parameter of the context with the object properties or with charStyle
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {Object} [charStyle] object with font style properties
- * @param {String} [charStyle.fontFamily] Font Family
- * @param {Number} [charStyle.fontSize] Font size in pixels. ( without px suffix )
- * @param {String} [charStyle.fontWeight] Font weight
- * @param {String} [charStyle.fontStyle] Font style (italic|normal)
- */
- _setTextStyles: function(ctx, charStyle, forMeasuring) {
- ctx.textBaseline = 'alphabetic';
- ctx.font = this._getFontDeclaration(charStyle, forMeasuring);
- },
-
- /**
- * calculate and return the text Width measuring each line.
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @return {Number} Maximum width of fabric.Text object
- */
- calcTextWidth: function() {
- var maxWidth = this.getLineWidth(0);
-
- for (var i = 1, len = this._textLines.length; i < len; i++) {
- var currentLineWidth = this.getLineWidth(i);
- if (currentLineWidth > maxWidth) {
- maxWidth = currentLineWidth;
- }
- }
- return maxWidth;
- },
-
- /**
- * @private
- * @param {String} method Method name ("fillText" or "strokeText")
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {String} line Text to render
- * @param {Number} left Left position of text
- * @param {Number} top Top position of text
- * @param {Number} lineIndex Index of a line in a text
- */
- _renderTextLine: function(method, ctx, line, left, top, lineIndex) {
- this._renderChars(method, ctx, line, left, top, lineIndex);
- },
-
- /**
- * Renders the text background for lines, taking care of style
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderTextLinesBackground: function(ctx) {
- if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) {
- return;
- }
- var lineTopOffset = 0, heightOfLine,
- lineLeftOffset, originalFill = ctx.fillStyle,
- line, lastColor,
- leftOffset = this._getLeftOffset(),
- topOffset = this._getTopOffset(),
- boxStart = 0, boxWidth = 0, charBox, currentColor;
-
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- heightOfLine = this.getHeightOfLine(i);
- if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) {
- lineTopOffset += heightOfLine;
- continue;
- }
- line = this._textLines[i];
- lineLeftOffset = this._getLineLeftOffset(i);
- boxWidth = 0;
- boxStart = 0;
- lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor');
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- charBox = this.__charBounds[i][j];
- currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
- if (currentColor !== lastColor) {
- ctx.fillStyle = lastColor;
- lastColor && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- topOffset + lineTopOffset,
- boxWidth,
- heightOfLine / this.lineHeight
- );
- boxStart = charBox.left;
- boxWidth = charBox.width;
- lastColor = currentColor;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- }
- if (currentColor) {
- ctx.fillStyle = currentColor;
- ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- topOffset + lineTopOffset,
- boxWidth,
- heightOfLine / this.lineHeight
- );
- }
- lineTopOffset += heightOfLine;
- }
- ctx.fillStyle = originalFill;
- // if there is text background color no
- // other shadows should be casted
- this._removeShadow(ctx);
- },
-
- /**
- * @private
- * @param {Object} decl style declaration for cache
- * @param {String} decl.fontFamily fontFamily
- * @param {String} decl.fontStyle fontStyle
- * @param {String} decl.fontWeight fontWeight
- * @return {Object} reference to cache
- */
- getFontCache: function(decl) {
- var fontFamily = decl.fontFamily.toLowerCase();
- if (!fabric.charWidthsCache[fontFamily]) {
- fabric.charWidthsCache[fontFamily] = { };
- }
- var cache = fabric.charWidthsCache[fontFamily],
- cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase();
- if (!cache[cacheProp]) {
- cache[cacheProp] = { };
- }
- return cache[cacheProp];
- },
-
- /**
- * apply all the character style to canvas for rendering
- * @private
- * @param {String} _char
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} [decl]
- */
- _applyCharStyles: function(method, ctx, lineIndex, charIndex, styleDeclaration) {
-
- this._setFillStyles(ctx, styleDeclaration);
- this._setStrokeStyles(ctx, styleDeclaration);
-
- ctx.font = this._getFontDeclaration(styleDeclaration);
- },
-
- /**
- * measure and return the width of a single character.
- * possibly overridden to accommodate different measure logic or
- * to hook some external lib for character measurement
- * @private
- * @param {String} char to be measured
- * @param {Object} charStyle style of char to be measured
- * @param {String} [previousChar] previous char
- * @param {Object} [prevCharStyle] style of previous char
- */
- _measureChar: function(_char, charStyle, previousChar, prevCharStyle) {
- // first i try to return from cache
- var fontCache = this.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle),
- previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char,
- stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth,
- fontMultiplier = charStyle.fontSize / CACHE_FONT_SIZE, kernedWidth;
-
- if (previousChar && fontCache[previousChar]) {
- previousWidth = fontCache[previousChar];
- }
- if (fontCache[_char]) {
- kernedWidth = width = fontCache[_char];
- }
- if (stylesAreEqual && fontCache[couple]) {
- coupleWidth = fontCache[couple];
- kernedWidth = coupleWidth - previousWidth;
- }
- if (!width || !previousWidth || !coupleWidth) {
- var ctx = this.getMeasuringContext();
- // send a TRUE to specify measuring font size CACHE_FONT_SIZE
- this._setTextStyles(ctx, charStyle, true);
- }
- if (!width) {
- kernedWidth = width = ctx.measureText(_char).width;
- fontCache[_char] = width;
- }
- if (!previousWidth && stylesAreEqual && previousChar) {
- previousWidth = ctx.measureText(previousChar).width;
- fontCache[previousChar] = previousWidth;
- }
- if (stylesAreEqual && !coupleWidth) {
- // we can measure the kerning couple and subtract the width of the previous character
- coupleWidth = ctx.measureText(couple).width;
- fontCache[couple] = coupleWidth;
- kernedWidth = coupleWidth - previousWidth;
- // try to fix a MS browsers oddity
- if (kernedWidth > width) {
- var diff = kernedWidth - width;
- fontCache[_char] = kernedWidth;
- fontCache[couple] += diff;
- width = kernedWidth;
- }
- }
- return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier };
- },
-
- /**
- * return height of char in fontSize for a character at lineIndex, charIndex
- * @param {Number} l line Index
- * @param {Number} c char index
- * @return {Number} fontSize of that character
- */
- getHeightOfChar: function(l, c) {
- return this.getValueOfPropertyAt(l, c, 'fontSize');
- },
-
- /**
- * measure a text line measuring all characters.
- * @param {Number} lineIndex line number
- * @return {Number} Line width
- */
- measureLine: function(lineIndex) {
- var lineInfo = this._measureLine(lineIndex);
- if (this.charSpacing !== 0) {
- lineInfo.width -= this._getWidthOfCharSpacing();
- }
- if (lineInfo.width < 0) {
- lineInfo.width = 0;
- }
- return lineInfo;
- },
-
- /**
- * measure every grapheme of a line, populating __charBounds
- * @param {Number} lineIndex
- * @return {Object} object.width total width of characters
- * @return {Object} object.widthOfSpaces length of chars that match this._reSpacesAndTabs
- */
- _measureLine: function(lineIndex) {
- var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme,
- graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length);
-
- this.__charBounds[lineIndex] = lineBounds;
- for (i = 0; i < line.length; i++) {
- grapheme = line[i];
- graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme);
- lineBounds[i] = graphemeInfo;
- width += graphemeInfo.kernedWidth;
- prevGrapheme = grapheme;
- }
- // this latest bound box represent the last character of the line
- // to simplify cursor handling in interactive mode.
- lineBounds[i] = {
- left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0,
- width: 0,
- kernedWidth: 0,
- height: this.fontSize
- };
- return { width: width, numOfSpaces: numOfSpaces };
- },
-
- /**
- * Measure and return the info of a single grapheme.
- * needs the the info of previous graphemes already filled
- * @private
- * @param {String} grapheme to be measured
- * @param {Number} lineIndex index of the line where the char is
- * @param {Number} charIndex position in the line
- * @param {String} [previousChar] character preceding the one to be measured
- */
- _getGraphemeBox: function(grapheme, lineIndex, charIndex, previousGrapheme, skipLeft) {
- var charStyle = this.getCompleteStyleDeclaration(lineIndex, charIndex),
- prevCharStyle = previousGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : { },
- info = this._measureChar(grapheme, charStyle, previousGrapheme, prevCharStyle),
- kernedWidth = info.kernedWidth, width = info.width;
-
- if (this.charSpacing !== 0) {
- width += this._getWidthOfCharSpacing();
- kernedWidth += this._getWidthOfCharSpacing();
- }
- var box = {
- width: width,
- left: 0,
- height: charStyle.fontSize,
- kernedWidth: kernedWidth,
- };
- if (charIndex > 0 && !skipLeft) {
- var previousBox = this.__charBounds[lineIndex][charIndex - 1];
- box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width;
- }
- return box;
- },
-
- /**
- * Calculate height of chosen line
- * height of line is based mainly on fontSize
- * @private
- * @param {Number} lineIndex index of the line to calculate
- */
- getHeightOfLine: function(lineIndex) {
- if (this.__lineHeights[lineIndex]) {
- return this.__lineHeights[lineIndex];
- }
-
- var line = this._textLines[lineIndex],
- maxHeight = this.getHeightOfChar(lineIndex, 0);
-
- for (var i = 1, len = line.length; i < len; i++) {
- var currentCharHeight = this.getHeightOfChar(lineIndex, i);
- if (currentCharHeight > maxHeight) {
- maxHeight = currentCharHeight;
- }
- }
- this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult;
- return this.__lineHeights[lineIndex];
- },
-
- /**
- * calculate text box height
- * @private
- */
- calcTextHeight: function() {
- var lineHeight, height = 0;
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- lineHeight = this.getHeightOfLine(i);
- height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight);
- }
- return height;
- },
-
- /**
- * @private
- * @return {Number} Left offset
- */
- _getLeftOffset: function() {
- return -this.width / 2;
- },
-
- /**
- * @private
- * @return {Number} Top offset
- */
- _getTopOffset: function() {
- return -this.height / 2;
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {String} method Method name ("fillText" or "strokeText")
- */
- _renderTextCommon: function(ctx, method) {
- ctx.save();
- var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(),
- offsets = this._applyPatternGradientTransform(ctx, method === 'fillText' ? this.fill : this.stroke);
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- var heightOfLine = this.getHeightOfLine(i),
- maxHeight = heightOfLine / this.lineHeight,
- leftOffset = this._getLineLeftOffset(i);
- this._renderTextLine(
- method,
- ctx,
- this._textLines[i],
- left + leftOffset - offsets.offsetX,
- top + lineHeights + maxHeight - offsets.offsetY,
- i
- );
- lineHeights += heightOfLine;
- }
- ctx.restore();
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderTextFill: function(ctx) {
- if (!this.fill && !this.styleHas('fill')) {
- return;
- }
-
- this._renderTextCommon(ctx, 'fillText');
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderTextStroke: function(ctx) {
- if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) {
- return;
- }
-
- if (this.shadow && !this.shadow.affectStroke) {
- this._removeShadow(ctx);
- }
-
- ctx.save();
- this._setLineDash(ctx, this.strokeDashArray);
- ctx.beginPath();
- this._renderTextCommon(ctx, 'strokeText');
- ctx.closePath();
- ctx.restore();
- },
-
- /**
- * @private
- * @param {String} method
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {String} line Content of the line
- * @param {Number} left
- * @param {Number} top
- * @param {Number} lineIndex
- * @param {Number} charOffset
- */
- _renderChars: function(method, ctx, line, left, top, lineIndex) {
- // set proper line offset
- var lineHeight = this.getHeightOfLine(lineIndex),
- isJustify = this.textAlign.indexOf('justify') !== -1,
- actualStyle,
- nextStyle,
- charsToRender = '',
- charBox,
- boxWidth = 0,
- timeToRender,
- shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex);
-
- ctx.save();
- top -= lineHeight * this._fontSizeFraction / this.lineHeight;
- if (shortCut) {
- // render all the line in one pass without checking
- this._renderChar(method, ctx, lineIndex, 0, this.textLines[lineIndex], left, top, lineHeight);
- ctx.restore();
- return;
- }
- for (var i = 0, len = line.length - 1; i <= len; i++) {
- timeToRender = i === len || this.charSpacing;
- charsToRender += line[i];
- charBox = this.__charBounds[lineIndex][i];
- if (boxWidth === 0) {
- left += charBox.kernedWidth - charBox.width;
- boxWidth += charBox.width;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- if (isJustify && !timeToRender) {
- if (this._reSpaceAndTab.test(line[i])) {
- timeToRender = true;
- }
- }
- if (!timeToRender) {
- // if we have charSpacing, we render char by char
- actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
- nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
- timeToRender = this._hasStyleChanged(actualStyle, nextStyle);
- }
- if (timeToRender) {
- this._renderChar(method, ctx, lineIndex, i, charsToRender, left, top, lineHeight);
- charsToRender = '';
- actualStyle = nextStyle;
- left += boxWidth;
- boxWidth = 0;
- }
- }
- ctx.restore();
- },
-
- /**
- * @private
- * @param {String} method
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {String} _char
- * @param {Number} left Left coordinate
- * @param {Number} top Top coordinate
- * @param {Number} lineHeight Height of the line
- */
- _renderChar: function(method, ctx, lineIndex, charIndex, _char, left, top) {
- var decl = this._getStyleDeclaration(lineIndex, charIndex),
- fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex),
- shouldFill = method === 'fillText' && fullDecl.fill,
- shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth;
-
- if (!shouldStroke && !shouldFill) {
- return;
- }
- decl && ctx.save();
-
- this._applyCharStyles(method, ctx, lineIndex, charIndex, fullDecl);
-
- if (decl && decl.textBackgroundColor) {
- this._removeShadow(ctx);
- }
- shouldFill && ctx.fillText(_char, left, top);
- shouldStroke && ctx.strokeText(_char, left, top);
- decl && ctx.restore();
- },
-
- /**
- * @private
- * @param {Object} prevStyle
- * @param {Object} thisStyle
- */
- _hasStyleChanged: function(prevStyle, thisStyle) {
- return prevStyle.fill !== thisStyle.fill ||
- prevStyle.stroke !== thisStyle.stroke ||
- prevStyle.strokeWidth !== thisStyle.strokeWidth ||
- prevStyle.fontSize !== thisStyle.fontSize ||
- prevStyle.fontFamily !== thisStyle.fontFamily ||
- prevStyle.fontWeight !== thisStyle.fontWeight ||
- prevStyle.fontStyle !== thisStyle.fontStyle;
- },
-
- /**
- * @private
- * @param {Object} prevStyle
- * @param {Object} thisStyle
- */
- _hasStyleChangedForSvg: function(prevStyle, thisStyle) {
- return this._hasStyleChanged(prevStyle, thisStyle) ||
- prevStyle.overline !== thisStyle.overline ||
- prevStyle.underline !== thisStyle.underline ||
- prevStyle.linethrough !== thisStyle.linethrough;
- },
-
- /**
- * @private
- * @param {Number} lineIndex index text line
- * @return {Number} Line left offset
- */
- _getLineLeftOffset: function(lineIndex) {
- var lineWidth = this.getLineWidth(lineIndex);
- if (this.textAlign === 'center') {
- return (this.width - lineWidth) / 2;
- }
- if (this.textAlign === 'right') {
- return this.width - lineWidth;
- }
- if (this.textAlign === 'justify-center' && this.isEndOfWrapping(lineIndex)) {
- return (this.width - lineWidth) / 2;
- }
- if (this.textAlign === 'justify-right' && this.isEndOfWrapping(lineIndex)) {
- return this.width - lineWidth;
- }
- return 0;
- },
-
- /**
- * @private
- */
- _clearCache: function() {
- this.__lineWidths = [];
- this.__lineHeights = [];
- this.__charBounds = [];
- },
-
- /**
- * @private
- */
- _shouldClearDimensionCache: function() {
- var shouldClear = this._forceClearCache;
- shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps'));
- if (shouldClear) {
- this.dirty = true;
- this._forceClearCache = false;
- }
- return shouldClear;
- },
-
- /**
- * Measure a single line given its index. Used to calculate the initial
- * text bounding box. The values are calculated and stored in __lineWidths cache.
- * @private
- * @param {Number} lineIndex line number
- * @return {Number} Line width
- */
- getLineWidth: function(lineIndex) {
- if (this.__lineWidths[lineIndex]) {
- return this.__lineWidths[lineIndex];
- }
-
- var width, line = this._textLines[lineIndex], lineInfo;
-
- if (line === '') {
- width = 0;
- }
- else {
- lineInfo = this.measureLine(lineIndex);
- width = lineInfo.width;
- }
- this.__lineWidths[lineIndex] = width;
- return width;
- },
-
- _getWidthOfCharSpacing: function() {
- if (this.charSpacing !== 0) {
- return this.fontSize * this.charSpacing / 1000;
- }
- return 0;
- },
-
- /**
- * @private
- * @param {Number} LineIndex
- * @param {Number} charIndex
- * @param {String} property
-
- */
- getValueOfPropertyAt: function(lineIndex, charIndex, property) {
- var charStyle = this._getStyleDeclaration(lineIndex, charIndex),
- styleDecoration = charStyle && typeof charStyle[property] !== 'undefined';
- return styleDecoration ? charStyle[property] : this[property];
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderTextDecoration: function(ctx, type) {
- if (!this[type] && !this.styleHas(type)) {
- return;
- }
- var heightOfLine,
- lineLeftOffset,
- line, lastDecoration,
- leftOffset = this._getLeftOffset(),
- topOffset = this._getTopOffset(),
- boxStart, boxWidth, charBox, currentDecoration,
- maxHeight, currentFill, lastFill;
-
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- heightOfLine = this.getHeightOfLine(i);
- if (!this[type] && !this.styleHas(type, i)) {
- topOffset += heightOfLine;
- continue;
- }
- line = this._textLines[i];
- maxHeight = heightOfLine / this.lineHeight;
- lineLeftOffset = this._getLineLeftOffset(i);
- boxStart = 0;
- boxWidth = 0;
- lastDecoration = this.getValueOfPropertyAt(i, 0, type);
- lastFill = this.getValueOfPropertyAt(i, 0, 'fill');
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- charBox = this.__charBounds[i][j];
- currentDecoration = this.getValueOfPropertyAt(i, j, type);
- currentFill = this.getValueOfPropertyAt(i, j, 'fill');
- if ((currentDecoration !== lastDecoration || currentFill !== lastFill) && boxWidth > 0) {
- ctx.fillStyle = lastFill;
- lastDecoration && lastFill && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- topOffset + maxHeight * (1 - this._fontSizeFraction) + this.offsets[type] * this.fontSize,
- boxWidth,
- this.fontSize / 15);
- boxStart = charBox.left;
- boxWidth = charBox.width;
- lastDecoration = currentDecoration;
- lastFill = currentFill;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- }
- ctx.fillStyle = currentFill;
- currentDecoration && currentFill && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- topOffset + maxHeight * (1 - this._fontSizeFraction) + this.offsets[type] * this.fontSize,
- boxWidth,
- this.fontSize / 15
- );
- topOffset += heightOfLine;
- }
- // if there is text background color no
- // other shadows should be casted
- this._removeShadow(ctx);
- },
-
- /**
- * return font declaration string for canvas context
- * @param {Object} [styleObject] object
- * @returns {String} font declaration formatted for canvas context.
- */
- _getFontDeclaration: function(styleObject, forMeasuring) {
- var style = styleObject || this;
- return [
- // node-canvas needs "weight style", while browsers need "style weight"
- (fabric.isLikelyNode ? style.fontWeight : style.fontStyle),
- (fabric.isLikelyNode ? style.fontStyle : style.fontWeight),
- forMeasuring ? CACHE_FONT_SIZE + 'px' : style.fontSize + 'px',
- (fabric.isLikelyNode ? ('"' + style.fontFamily + '"') : style.fontFamily)
- ].join(' ');
- },
-
- /**
- * Renders text instance on a specified context
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- render: function(ctx) {
- // do not render if object is not visible
- if (!this.visible) {
- return;
- }
- if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) {
- return;
- }
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- }
- this.callSuper('render', ctx);
- },
-
- /**
- * Returns the text as an array of lines.
- * @param {String} text text to split
- * @returns {Array} Lines in the text
- */
- _splitTextIntoLines: function(text) {
- var lines = text.split(this._reNewline),
- newLines = new Array(lines.length),
- newLine = ['\n'],
- newText = [];
- for (var i = 0; i < lines.length; i++) {
- newLines[i] = fabric.util.string.graphemeSplit(lines[i]);
- newText = newText.concat(newLines[i], newLine);
- }
- newText.pop();
- return { _unwrappedLines: newLines, lines: lines, graphemeText: newText, graphemeLines: newLines };
- },
-
- /**
- * Returns object representation of an instance
- * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
- * @return {Object} Object representation of an instance
- */
- toObject: function(propertiesToInclude) {
- var additionalProperties = [
- 'text',
- 'fontSize',
- 'fontWeight',
- 'fontFamily',
- 'fontStyle',
- 'lineHeight',
- 'underline',
- 'overline',
- 'linethrough',
- 'textAlign',
- 'textBackgroundColor',
- 'charSpacing',
- ].concat(propertiesToInclude);
- var obj = this.callSuper('toObject', additionalProperties);
- obj.styles = clone(this.styles, true);
- return obj;
- },
-
- /**
- * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`.
- * @param {String|Object} key Property name or object (if object, iterate over the object properties)
- * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one)
- * @return {fabric.Object} thisArg
- * @chainable
- */
- set: function(key, value) {
- this.callSuper('set', key, value);
- var needsDims = false;
- if (typeof key === 'object') {
- for (var _key in key) {
- needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1;
- }
- }
- else {
- needsDims = this._dimensionAffectingProps.indexOf(key) !== -1;
- }
- if (needsDims) {
- this.initDimensions();
- this.setCoords();
- }
- return this;
- },
-
- /**
- * Returns complexity of an instance
- * @return {Number} complexity
- */
- complexity: function() {
- return 1;
- }
- });
-
- /* _FROM_SVG_START_ */
- /**
- * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement})
- * @static
- * @memberOf fabric.Text
- * @see: http://www.w3.org/TR/SVG/text.html#TextElement
- */
- fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(
- 'x y dx dy font-family font-style font-weight font-size text-decoration text-anchor'.split(' '));
-
- /**
- * Default SVG font size
- * @static
- * @memberOf fabric.Text
- */
- fabric.Text.DEFAULT_SVG_FONT_SIZE = 16;
-
- /**
- * Returns fabric.Text instance from an SVG element (not yet implemented)
- * @static
- * @memberOf fabric.Text
- * @param {SVGElement} element Element to parse
- * @param {Function} callback callback function invoked after parsing
- * @param {Object} [options] Options object
- */
- fabric.Text.fromElement = function(element, callback, options) {
- if (!element) {
- return callback(null);
- }
-
- var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES),
- parsedAnchor = parsedAttributes.textAnchor || 'left';
- options = fabric.util.object.extend((options ? clone(options) : { }), parsedAttributes);
-
- options.top = options.top || 0;
- options.left = options.left || 0;
- if (parsedAttributes.textDecoration) {
- var textDecoration = parsedAttributes.textDecoration;
- if (textDecoration.indexOf('underline') !== -1) {
- options.underline = true;
- }
- if (textDecoration.indexOf('overline') !== -1) {
- options.overline = true;
- }
- if (textDecoration.indexOf('line-through') !== -1) {
- options.linethrough = true;
- }
- delete options.textDecoration;
- }
- if ('dx' in parsedAttributes) {
- options.left += parsedAttributes.dx;
- }
- if ('dy' in parsedAttributes) {
- options.top += parsedAttributes.dy;
- }
- if (!('fontSize' in options)) {
- options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE;
- }
-
- var textContent = '';
-
- // The XML is not properly parsed in IE9 so a workaround to get
- // textContent is through firstChild.data. Another workaround would be
- // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does)
- if (!('textContent' in element)) {
- if ('firstChild' in element && element.firstChild !== null) {
- if ('data' in element.firstChild && element.firstChild.data !== null) {
- textContent = element.firstChild.data;
- }
- }
- }
- else {
- textContent = element.textContent;
- }
-
- textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' ');
-
- var text = new fabric.Text(textContent, options),
- textHeightScaleFactor = text.getScaledHeight() / text.height,
- lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height,
- scaledDiff = lineHeightDiff * textHeightScaleFactor,
- textHeight = text.getScaledHeight() + scaledDiff,
- offX = 0;
- /*
- Adjust positioning:
- x/y attributes in SVG correspond to the bottom-left corner of text bounding box
- fabric output by default at top, left.
- */
- if (parsedAnchor === 'center') {
- offX = text.getScaledWidth() / 2;
- }
- if (parsedAnchor === 'right') {
- offX = text.getScaledWidth();
- }
- text.set({
- left: text.left - offX,
- top: text.top - (textHeight - text.fontSize * (0.18 + text._fontSizeFraction)) / text.lineHeight
- });
- callback(text);
- };
- /* _FROM_SVG_END_ */
-
- /**
- * Returns fabric.Text instance from an object representation
- * @static
- * @memberOf fabric.Text
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created
- */
- fabric.Text.fromObject = function(object, callback) {
- return fabric.Object._fromObject('Text', object, callback, 'text');
- };
-
- fabric.util.createAccessors && fabric.util.createAccessors(fabric.Text);
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function() {
- fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ {
- /**
- * Returns true if object has no styling or no styling in a line
- * @param {Number} lineIndex
- * @return {Boolean}
- */
- isEmptyStyles: function(lineIndex) {
- if (!this.styles) {
- return true;
- }
- if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) {
- return true;
- }
- var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] };
- for (var p1 in obj) {
- for (var p2 in obj[p1]) {
- // eslint-disable-next-line no-unused-vars
- for (var p3 in obj[p1][p2]) {
- return false;
- }
- }
- }
- return true;
- },
-
- /**
- * Returns true if object has a style property or has it ina specified line
- * @param {Number} lineIndex
- * @return {Boolean}
- */
- styleHas: function(property, lineIndex) {
- if (!this.styles || !property || property === '') {
- return false;
- }
- if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) {
- return false;
- }
- var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] };
- // eslint-disable-next-line
- for (var p1 in obj) {
- // eslint-disable-next-line
- for (var p2 in obj[p1]) {
- if (typeof obj[p1][p2][property] !== 'undefined') {
- return true;
- }
- }
- }
- return false;
- },
-
- /**
- * Check if characters in a text have a value for a property
- * whose value matches the textbox's value for that property. If so,
- * the character-level property is deleted. If the character
- * has no other properties, then it is also deleted. Finally,
- * if the line containing that character has no other characters
- * then it also is deleted.
- *
- * @param {string} property The property to compare between characters and text.
- */
- cleanStyle: function(property) {
- if (!this.styles || !property || property === '') {
- return false;
- }
- var obj = this.styles, stylesCount = 0, letterCount, foundStyle = false, style,
- canBeSwapped = true, graphemeCount = 0;
- // eslint-disable-next-line
- for (var p1 in obj) {
- letterCount = 0;
- // eslint-disable-next-line
- for (var p2 in obj[p1]) {
- stylesCount++;
- if (!foundStyle) {
- style = obj[p1][p2][property];
- foundStyle = true;
- }
- else if (obj[p1][p2][property] !== style || !obj[p1][p2].hasOwnProperty(property)) {
- canBeSwapped = false;
- }
- if (obj[p1][p2][property] === this[property]) {
- delete obj[p1][p2][property];
- }
- if (Object.keys(obj[p1][p2]).length !== 0) {
- letterCount++;
- }
- else {
- delete obj[p1][p2];
- }
- }
- if (letterCount === 0) {
- delete obj[p1];
- }
- }
- // if every grapheme has the same style set then
- // delete those styles and set it on the parent
- for (var i = 0; i < this._textLines.length; i++) {
- graphemeCount += this._textLines[i].length;
- }
- if (canBeSwapped && stylesCount === graphemeCount) {
- this[property] = style;
- this.removeStyle(property);
- }
- },
-
- /**
- * Remove a style property or properties from all individual character styles
- * in a text object. Deletes the character style object if it contains no other style
- * props. Deletes a line style object if it contains no other character styles.
- *
- * @param {String} props The property to remove from character styles.
- */
- removeStyle: function(property) {
- if (!this.styles || !property || property === '') {
- return;
- }
- var obj = this.styles, line, lineNum, charNum;
- for (lineNum in obj) {
- line = obj[lineNum];
- for (charNum in line) {
- delete line[charNum][property];
- if (Object.keys(line[charNum]).length === 0) {
- delete line[charNum];
- }
- }
- if (Object.keys(line).length === 0) {
- delete obj[lineNum];
- }
- }
- },
-
- /**
- * @private
- */
- _extendStyles: function(index, styles) {
- var loc = this.get2DCursorLocation(index);
-
- if (!this._getLineStyle(loc.lineIndex)) {
- this._setLineStyle(loc.lineIndex, {});
- }
-
- if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) {
- this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {});
- }
-
- fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles);
- },
-
- /**
- * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start)
- * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.
- * @param {Boolean} [skipWrapping] consider the location for unwrapped lines. usefull to manage styles.
- */
- get2DCursorLocation: function(selectionStart, skipWrapping) {
- if (typeof selectionStart === 'undefined') {
- selectionStart = this.selectionStart;
- }
- var lines = skipWrapping ? this._unwrappedTextLines : this._textLines;
- var len = lines.length;
- for (var i = 0; i < len; i++) {
- if (selectionStart <= lines[i].length) {
- return {
- lineIndex: i,
- charIndex: selectionStart
- };
- }
- selectionStart -= lines[i].length + 1;
- }
- return {
- lineIndex: i - 1,
- charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart
- };
- },
-
- /**
- * Gets style of a current selection/cursor (at the start position)
- * if startIndex or endIndex are not provided, slectionStart or selectionEnd will be used.
- * @param {Number} [startIndex] Start index to get styles at
- * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1
- * @param {Boolean} [complete] get full style or not
- * @return {Array} styles an array with one, zero or more Style objects
- */
- getSelectionStyles: function(startIndex, endIndex, complete) {
- if (typeof startIndex === 'undefined') {
- startIndex = this.selectionStart || 0;
- }
- if (typeof endIndex === 'undefined') {
- endIndex = this.selectionEnd || startIndex;
- }
- var styles = [];
- for (var i = startIndex; i < endIndex; i++) {
- styles.push(this.getStyleAtPosition(i, complete));
- }
- return styles;
- },
-
- /**
- * Gets style of a current selection/cursor position
- * @param {Number} position to get styles at
- * @param {Boolean} [complete] full style if true
- * @return {Object} style Style object at a specified index
- * @private
- */
- getStyleAtPosition: function(position, complete) {
- var loc = this.get2DCursorLocation(position),
- style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) :
- this._getStyleDeclaration(loc.lineIndex, loc.charIndex);
- return style || {};
- },
-
- /**
- * Sets style of a current selection, if no selection exist, do not set anything.
- * @param {Object} [styles] Styles object
- * @param {Number} [startIndex] Start index to get styles at
- * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1
- * @return {fabric.IText} thisArg
- * @chainable
- */
- setSelectionStyles: function(styles, startIndex, endIndex) {
- if (typeof startIndex === 'undefined') {
- startIndex = this.selectionStart || 0;
- }
- if (typeof endIndex === 'undefined') {
- endIndex = this.selectionEnd || startIndex;
- }
- for (var i = startIndex; i < endIndex; i++) {
- this._extendStyles(i, styles);
- }
- /* not included in _extendStyles to avoid clearing cache more than once */
- this._forceClearCache = true;
- return this;
- },
-
- /**
- * get the reference, not a clone, of the style object for a given character
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @return {Object} style object
- */
- _getStyleDeclaration: function(lineIndex, charIndex) {
- var lineStyle = this.styles && this.styles[lineIndex];
- if (!lineStyle) {
- return null;
- }
- return lineStyle[charIndex];
- },
-
- /**
- * return a new object that contains all the style property for a character
- * the object returned is newly created
- * @param {Number} lineIndex of the line where the character is
- * @param {Number} charIndex position of the character on the line
- * @return {Object} style object
- */
- getCompleteStyleDeclaration: function(lineIndex, charIndex) {
- var style = this._getStyleDeclaration(lineIndex, charIndex) || { },
- styleObject = { }, prop;
- for (var i = 0; i < this._styleProperties.length; i++) {
- prop = this._styleProperties[i];
- styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop];
- }
- return styleObject;
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} style
- * @private
- */
- _setStyleDeclaration: function(lineIndex, charIndex, style) {
- this.styles[lineIndex][charIndex] = style;
- },
-
- /**
- *
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @private
- */
- _deleteStyleDeclaration: function(lineIndex, charIndex) {
- delete this.styles[lineIndex][charIndex];
- },
-
- /**
- * @param {Number} lineIndex
- * @private
- */
- _getLineStyle: function(lineIndex) {
- return this.styles[lineIndex];
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Object} style
- * @private
- */
- _setLineStyle: function(lineIndex, style) {
- this.styles[lineIndex] = style;
- },
-
- /**
- * @param {Number} lineIndex
- * @private
- */
- _deleteLineStyle: function(lineIndex) {
- delete this.styles[lineIndex];
- }
- });
-})();
-
-
-(function() {
-
- function parseDecoration(object) {
- if (object.textDecoration) {
- object.textDecoration.indexOf('underline') > -1 && (object.underline = true);
- object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true);
- object.textDecoration.indexOf('overline') > -1 && (object.overline = true);
- delete object.textDecoration;
- }
- }
-
- /**
- * IText class (introduced in v1.4) Events are also fired with "text:"
- * prefix when observing canvas.
- * @class fabric.IText
- * @extends fabric.Text
- * @mixes fabric.Observable
- *
- * @fires changed
- * @fires selection:changed
- * @fires editing:entered
- * @fires editing:exited
- *
- * @return {fabric.IText} thisArg
- * @see {@link fabric.IText#initialize} for constructor definition
- *
- * Supported key combinations:
- *
- * Move cursor: left, right, up, down
- * Select character: shift + left, shift + right
- * Select text vertically: shift + up, shift + down
- * Move cursor by word: alt + left, alt + right
- * Select words: shift + alt + left, shift + alt + right
- * Move cursor to line start/end: cmd + left, cmd + right or home, end
- * Select till start/end of line: cmd + shift + left, cmd + shift + right or shift + home, shift + end
- * Jump to start/end of text: cmd + up, cmd + down
- * Select till start/end of text: cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
- * Delete character: backspace
- * Delete word: alt + backspace
- * Delete line: cmd + backspace
- * Forward delete: delete
- * Copy text: ctrl/cmd + c
- * Paste text: ctrl/cmd + v
- * Cut text: ctrl/cmd + x
- * Select entire text: ctrl/cmd + a
- * Quit editing tab or esc
- *
- *
- * Supported mouse/touch combination
- *
- * Position cursor: click/touch
- * Create selection: click/touch & drag
- * Create selection: click & shift + click
- * Select word: double click
- * Select line: triple click
- *
- */
- fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ {
-
- /**
- * Type of an object
- * @type String
- * @default
- */
- type: 'i-text',
-
- /**
- * Index where text selection starts (or where cursor is when there is no selection)
- * @type Number
- * @default
- */
- selectionStart: 0,
-
- /**
- * Index where text selection ends
- * @type Number
- * @default
- */
- selectionEnd: 0,
-
- /**
- * Color of text selection
- * @type String
- * @default
- */
- selectionColor: 'rgba(17,119,255,0.3)',
-
- /**
- * Indicates whether text is in editing mode
- * @type Boolean
- * @default
- */
- isEditing: false,
-
- /**
- * Indicates whether a text can be edited
- * @type Boolean
- * @default
- */
- editable: true,
-
- /**
- * Border color of text object while it's in editing mode
- * @type String
- * @default
- */
- editingBorderColor: 'rgba(102,153,255,0.25)',
-
- /**
- * Width of cursor (in px)
- * @type Number
- * @default
- */
- cursorWidth: 2,
-
- /**
- * Color of default cursor (when not overwritten by character style)
- * @type String
- * @default
- */
- cursorColor: '#333',
-
- /**
- * Delay between cursor blink (in ms)
- * @type Number
- * @default
- */
- cursorDelay: 1000,
-
- /**
- * Duration of cursor fadein (in ms)
- * @type Number
- * @default
- */
- cursorDuration: 600,
-
- /**
- * Indicates whether internal text char widths can be cached
- * @type Boolean
- * @default
- */
- caching: true,
-
- /**
- * @private
- */
- _reSpace: /\s|\n/,
-
- /**
- * @private
- */
- _currentCursorOpacity: 0,
-
- /**
- * @private
- */
- _selectionDirection: null,
-
- /**
- * @private
- */
- _abortCursorAnimation: false,
-
- /**
- * @private
- */
- __widthOfSpace: [],
-
- /**
- * Helps determining when the text is in composition, so that the cursor
- * rendering is altered.
- */
- inCompositionMode: false,
-
- /**
- * Constructor
- * @param {String} text Text string
- * @param {Object} [options] Options object
- * @return {fabric.IText} thisArg
- */
- initialize: function(text, options) {
- this.callSuper('initialize', text, options);
- this.initBehavior();
- },
-
- /**
- * Sets selection start (left boundary of a selection)
- * @param {Number} index Index to set selection start to
- */
- setSelectionStart: function(index) {
- index = Math.max(index, 0);
- this._updateAndFire('selectionStart', index);
- },
-
- /**
- * Sets selection end (right boundary of a selection)
- * @param {Number} index Index to set selection end to
- */
- setSelectionEnd: function(index) {
- index = Math.min(index, this.text.length);
- this._updateAndFire('selectionEnd', index);
- },
-
- /**
- * @private
- * @param {String} property 'selectionStart' or 'selectionEnd'
- * @param {Number} index new position of property
- */
- _updateAndFire: function(property, index) {
- if (this[property] !== index) {
- this._fireSelectionChanged();
- this[property] = index;
- }
- this._updateTextarea();
- },
-
- /**
- * Fires the even of selection changed
- * @private
- */
- _fireSelectionChanged: function() {
- this.fire('selection:changed');
- this.canvas && this.canvas.fire('text:selection:changed', { target: this });
- },
-
- /**
- * Initialize text dimensions. Render all text on given context
- * or on a offscreen canvas to get the text width with measureText.
- * Updates this.width and this.height with the proper values.
- * Does not return dimensions.
- * @private
- */
- initDimensions: function() {
- this.isEditing && this.initDelayedCursor();
- this.clearContextTop();
- this.callSuper('initDimensions');
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- render: function(ctx) {
- this.clearContextTop();
- this.callSuper('render', ctx);
- // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor
- // the correct position but not at every cursor animation.
- this.cursorOffsetCache = { };
- this.renderCursorOrSelection();
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _render: function(ctx) {
- this.callSuper('_render', ctx);
- },
-
- /**
- * Prepare and clean the contextTop
- */
- clearContextTop: function(skipRestore) {
- if (!this.isEditing) {
- return;
- }
- if (this.canvas && this.canvas.contextTop) {
- var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform;
- ctx.save();
- ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
- this.transform(ctx);
- this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix);
- this._clearTextArea(ctx);
- skipRestore || ctx.restore();
- }
- },
-
- /**
- * Renders cursor or selection (depending on what exists)
- */
- renderCursorOrSelection: function() {
- if (!this.isEditing || !this.canvas) {
- return;
- }
- var boundaries = this._getCursorBoundaries(), ctx;
- if (this.canvas && this.canvas.contextTop) {
- ctx = this.canvas.contextTop;
- this.clearContextTop(true);
- }
- else {
- ctx = this.canvas.contextContainer;
- ctx.save();
- }
- if (this.selectionStart === this.selectionEnd) {
- this.renderCursor(boundaries, ctx);
- }
- else {
- this.renderSelection(boundaries, ctx);
- }
- ctx.restore();
- },
-
- _clearTextArea: function(ctx) {
- // we add 4 pixel, to be sure to do not leave any pixel out
- var width = this.width + 4, height = this.height + 4;
- ctx.clearRect(-width / 2, -height / 2, width, height);
- },
-
- /**
- * Returns cursor boundaries (left, top, leftOffset, topOffset)
- * @private
- * @param {Array} chars Array of characters
- * @param {String} typeOfBoundaries
- */
- _getCursorBoundaries: function(position) {
-
- // left/top are left/top of entire text box
- // leftOffset/topOffset are offset from that left/top point of a text box
-
- if (typeof position === 'undefined') {
- position = this.selectionStart;
- }
-
- var left = this._getLeftOffset(),
- top = this._getTopOffset(),
- offsets = this._getCursorBoundariesOffsets(position);
-
- return {
- left: left,
- top: top,
- leftOffset: offsets.left,
- topOffset: offsets.top
- };
- },
-
- /**
- * @private
- */
- _getCursorBoundariesOffsets: function(position) {
- if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) {
- return this.cursorOffsetCache;
- }
- var lineLeftOffset,
- lineIndex = 0,
- charIndex = 0,
- topOffset = 0,
- leftOffset = 0,
- boundaries,
- cursorPosition = this.get2DCursorLocation(position);
- for (var i = 0; i < cursorPosition.lineIndex; i++) {
- topOffset += this.getHeightOfLine(i);
- }
-
- lineLeftOffset = this._getLineLeftOffset(cursorPosition.lineIndex);
- var bound = this.__charBounds[cursorPosition.lineIndex][cursorPosition.charIndex];
- bound && (leftOffset = bound.left);
- if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) {
- leftOffset -= this._getWidthOfCharSpacing();
- }
- boundaries = {
- top: topOffset,
- left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0),
- };
- this.cursorOffsetCache = boundaries;
- return this.cursorOffsetCache;
- },
-
- /**
- * Renders cursor
- * @param {Object} boundaries
- * @param {CanvasRenderingContext2D} ctx transformed context to draw on
- */
- renderCursor: function(boundaries, ctx) {
- var cursorLocation = this.get2DCursorLocation(),
- lineIndex = cursorLocation.lineIndex,
- charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0,
- charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'),
- multiplier = this.scaleX * this.canvas.getZoom(),
- cursorWidth = this.cursorWidth / multiplier,
- topOffset = boundaries.topOffset;
-
- topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight
- - charHeight * (1 - this._fontSizeFraction);
-
- if (this.inCompositionMode) {
- this.renderSelection(boundaries, ctx);
- }
-
- ctx.fillStyle = this.getValueOfPropertyAt(lineIndex, charIndex, 'fill');
- ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity;
- ctx.fillRect(
- boundaries.left + boundaries.leftOffset - cursorWidth / 2,
- topOffset + boundaries.top,
- cursorWidth,
- charHeight);
- },
-
- /**
- * Renders text selection
- * @param {Object} boundaries Object with left/top/leftOffset/topOffset
- * @param {CanvasRenderingContext2D} ctx transformed context to draw on
- */
- renderSelection: function(boundaries, ctx) {
-
- var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart,
- selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd,
- isJustify = this.textAlign.indexOf('justify') !== -1,
- start = this.get2DCursorLocation(selectionStart),
- end = this.get2DCursorLocation(selectionEnd),
- startLine = start.lineIndex,
- endLine = end.lineIndex,
- startChar = start.charIndex < 0 ? 0 : start.charIndex,
- endChar = end.charIndex < 0 ? 0 : end.charIndex;
-
- for (var i = startLine; i <= endLine; i++) {
- var lineOffset = this._getLineLeftOffset(i) || 0,
- lineHeight = this.getHeightOfLine(i),
- realLineHeight = 0, boxStart = 0, boxEnd = 0;
-
- if (i === startLine) {
- boxStart = this.__charBounds[startLine][startChar].left;
- }
- if (i >= startLine && i < endLine) {
- boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5; // WTF is this 5?
- }
- else if (i === endLine) {
- if (endChar === 0) {
- boxEnd = this.__charBounds[endLine][endChar].left;
- }
- else {
- boxEnd = this.__charBounds[endLine][endChar - 1].left + this.__charBounds[endLine][endChar - 1].width;
- }
- }
- realLineHeight = lineHeight;
- if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) {
- lineHeight /= this.lineHeight;
- }
- if (this.inCompositionMode) {
- ctx.fillStyle = this.compositionColor || 'black';
- ctx.fillRect(
- boundaries.left + lineOffset + boxStart,
- boundaries.top + boundaries.topOffset + lineHeight,
- boxEnd - boxStart,
- 1);
- }
- else {
- ctx.fillStyle = this.selectionColor;
- ctx.fillRect(
- boundaries.left + lineOffset + boxStart,
- boundaries.top + boundaries.topOffset,
- boxEnd - boxStart,
- lineHeight);
- }
-
-
- boundaries.topOffset += realLineHeight;
- }
- },
-
- /**
- * High level function to know the height of the cursor.
- * the currentChar is the one that precedes the cursor
- * Returns fontSize of char at the current cursor
- * @return {Number} Character font size
- */
- getCurrentCharFontSize: function() {
- var cp = this._getCurrentCharIndex();
- return this.getValueOfPropertyAt(cp.l, cp.c, 'fontSize');
- },
-
- /**
- * High level function to know the color of the cursor.
- * the currentChar is the one that precedes the cursor
- * Returns color (fill) of char at the current cursor
- * @return {String} Character color (fill)
- */
- getCurrentCharColor: function() {
- var cp = this._getCurrentCharIndex();
- return this.getValueOfPropertyAt(cp.l, cp.c, 'fill');
- },
-
- /**
- * Returns the cursor position for the getCurrent.. functions
- * @private
- */
- _getCurrentCharIndex: function() {
- var cursorPosition = this.get2DCursorLocation(this.selectionStart, true),
- charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0;
- return { l: cursorPosition.lineIndex, c: charIndex };
- }
- });
-
- /**
- * Returns fabric.IText instance from an object representation
- * @static
- * @memberOf fabric.IText
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] invoked with new instance as argument
- */
- fabric.IText.fromObject = function(object, callback) {
- parseDecoration(object);
- if (object.styles) {
- for (var i in object.styles) {
- for (var j in object.styles[i]) {
- parseDecoration(object.styles[i][j]);
- }
- }
- }
- fabric.Object._fromObject('IText', object, callback, 'text');
- };
-})();
-
-
-(function() {
-
- var clone = fabric.util.object.clone;
-
- fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
-
- /**
- * Initializes all the interactive behavior of IText
- */
- initBehavior: function() {
- this.initAddedHandler();
- this.initRemovedHandler();
- this.initCursorSelectionHandlers();
- this.initDoubleClickSimulation();
- this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
- },
-
- onDeselect: function(options) {
- this.isEditing && this.exitEditing();
- this.selected = false;
- fabric.Object.prototype.onDeselect.call(this, options);
- },
-
- /**
- * Initializes "added" event handler
- */
- initAddedHandler: function() {
- var _this = this;
- this.on('added', function() {
- var canvas = _this.canvas;
- if (canvas) {
- if (!canvas._hasITextHandlers) {
- canvas._hasITextHandlers = true;
- _this._initCanvasHandlers(canvas);
- }
- canvas._iTextInstances = canvas._iTextInstances || [];
- canvas._iTextInstances.push(_this);
- }
- });
- },
-
- initRemovedHandler: function() {
- var _this = this;
- this.on('removed', function() {
- var canvas = _this.canvas;
- if (canvas) {
- canvas._iTextInstances = canvas._iTextInstances || [];
- fabric.util.removeFromArray(canvas._iTextInstances, _this);
- if (canvas._iTextInstances.length === 0) {
- canvas._hasITextHandlers = false;
- _this._removeCanvasHandlers(canvas);
- }
- }
- });
- },
-
- /**
- * register canvas event to manage exiting on other instances
- * @private
- */
- _initCanvasHandlers: function(canvas) {
- canvas._mouseUpITextHandler = (function() {
- if (canvas._iTextInstances) {
- canvas._iTextInstances.forEach(function(obj) {
- obj.__isMousedown = false;
- });
- }
- }).bind(this);
- canvas.on('mouse:up', canvas._mouseUpITextHandler);
- },
-
- /**
- * remove canvas event to manage exiting on other instances
- * @private
- */
- _removeCanvasHandlers: function(canvas) {
- canvas.off('mouse:up', canvas._mouseUpITextHandler);
- },
-
- /**
- * @private
- */
- _tick: function() {
- this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete');
- },
-
- /**
- * @private
- */
- _animateCursor: function(obj, targetOpacity, duration, completeMethod) {
-
- var tickState;
-
- tickState = {
- isAborted: false,
- abort: function() {
- this.isAborted = true;
- },
- };
-
- obj.animate('_currentCursorOpacity', targetOpacity, {
- duration: duration,
- onComplete: function() {
- if (!tickState.isAborted) {
- obj[completeMethod]();
- }
- },
- onChange: function() {
- // we do not want to animate a selection, only cursor
- if (obj.canvas && obj.selectionStart === obj.selectionEnd) {
- obj.renderCursorOrSelection();
- }
- },
- abort: function() {
- return tickState.isAborted;
- }
- });
- return tickState;
- },
-
- /**
- * @private
- */
- _onTickComplete: function() {
-
- var _this = this;
-
- if (this._cursorTimeout1) {
- clearTimeout(this._cursorTimeout1);
- }
- this._cursorTimeout1 = setTimeout(function() {
- _this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick');
- }, 100);
- },
-
- /**
- * Initializes delayed cursor
- */
- initDelayedCursor: function(restart) {
- var _this = this,
- delay = restart ? 0 : this.cursorDelay;
-
- this.abortCursorAnimation();
- this._currentCursorOpacity = 1;
- this._cursorTimeout2 = setTimeout(function() {
- _this._tick();
- }, delay);
- },
-
- /**
- * Aborts cursor animation and clears all timeouts
- */
- abortCursorAnimation: function() {
- var shouldClear = this._currentTickState || this._currentTickCompleteState,
- canvas = this.canvas;
- this._currentTickState && this._currentTickState.abort();
- this._currentTickCompleteState && this._currentTickCompleteState.abort();
-
- clearTimeout(this._cursorTimeout1);
- clearTimeout(this._cursorTimeout2);
-
- this._currentCursorOpacity = 0;
- // to clear just itext area we need to transform the context
- // it may not be worth it
- if (shouldClear && canvas) {
- canvas.clearContext(canvas.contextTop || canvas.contextContainer);
- }
-
- },
-
- /**
- * Selects entire text
- * @return {fabric.IText} thisArg
- * @chainable
- */
- selectAll: function() {
- this.selectionStart = 0;
- this.selectionEnd = this._text.length;
- this._fireSelectionChanged();
- this._updateTextarea();
- return this;
- },
-
- /**
- * Returns selected text
- * @return {String}
- */
- getSelectedText: function() {
- return this._text.slice(this.selectionStart, this.selectionEnd).join('');
- },
-
- /**
- * Find new selection index representing start of current word according to current selection index
- * @param {Number} startFrom Surrent selection index
- * @return {Number} New selection index
- */
- findWordBoundaryLeft: function(startFrom) {
- var offset = 0, index = startFrom - 1;
-
- // remove space before cursor first
- if (this._reSpace.test(this._text[index])) {
- while (this._reSpace.test(this._text[index])) {
- offset++;
- index--;
- }
- }
- while (/\S/.test(this._text[index]) && index > -1) {
- offset++;
- index--;
- }
-
- return startFrom - offset;
- },
-
- /**
- * Find new selection index representing end of current word according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findWordBoundaryRight: function(startFrom) {
- var offset = 0, index = startFrom;
-
- // remove space after cursor first
- if (this._reSpace.test(this._text[index])) {
- while (this._reSpace.test(this._text[index])) {
- offset++;
- index++;
- }
- }
- while (/\S/.test(this._text[index]) && index < this.text.length) {
- offset++;
- index++;
- }
-
- return startFrom + offset;
- },
-
- /**
- * Find new selection index representing start of current line according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findLineBoundaryLeft: function(startFrom) {
- var offset = 0, index = startFrom - 1;
-
- while (!/\n/.test(this._text[index]) && index > -1) {
- offset++;
- index--;
- }
-
- return startFrom - offset;
- },
-
- /**
- * Find new selection index representing end of current line according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findLineBoundaryRight: function(startFrom) {
- var offset = 0, index = startFrom;
-
- while (!/\n/.test(this._text[index]) && index < this.text.length) {
- offset++;
- index++;
- }
-
- return startFrom + offset;
- },
-
- /**
- * Finds index corresponding to beginning or end of a word
- * @param {Number} selectionStart Index of a character
- * @param {Number} direction 1 or -1
- * @return {Number} Index of the beginning or end of a word
- */
- searchWordBoundary: function(selectionStart, direction) {
- var index = this._reSpace.test(this.text.charAt(selectionStart)) ? selectionStart - 1 : selectionStart,
- _char = this.text.charAt(index),
- reNonWord = /[ \n\.,;!\?\-]/;
-
- while (!reNonWord.test(_char) && index > 0 && index < this.text.length) {
- index += direction;
- _char = this.text.charAt(index);
- }
- if (reNonWord.test(_char) && _char !== '\n') {
- index += direction === 1 ? 0 : 1;
- }
- return index;
- },
-
- /**
- * Selects a word based on the index
- * @param {Number} selectionStart Index of a character
- */
- selectWord: function(selectionStart) {
- selectionStart = selectionStart || this.selectionStart;
- var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */
- newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */
-
- this.selectionStart = newSelectionStart;
- this.selectionEnd = newSelectionEnd;
- this._fireSelectionChanged();
- this._updateTextarea();
- this.renderCursorOrSelection();
- },
-
- /**
- * Selects a line based on the index
- * @param {Number} selectionStart Index of a character
- * @return {fabric.IText} thisArg
- * @chainable
- */
- selectLine: function(selectionStart) {
- selectionStart = selectionStart || this.selectionStart;
- var newSelectionStart = this.findLineBoundaryLeft(selectionStart),
- newSelectionEnd = this.findLineBoundaryRight(selectionStart);
-
- this.selectionStart = newSelectionStart;
- this.selectionEnd = newSelectionEnd;
- this._fireSelectionChanged();
- this._updateTextarea();
- return this;
- },
-
- /**
- * Enters editing state
- * @return {fabric.IText} thisArg
- * @chainable
- */
- enterEditing: function(e) {
- if (this.isEditing || !this.editable) {
- return;
- }
-
- if (this.canvas) {
- this.canvas.calcOffset();
- this.exitEditingOnOthers(this.canvas);
- }
-
- this.isEditing = true;
-
- this.initHiddenTextarea(e);
- this.hiddenTextarea.focus();
- this.hiddenTextarea.value = this.text;
- this._updateTextarea();
- this._saveEditingProps();
- this._setEditingProps();
- this._textBeforeEdit = this.text;
-
- this._tick();
- this.fire('editing:entered');
- this._fireSelectionChanged();
- if (!this.canvas) {
- return this;
- }
- this.canvas.fire('text:editing:entered', { target: this });
- this.initMouseMoveHandler();
- this.canvas.requestRenderAll();
- return this;
- },
-
- exitEditingOnOthers: function(canvas) {
- if (canvas._iTextInstances) {
- canvas._iTextInstances.forEach(function(obj) {
- obj.selected = false;
- if (obj.isEditing) {
- obj.exitEditing();
- }
- });
- }
- },
-
- /**
- * Initializes "mousemove" event handler
- */
- initMouseMoveHandler: function() {
- this.canvas.on('mouse:move', this.mouseMoveHandler);
- },
-
- /**
- * @private
- */
- mouseMoveHandler: function(options) {
- if (!this.__isMousedown || !this.isEditing) {
- return;
- }
-
- var newSelectionStart = this.getSelectionStartFromPointer(options.e),
- currentStart = this.selectionStart,
- currentEnd = this.selectionEnd;
- if (
- (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd)
- &&
- (currentStart === newSelectionStart || currentEnd === newSelectionStart)
- ) {
- return;
- }
- if (newSelectionStart > this.__selectionStartOnMouseDown) {
- this.selectionStart = this.__selectionStartOnMouseDown;
- this.selectionEnd = newSelectionStart;
- }
- else {
- this.selectionStart = newSelectionStart;
- this.selectionEnd = this.__selectionStartOnMouseDown;
- }
- if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) {
- this.restartCursorIfNeeded();
- this._fireSelectionChanged();
- this._updateTextarea();
- this.renderCursorOrSelection();
- }
- },
-
- /**
- * @private
- */
- _setEditingProps: function() {
- this.hoverCursor = 'text';
-
- if (this.canvas) {
- this.canvas.defaultCursor = this.canvas.moveCursor = 'text';
- }
-
- this.borderColor = this.editingBorderColor;
-
- this.hasControls = this.selectable = false;
- this.lockMovementX = this.lockMovementY = true;
- },
-
- /**
- * convert from textarea to grapheme indexes
- */
- fromStringToGraphemeSelection: function(start, end, text) {
- var smallerTextStart = text.slice(0, start),
- graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length;
- if (start === end) {
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
- }
- var smallerTextEnd = text.slice(start, end),
- graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length;
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
- },
-
- /**
- * convert from fabric to textarea values
- */
- fromGraphemeToStringSelection: function(start, end, _text) {
- var smallerTextStart = _text.slice(0, start),
- graphemeStart = smallerTextStart.join('').length;
- if (start === end) {
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
- }
- var smallerTextEnd = _text.slice(start, end),
- graphemeEnd = smallerTextEnd.join('').length;
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
- },
-
- /**
- * @private
- */
- _updateTextarea: function() {
- this.cursorOffsetCache = { };
- if (!this.hiddenTextarea) {
- return;
- }
- if (!this.inCompositionMode) {
- var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text);
- this.hiddenTextarea.selectionStart = newSelection.selectionStart;
- this.hiddenTextarea.selectionEnd = newSelection.selectionEnd;
- }
- this.updateTextareaPosition();
- },
-
- /**
- * @private
- */
- updateFromTextArea: function() {
- if (!this.hiddenTextarea) {
- return;
- }
- this.cursorOffsetCache = { };
- this.text = this.hiddenTextarea.value;
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- var newSelection = this.fromStringToGraphemeSelection(
- this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value);
- this.selectionEnd = this.selectionStart = newSelection.selectionEnd;
- if (!this.inCompositionMode) {
- this.selectionStart = newSelection.selectionStart;
- }
- this.updateTextareaPosition();
- },
-
- /**
- * @private
- */
- updateTextareaPosition: function() {
- if (this.selectionStart === this.selectionEnd) {
- var style = this._calcTextareaPosition();
- this.hiddenTextarea.style.left = style.left;
- this.hiddenTextarea.style.top = style.top;
- }
- },
-
- /**
- * @private
- * @return {Object} style contains style for hiddenTextarea
- */
- _calcTextareaPosition: function() {
- if (!this.canvas) {
- return { x: 1, y: 1 };
- }
- var desiredPostion = this.inCompositionMode ? this.compositionStart : this.selectionStart,
- boundaries = this._getCursorBoundaries(desiredPostion),
- cursorLocation = this.get2DCursorLocation(desiredPostion),
- lineIndex = cursorLocation.lineIndex,
- charIndex = cursorLocation.charIndex,
- charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight,
- leftOffset = boundaries.leftOffset,
- m = this.calcTransformMatrix(),
- p = {
- x: boundaries.left + leftOffset,
- y: boundaries.top + boundaries.topOffset + charHeight
- },
- upperCanvas = this.canvas.upperCanvasEl,
- maxWidth = upperCanvas.width - charHeight,
- maxHeight = upperCanvas.height - charHeight;
-
- p = fabric.util.transformPoint(p, m);
- p = fabric.util.transformPoint(p, this.canvas.viewportTransform);
- if (p.x < 0) {
- p.x = 0;
- }
- if (p.x > maxWidth) {
- p.x = maxWidth;
- }
- if (p.y < 0) {
- p.y = 0;
- }
- if (p.y > maxHeight) {
- p.y = maxHeight;
- }
-
- // add canvas offset on document
- p.x += this.canvas._offset.left;
- p.y += this.canvas._offset.top;
-
- return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight };
- },
-
- /**
- * @private
- */
- _saveEditingProps: function() {
- this._savedProps = {
- hasControls: this.hasControls,
- borderColor: this.borderColor,
- lockMovementX: this.lockMovementX,
- lockMovementY: this.lockMovementY,
- hoverCursor: this.hoverCursor,
- defaultCursor: this.canvas && this.canvas.defaultCursor,
- moveCursor: this.canvas && this.canvas.moveCursor
- };
- },
-
- /**
- * @private
- */
- _restoreEditingProps: function() {
- if (!this._savedProps) {
- return;
- }
-
- this.hoverCursor = this._savedProps.hoverCursor;
- this.hasControls = this._savedProps.hasControls;
- this.borderColor = this._savedProps.borderColor;
- this.lockMovementX = this._savedProps.lockMovementX;
- this.lockMovementY = this._savedProps.lockMovementY;
-
- if (this.canvas) {
- this.canvas.defaultCursor = this._savedProps.defaultCursor;
- this.canvas.moveCursor = this._savedProps.moveCursor;
- }
- },
-
- /**
- * Exits from editing state
- * @return {fabric.IText} thisArg
- * @chainable
- */
- exitEditing: function() {
- var isTextChanged = (this._textBeforeEdit !== this.text);
- this.selected = false;
- this.isEditing = false;
- this.selectable = true;
-
- this.selectionEnd = this.selectionStart;
-
- if (this.hiddenTextarea) {
- this.hiddenTextarea.blur && this.hiddenTextarea.blur();
- this.canvas && this.hiddenTextarea.parentNode.removeChild(this.hiddenTextarea);
- this.hiddenTextarea = null;
- }
-
- this.abortCursorAnimation();
- this._restoreEditingProps();
- this._currentCursorOpacity = 0;
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- this.fire('editing:exited');
- isTextChanged && this.fire('modified');
- if (this.canvas) {
- this.canvas.off('mouse:move', this.mouseMoveHandler);
- this.canvas.fire('text:editing:exited', { target: this });
- isTextChanged && this.canvas.fire('object:modified', { target: this });
- }
- return this;
- },
-
- /**
- * @private
- */
- _removeExtraneousStyles: function() {
- for (var prop in this.styles) {
- if (!this._textLines[prop]) {
- delete this.styles[prop];
- }
- }
- },
-
- /**
- * remove and reflow a style block from start to end.
- * @param {Number} start linear start position for removal (included in removal)
- * @param {Number} end linear end position for removal ( excluded from removal )
- */
- removeStyleFromTo: function(start, end) {
- var cursorStart = this.get2DCursorLocation(start, true),
- cursorEnd = this.get2DCursorLocation(end, true),
- lineStart = cursorStart.lineIndex,
- charStart = cursorStart.charIndex,
- lineEnd = cursorEnd.lineIndex,
- charEnd = cursorEnd.charIndex,
- i, styleObj;
- if (lineStart !== lineEnd) {
- // step1 remove the trailing of lineStart
- if (this.styles[lineStart]) {
- for (i = charStart; i < this._unwrappedTextLines[lineStart].length; i++) {
- delete this.styles[lineStart][i];
- }
- }
- // step2 move the trailing of lineEnd to lineStart if needed
- if (this.styles[lineEnd]) {
- for (i = charEnd; i < this._unwrappedTextLines[lineEnd].length; i++) {
- styleObj = this.styles[lineEnd][i];
- if (styleObj) {
- this.styles[lineStart] || (this.styles[lineStart] = { });
- this.styles[lineStart][charStart + i - charEnd] = styleObj;
- }
- }
- }
- // step3 detects lines will be completely removed.
- for (i = lineStart + 1; i <= lineEnd; i++) {
- delete this.styles[i];
- }
- // step4 shift remaining lines.
- this.shiftLineStyles(lineEnd, lineStart - lineEnd);
- }
- else {
- // remove and shift left on the same line
- if (this.styles[lineStart]) {
- styleObj = this.styles[lineStart];
- var diff = charEnd - charStart, numericChar, _char;
- for (i = charStart; i < charEnd; i++) {
- delete styleObj[i];
- }
- for (_char in this.styles[lineStart]) {
- numericChar = parseInt(_char, 10);
- if (numericChar >= charEnd) {
- styleObj[numericChar - diff] = styleObj[_char];
- delete styleObj[_char];
- }
- }
- }
- }
- },
-
- /**
- * Shifts line styles up or down
- * @param {Number} lineIndex Index of a line
- * @param {Number} offset Can any number?
- */
- shiftLineStyles: function(lineIndex, offset) {
- // shift all line styles by offset upward or downward
- // do not clone deep. we need new array, not new style objects
- var clonedStyles = clone(this.styles);
- for (var line in this.styles) {
- var numericLine = parseInt(line, 10);
- if (numericLine > lineIndex) {
- this.styles[numericLine + offset] = clonedStyles[numericLine];
- if (!clonedStyles[numericLine - offset]) {
- delete this.styles[numericLine];
- }
- }
- }
- },
-
- restartCursorIfNeeded: function() {
- if (!this._currentTickState || this._currentTickState.isAborted
- || !this._currentTickCompleteState || this._currentTickCompleteState.isAborted
- ) {
- this.initDelayedCursor();
- }
- },
-
- /**
- * Inserts new style object
- * @param {Number} lineIndex Index of a line
- * @param {Number} charIndex Index of a char
- * @param {Number} qty number of lines to add
- * @param {Array} copiedStyle Array of objects styles
- */
- insertNewlineStyleObject: function(lineIndex, charIndex, qty, copiedStyle) {
- var currentCharStyle,
- newLineStyles = {},
- somethingAdded = false;
-
- qty || (qty = 1);
- this.shiftLineStyles(lineIndex, qty);
- if (this.styles[lineIndex]) {
- currentCharStyle = this.styles[lineIndex][charIndex === 0 ? charIndex : charIndex - 1];
- }
-
- // we clone styles of all chars
- // after cursor onto the current line
- for (var index in this.styles[lineIndex]) {
- var numIndex = parseInt(index, 10);
- if (numIndex >= charIndex) {
- somethingAdded = true;
- newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index];
- // remove lines from the previous line since they're on a new line now
- delete this.styles[lineIndex][index];
- }
- }
- if (somethingAdded) {
- this.styles[lineIndex + qty] = newLineStyles;
- }
- else {
- delete this.styles[lineIndex + qty];
- }
- // for the other lines
- // we clone current char style onto the next (otherwise empty) line
- while (qty > 1) {
- qty--;
- if (copiedStyle && copiedStyle[qty]) {
- this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty]) };
- }
- else if (currentCharStyle) {
- this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) };
- }
- else {
- delete this.styles[lineIndex + qty];
- }
- }
- this._forceClearCache = true;
- },
-
- /**
- * Inserts style object for a given line/char index
- * @param {Number} lineIndex Index of a line
- * @param {Number} charIndex Index of a char
- * @param {Number} quantity number Style object to insert, if given
- * @param {Array} copiedStyle array of style objecs
- */
- insertCharStyleObject: function(lineIndex, charIndex, quantity, copiedStyle) {
- if (!this.styles) {
- this.styles = {};
- }
- var currentLineStyles = this.styles[lineIndex],
- currentLineStylesCloned = currentLineStyles ? clone(currentLineStyles) : {};
-
- quantity || (quantity = 1);
- // shift all char styles by quantity forward
- // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4
- for (var index in currentLineStylesCloned) {
- var numericIndex = parseInt(index, 10);
- if (numericIndex >= charIndex) {
- currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex];
- // only delete the style if there was nothing moved there
- if (!currentLineStylesCloned[numericIndex - quantity]) {
- delete currentLineStyles[numericIndex];
- }
- }
- }
- this._forceClearCache = true;
- if (copiedStyle) {
- while (quantity--) {
- if (!Object.keys(copiedStyle[quantity]).length) {
- continue;
- }
- if (!this.styles[lineIndex]) {
- this.styles[lineIndex] = {};
- }
- this.styles[lineIndex][charIndex + quantity] = clone(copiedStyle[quantity]);
- }
- return;
- }
- if (!currentLineStyles) {
- return;
- }
- var newStyle = currentLineStyles[charIndex ? charIndex - 1 : 1];
- while (newStyle && quantity--) {
- this.styles[lineIndex][charIndex + quantity] = clone(newStyle);
- }
- },
-
- /**
- * Inserts style object(s)
- * @param {Array} insertedText Characters at the location where style is inserted
- * @param {Number} start cursor index for inserting style
- * @param {Array} [copiedStyle] array of style objects to insert.
- */
- insertNewStyleBlock: function(insertedText, start, copiedStyle) {
- var cursorLoc = this.get2DCursorLocation(start, true),
- addedLines = [0], linesLenght = 0;
- for (var i = 0; i < insertedText.length; i++) {
- if (insertedText[i] === '\n') {
- linesLenght++;
- addedLines[linesLenght] = 0;
- }
- else {
- addedLines[linesLenght]++;
- }
- }
- if (addedLines[0] > 0) {
- this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addedLines[0], copiedStyle);
- copiedStyle = copiedStyle && copiedStyle.slice(addedLines[0] + 1);
- }
- linesLenght && this.insertNewlineStyleObject(
- cursorLoc.lineIndex, cursorLoc.charIndex + addedLines[0], linesLenght);
- for (var i = 1; i < linesLenght; i++) {
- if (addedLines[i] > 0) {
- this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle);
- }
- else if (copiedStyle) {
- this.styles[cursorLoc.lineIndex + i][0] = copiedStyle[0];
- }
- copiedStyle = copiedStyle && copiedStyle.slice(addedLines[i] + 1);
- }
- // we use i outside the loop to get it like linesLength
- if (addedLines[i] > 0) {
- this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle);
- }
- },
-
- /**
- * Set the selectionStart and selectionEnd according to the ne postion of cursor
- * mimic the key - mouse navigation when shift is pressed.
- */
- setSelectionStartEndWithShift: function(start, end, newSelection) {
- if (newSelection <= start) {
- if (end === start) {
- this._selectionDirection = 'left';
- }
- else if (this._selectionDirection === 'right') {
- this._selectionDirection = 'left';
- this.selectionEnd = start;
- }
- this.selectionStart = newSelection;
- }
- else if (newSelection > start && newSelection < end) {
- if (this._selectionDirection === 'right') {
- this.selectionEnd = newSelection;
- }
- else {
- this.selectionStart = newSelection;
- }
- }
- else {
- // newSelection is > selection start and end
- if (end === start) {
- this._selectionDirection = 'right';
- }
- else if (this._selectionDirection === 'left') {
- this._selectionDirection = 'right';
- this.selectionStart = end;
- }
- this.selectionEnd = newSelection;
- }
- },
-
- setSelectionInBoundaries: function() {
- var length = this.text.length;
- if (this.selectionStart > length) {
- this.selectionStart = length;
- }
- else if (this.selectionStart < 0) {
- this.selectionStart = 0;
- }
- if (this.selectionEnd > length) {
- this.selectionEnd = length;
- }
- else if (this.selectionEnd < 0) {
- this.selectionEnd = 0;
- }
- }
- });
-})();
-
-
-fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
- /**
- * Initializes "dbclick" event handler
- */
- initDoubleClickSimulation: function() {
-
- // for double click
- this.__lastClickTime = +new Date();
-
- // for triple click
- this.__lastLastClickTime = +new Date();
-
- this.__lastPointer = { };
-
- this.on('mousedown', this.onMouseDown.bind(this));
- },
-
- onMouseDown: function(options) {
-
- this.__newClickTime = +new Date();
- var newPointer = this.canvas.getPointer(options.e);
-
- if (this.isTripleClick(newPointer, options.e)) {
- this.fire('tripleclick', options);
- this._stopEvent(options.e);
- }
- this.__lastLastClickTime = this.__lastClickTime;
- this.__lastClickTime = this.__newClickTime;
- this.__lastPointer = newPointer;
- this.__lastIsEditing = this.isEditing;
- this.__lastSelected = this.selected;
- },
-
- isTripleClick: function(newPointer) {
- return this.__newClickTime - this.__lastClickTime < 500 &&
- this.__lastClickTime - this.__lastLastClickTime < 500 &&
- this.__lastPointer.x === newPointer.x &&
- this.__lastPointer.y === newPointer.y;
- },
-
- /**
- * @private
- */
- _stopEvent: function(e) {
- e.preventDefault && e.preventDefault();
- e.stopPropagation && e.stopPropagation();
- },
-
- /**
- * Initializes event handlers related to cursor or selection
- */
- initCursorSelectionHandlers: function() {
- this.initMousedownHandler();
- this.initMouseupHandler();
- this.initClicks();
- },
-
- /**
- * Initializes double and triple click event handlers
- */
- initClicks: function() {
- this.on('mousedblclick', function(options) {
- this.selectWord(this.getSelectionStartFromPointer(options.e));
- });
- this.on('tripleclick', function(options) {
- this.selectLine(this.getSelectionStartFromPointer(options.e));
- });
- },
-
- /**
- * Default event handler for the basic functionalities needed on _mouseDown
- * can be overridden to do something different.
- * Scope of this implementation is: find the click position, set selectionStart
- * find selectionEnd, initialize the drawing of either cursor or selection area
- */
- _mouseDownHandler: function(options) {
- if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) {
- return;
- }
- var pointer = this.canvas.getPointer(options.e);
-
- this.__mousedownX = pointer.x;
- this.__mousedownY = pointer.y;
- this.__isMousedown = true;
-
- if (this.selected) {
- this.setCursorByClick(options.e);
- }
-
- if (this.isEditing) {
- this.__selectionStartOnMouseDown = this.selectionStart;
- if (this.selectionStart === this.selectionEnd) {
- this.abortCursorAnimation();
- }
- this.renderCursorOrSelection();
- }
- },
-
- /**
- * Initializes "mousedown" event handler
- */
- initMousedownHandler: function() {
- this.on('mousedown', this._mouseDownHandler);
- },
-
- /**
- * @private
- */
- _isObjectMoved: function(e) {
- var pointer = this.canvas.getPointer(e);
-
- return this.__mousedownX !== pointer.x ||
- this.__mousedownY !== pointer.y;
- },
-
- /**
- * Initializes "mouseup" event handler
- */
- initMouseupHandler: function() {
- this.on('mouseup', function(options) {
- this.__isMousedown = false;
- if (!this.editable || this._isObjectMoved(options.e) || (options.e.button && options.e.button !== 1)) {
- return;
- }
-
- if (this.__lastSelected && !this.__corner) {
- this.enterEditing(options.e);
- if (this.selectionStart === this.selectionEnd) {
- this.initDelayedCursor(true);
- }
- else {
- this.renderCursorOrSelection();
- }
- }
- this.selected = true;
- });
- },
-
- /**
- * Changes cursor location in a text depending on passed pointer (x/y) object
- * @param {Event} e Event object
- */
- setCursorByClick: function(e) {
- var newSelection = this.getSelectionStartFromPointer(e),
- start = this.selectionStart, end = this.selectionEnd;
- if (e.shiftKey) {
- this.setSelectionStartEndWithShift(start, end, newSelection);
- }
- else {
- this.selectionStart = newSelection;
- this.selectionEnd = newSelection;
- }
- if (this.isEditing) {
- this._fireSelectionChanged();
- this._updateTextarea();
- }
- },
-
- /**
- * Returns index of a character corresponding to where an object was clicked
- * @param {Event} e Event object
- * @return {Number} Index of a character
- */
- getSelectionStartFromPointer: function(e) {
- var mouseOffset = this.getLocalPointer(e),
- prevWidth = 0,
- width = 0,
- height = 0,
- charIndex = 0,
- lineIndex = 0,
- lineLeftOffset,
- line;
-
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- if (height <= mouseOffset.y) {
- height += this.getHeightOfLine(i) * this.scaleY;
- lineIndex = i;
- if (i > 0) {
- charIndex += this._textLines[i - 1].length + 1;
- }
- }
- else {
- break;
- }
- }
- lineLeftOffset = this._getLineLeftOffset(lineIndex);
- width = lineLeftOffset * this.scaleX;
- line = this._textLines[lineIndex];
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- prevWidth = width;
- // i removed something about flipX here, check.
- width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX;
- if (width <= mouseOffset.x) {
- charIndex++;
- }
- else {
- break;
- }
- }
- return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen);
- },
-
- /**
- * @private
- */
- _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) {
- // we need Math.abs because when width is after the last char, the offset is given as 1, while is 0
- var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth,
- distanceBtwNextCharAndCursor = width - mouseOffset.x,
- offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ||
- distanceBtwNextCharAndCursor < 0 ? 0 : 1,
- newSelectionStart = index + offset;
- // if object is horizontally flipped, mirror cursor location from the end
- if (this.flipX) {
- newSelectionStart = jlen - newSelectionStart;
- }
-
- if (newSelectionStart > this._text.length) {
- newSelectionStart = this._text.length;
- }
-
- return newSelectionStart;
- }
-});
-
-
-fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
-
- /**
- * Initializes hidden textarea (needed to bring up keyboard in iOS)
- */
- initHiddenTextarea: function() {
- this.hiddenTextarea = fabric.document.createElement('textarea');
- this.hiddenTextarea.setAttribute('autocapitalize', 'off');
- this.hiddenTextarea.setAttribute('autocorrect', 'off');
- this.hiddenTextarea.setAttribute('autocomplete', 'off');
- this.hiddenTextarea.setAttribute('spellcheck', 'false');
- this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', '');
- this.hiddenTextarea.setAttribute('wrap', 'off');
- var style = this._calcTextareaPosition();
- this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top +
- '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' +
- ' line-height: 1px; paddingーtop: ' + style.fontSize + ';';
- fabric.document.body.appendChild(this.hiddenTextarea);
-
- fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this));
-
- if (!this._clickHandlerInitialized && this.canvas) {
- fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this));
- this._clickHandlerInitialized = true;
- }
- },
-
- /**
- * For functionalities on keyDown
- * Map a special key to a function of the instance/prototype
- * If you need different behaviour for ESC or TAB or arrows, you have to change
- * this map setting the name of a function that you build on the fabric.Itext or
- * your prototype.
- * the map change will affect all Instances unless you need for only some text Instances
- * in that case you have to clone this object and assign your Instance.
- * this.keysMap = fabric.util.object.clone(this.keysMap);
- * The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0]
- */
- keysMap: {
- 9: 'exitEditing',
- 27: 'exitEditing',
- 33: 'moveCursorUp',
- 34: 'moveCursorDown',
- 35: 'moveCursorRight',
- 36: 'moveCursorLeft',
- 37: 'moveCursorLeft',
- 38: 'moveCursorUp',
- 39: 'moveCursorRight',
- 40: 'moveCursorDown',
- },
-
- /**
- * For functionalities on keyUp + ctrl || cmd
- */
- ctrlKeysMapUp: {
- 67: 'copy',
- 88: 'cut'
- },
-
- /**
- * For functionalities on keyDown + ctrl || cmd
- */
- ctrlKeysMapDown: {
- 65: 'selectAll'
- },
-
- onClick: function() {
- // No need to trigger click event here, focus is enough to have the keyboard appear on Android
- this.hiddenTextarea && this.hiddenTextarea.focus();
- },
-
- /**
- * Handles keyup event
- * @param {Event} e Event object
- */
- onKeyDown: function(e) {
- if (!this.isEditing || this.inCompositionMode) {
- return;
- }
- if (e.keyCode in this.keysMap) {
- this[this.keysMap[e.keyCode]](e);
- }
- else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) {
- this[this.ctrlKeysMapDown[e.keyCode]](e);
- }
- else {
- return;
- }
- e.stopImmediatePropagation();
- e.preventDefault();
- if (e.keyCode >= 33 && e.keyCode <= 40) {
- // if i press an arrow key just update selection
- this.clearContextTop();
- this.renderCursorOrSelection();
- }
- else {
- this.canvas && this.canvas.requestRenderAll();
- }
- },
-
- /**
- * Handles keyup event
- * We handle KeyUp because ie11 and edge have difficulties copy/pasting
- * if a copy/cut event fired, keyup is dismissed
- * @param {Event} e Event object
- */
- onKeyUp: function(e) {
- if (!this.isEditing || this._copyDone || this.inCompositionMode) {
- this._copyDone = false;
- return;
- }
- if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) {
- this[this.ctrlKeysMapUp[e.keyCode]](e);
- }
- else {
- return;
- }
- e.stopImmediatePropagation();
- e.preventDefault();
- this.canvas && this.canvas.requestRenderAll();
- },
-
- /**
- * Handles onInput event
- * @param {Event} e Event object
- */
- onInput: function(e) {
- var fromPaste = this.fromPaste;
- this.fromPaste = false;
- e && e.stopPropagation();
- if (!this.isEditing) {
- return;
- }
- // decisions about style changes.
- var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,
- charCount = this._text.length,
- nextCharCount = nextText.length,
- removedText, insertedText,
- charDiff = nextCharCount - charCount;
- if (this.hiddenTextarea.value === '') {
- this.styles = { };
- this.updateFromTextArea();
- this.fire('changed');
- if (this.canvas) {
- this.canvas.fire('text:changed', { target: this });
- this.canvas.requestRenderAll();
- }
- return;
- }
-
- var textareaSelection = this.fromStringToGraphemeSelection(
- this.hiddenTextarea.selectionStart,
- this.hiddenTextarea.selectionEnd,
- this.hiddenTextarea.value
- );
- var backDelete = this.selectionStart > textareaSelection.selectionStart;
-
- if (this.selectionStart !== this.selectionEnd) {
- removedText = this._text.slice(this.selectionStart, this.selectionEnd);
- charDiff += this.selectionEnd - this.selectionStart;
- }
- else if (nextCharCount < charCount) {
- if (backDelete) {
- removedText = this._text.slice(this.selectionEnd + charDiff, this.selectionEnd);
- }
- else {
- removedText = this._text.slice(this.selectionStart, this.selectionStart - charDiff);
- }
- }
- insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd);
- if (removedText && removedText.length) {
- if (this.selectionStart !== this.selectionEnd) {
- this.removeStyleFromTo(this.selectionStart, this.selectionEnd);
- }
- else if (backDelete) {
- // detect differencies between forwardDelete and backDelete
- this.removeStyleFromTo(this.selectionEnd - removedText.length, this.selectionEnd);
- }
- else {
- this.removeStyleFromTo(this.selectionEnd, this.selectionEnd + removedText.length);
- }
- }
- if (insertedText.length) {
- if (fromPaste && insertedText.join('') === fabric.copiedText) {
- this.insertNewStyleBlock(insertedText, this.selectionStart, fabric.copiedTextStyle);
- }
- else {
- this.insertNewStyleBlock(insertedText, this.selectionStart);
- }
- }
- this.updateFromTextArea();
- this.fire('changed');
- if (this.canvas) {
- this.canvas.fire('text:changed', { target: this });
- this.canvas.requestRenderAll();
- }
- },
- /**
- * Composition start
- */
- onCompositionStart: function() {
- this.inCompositionMode = true;
- },
-
- /**
- * Composition end
- */
- onCompositionEnd: function() {
- this.inCompositionMode = false;
- },
-
- // /**
- // * Composition update
- // */
- onCompositionUpdate: function(e) {
- this.compositionStart = e.target.selectionStart;
- this.compositionEnd = e.target.selectionEnd;
- this.updateTextareaPosition();
- },
-
- /**
- * Copies selected text
- * @param {Event} e Event object
- */
- copy: function() {
- if (this.selectionStart === this.selectionEnd) {
- //do not cut-copy if no selection
- return;
- }
-
- fabric.copiedText = this.getSelectedText();
- fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true);
- this._copyDone = true;
- },
-
- /**
- * Pastes text
- * @param {Event} e Event object
- */
- paste: function() {
- this.fromPaste = true;
- },
-
- /**
- * @private
- * @param {Event} e Event object
- * @return {Object} Clipboard data object
- */
- _getClipboardData: function(e) {
- return (e && e.clipboardData) || fabric.window.clipboardData;
- },
-
- /**
- * Finds the width in pixels before the cursor on the same line
- * @private
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @return {Number} widthBeforeCursor width before cursor
- */
- _getWidthBeforeCursor: function(lineIndex, charIndex) {
- var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound;
-
- if (charIndex > 0) {
- bound = this.__charBounds[lineIndex][charIndex - 1];
- widthBeforeCursor += bound.left + bound.width;
- }
- return widthBeforeCursor;
- },
-
- /**
- * Gets start offset of a selection
- * @param {Event} e Event object
- * @param {Boolean} isRight
- * @return {Number}
- */
- getDownCursorOffset: function(e, isRight) {
- var selectionProp = this._getSelectionForOffset(e, isRight),
- cursorLocation = this.get2DCursorLocation(selectionProp),
- lineIndex = cursorLocation.lineIndex;
- // if on last line, down cursor goes to end of line
- if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) {
- // move to the end of a text
- return this._text.length - selectionProp;
- }
- var charIndex = cursorLocation.charIndex,
- widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
- indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor),
- textAfterCursor = this._textLines[lineIndex].slice(charIndex);
- return textAfterCursor.length + indexOnOtherLine + 2;
- },
-
- /**
- * private
- * Helps finding if the offset should be counted from Start or End
- * @param {Event} e Event object
- * @param {Boolean} isRight
- * @return {Number}
- */
- _getSelectionForOffset: function(e, isRight) {
- if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) {
- return this.selectionEnd;
- }
- else {
- return this.selectionStart;
- }
- },
-
- /**
- * @param {Event} e Event object
- * @param {Boolean} isRight
- * @return {Number}
- */
- getUpCursorOffset: function(e, isRight) {
- var selectionProp = this._getSelectionForOffset(e, isRight),
- cursorLocation = this.get2DCursorLocation(selectionProp),
- lineIndex = cursorLocation.lineIndex;
- if (lineIndex === 0 || e.metaKey || e.keyCode === 33) {
- // if on first line, up cursor goes to start of line
- return -selectionProp;
- }
- var charIndex = cursorLocation.charIndex,
- widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
- indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor),
- textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex);
- // return a negative offset
- return -this._textLines[lineIndex - 1].length + indexOnOtherLine - textBeforeCursor.length;
- },
-
- /**
- * for a given width it founds the matching character.
- * @private
- */
- _getIndexOnLine: function(lineIndex, width) {
-
- var line = this._textLines[lineIndex],
- lineLeftOffset = this._getLineLeftOffset(lineIndex),
- widthOfCharsOnLine = lineLeftOffset,
- indexOnLine = 0, charWidth, foundMatch;
-
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- charWidth = this.__charBounds[lineIndex][j].width;
- widthOfCharsOnLine += charWidth;
- if (widthOfCharsOnLine > width) {
- foundMatch = true;
- var leftEdge = widthOfCharsOnLine - charWidth,
- rightEdge = widthOfCharsOnLine,
- offsetFromLeftEdge = Math.abs(leftEdge - width),
- offsetFromRightEdge = Math.abs(rightEdge - width);
-
- indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1);
- break;
- }
- }
-
- // reached end
- if (!foundMatch) {
- indexOnLine = line.length - 1;
- }
-
- return indexOnLine;
- },
-
-
- /**
- * Moves cursor down
- * @param {Event} e Event object
- */
- moveCursorDown: function(e) {
- if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
- return;
- }
- this._moveCursorUpOrDown('Down', e);
- },
-
- /**
- * Moves cursor up
- * @param {Event} e Event object
- */
- moveCursorUp: function(e) {
- if (this.selectionStart === 0 && this.selectionEnd === 0) {
- return;
- }
- this._moveCursorUpOrDown('Up', e);
- },
-
- /**
- * Moves cursor up or down, fires the events
- * @param {String} direction 'Up' or 'Down'
- * @param {Event} e Event object
- */
- _moveCursorUpOrDown: function(direction, e) {
- // getUpCursorOffset
- // getDownCursorOffset
- var action = 'get' + direction + 'CursorOffset',
- offset = this[action](e, this._selectionDirection === 'right');
- if (e.shiftKey) {
- this.moveCursorWithShift(offset);
- }
- else {
- this.moveCursorWithoutShift(offset);
- }
- if (offset !== 0) {
- this.setSelectionInBoundaries();
- this.abortCursorAnimation();
- this._currentCursorOpacity = 1;
- this.initDelayedCursor();
- this._fireSelectionChanged();
- this._updateTextarea();
- }
- },
-
- /**
- * Moves cursor with shift
- * @param {Number} offset
- */
- moveCursorWithShift: function(offset) {
- var newSelection = this._selectionDirection === 'left'
- ? this.selectionStart + offset
- : this.selectionEnd + offset;
- this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection);
- return offset !== 0;
- },
-
- /**
- * Moves cursor up without shift
- * @param {Number} offset
- */
- moveCursorWithoutShift: function(offset) {
- if (offset < 0) {
- this.selectionStart += offset;
- this.selectionEnd = this.selectionStart;
- }
- else {
- this.selectionEnd += offset;
- this.selectionStart = this.selectionEnd;
- }
- return offset !== 0;
- },
-
- /**
- * Moves cursor left
- * @param {Event} e Event object
- */
- moveCursorLeft: function(e) {
- if (this.selectionStart === 0 && this.selectionEnd === 0) {
- return;
- }
- this._moveCursorLeftOrRight('Left', e);
- },
-
- /**
- * @private
- * @return {Boolean} true if a change happened
- */
- _move: function(e, prop, direction) {
- var newValue;
- if (e.altKey) {
- newValue = this['findWordBoundary' + direction](this[prop]);
- }
- else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) {
- newValue = this['findLineBoundary' + direction](this[prop]);
- }
- else {
- this[prop] += direction === 'Left' ? -1 : 1;
- return true;
- }
- if (typeof newValue !== undefined && this[prop] !== newValue) {
- this[prop] = newValue;
- return true;
- }
- },
-
- /**
- * @private
- */
- _moveLeft: function(e, prop) {
- return this._move(e, prop, 'Left');
- },
-
- /**
- * @private
- */
- _moveRight: function(e, prop) {
- return this._move(e, prop, 'Right');
- },
-
- /**
- * Moves cursor left without keeping selection
- * @param {Event} e
- */
- moveCursorLeftWithoutShift: function(e) {
- var change = true;
- this._selectionDirection = 'left';
-
- // only move cursor when there is no selection,
- // otherwise we discard it, and leave cursor on same place
- if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) {
- change = this._moveLeft(e, 'selectionStart');
-
- }
- this.selectionEnd = this.selectionStart;
- return change;
- },
-
- /**
- * Moves cursor left while keeping selection
- * @param {Event} e
- */
- moveCursorLeftWithShift: function(e) {
- if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) {
- return this._moveLeft(e, 'selectionEnd');
- }
- else if (this.selectionStart !== 0){
- this._selectionDirection = 'left';
- return this._moveLeft(e, 'selectionStart');
- }
- },
-
- /**
- * Moves cursor right
- * @param {Event} e Event object
- */
- moveCursorRight: function(e) {
- if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
- return;
- }
- this._moveCursorLeftOrRight('Right', e);
- },
-
- /**
- * Moves cursor right or Left, fires event
- * @param {String} direction 'Left', 'Right'
- * @param {Event} e Event object
- */
- _moveCursorLeftOrRight: function(direction, e) {
- var actionName = 'moveCursor' + direction + 'With';
- this._currentCursorOpacity = 1;
-
- if (e.shiftKey) {
- actionName += 'Shift';
- }
- else {
- actionName += 'outShift';
- }
- if (this[actionName](e)) {
- this.abortCursorAnimation();
- this.initDelayedCursor();
- this._fireSelectionChanged();
- this._updateTextarea();
- }
- },
-
- /**
- * Moves cursor right while keeping selection
- * @param {Event} e
- */
- moveCursorRightWithShift: function(e) {
- if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) {
- return this._moveRight(e, 'selectionStart');
- }
- else if (this.selectionEnd !== this._text.length) {
- this._selectionDirection = 'right';
- return this._moveRight(e, 'selectionEnd');
- }
- },
-
- /**
- * Moves cursor right without keeping selection
- * @param {Event} e Event object
- */
- moveCursorRightWithoutShift: function(e) {
- var changed = true;
- this._selectionDirection = 'right';
-
- if (this.selectionStart === this.selectionEnd) {
- changed = this._moveRight(e, 'selectionStart');
- this.selectionEnd = this.selectionStart;
- }
- else {
- this.selectionStart = this.selectionEnd;
- }
- return changed;
- },
-
- /**
- * Removes characters from start/end
- * start/end ar per grapheme position in _text array.
- *
- * @param {Number} start
- * @param {Number} end default to start + 1
- */
- removeChars: function(start, end) {
- if (typeof end === 'undefined') {
- end = start + 1;
- }
- this.removeStyleFromTo(start, end);
- this._text.splice(start, end - start);
- this.text = this._text.join('');
- this.set('dirty', true);
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- this._removeExtraneousStyles();
- },
-
- /**
- * insert characters at start position, before start position.
- * start equal 1 it means the text get inserted between actual grapheme 0 and 1
- * if style array is provided, it must be as the same length of text in graphemes
- * if end is provided and is bigger than start, old text is replaced.
- * start/end ar per grapheme position in _text array.
- *
- * @param {String} text text to insert
- * @param {Array} style array of style objects
- * @param {Number} start
- * @param {Number} end default to start + 1
- */
- insertChars: function(text, style, start, end) {
- if (typeof end === 'undefined') {
- end = start;
- }
- if (end > start) {
- this.removeStyleFromTo(start, end);
- }
- var graphemes = fabric.util.string.graphemeSplit(text);
- this.insertNewStyleBlock(graphemes, start, style);
- this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end));
- this.text = this._text.join('');
- this.set('dirty', true);
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
+fabric.util.object.extend(fabric.IText.prototype, {
+ initDoubleClickSimulation: function() {
+ this.__lastClickTime = +new Date();
+ this.__lastLastClickTime = +new Date();
+ this.__lastPointer = {};
+ this.on("mousedown", this.onMouseDown.bind(this));
+ },
+ onMouseDown: function(options) {
+ this.__newClickTime = +new Date();
+ var newPointer = this.canvas.getPointer(options.e);
+ if (this.isTripleClick(newPointer, options.e)) {
+ this.fire("tripleclick", options);
+ this._stopEvent(options.e);
+ }
+ this.__lastLastClickTime = this.__lastClickTime;
+ this.__lastClickTime = this.__newClickTime;
+ this.__lastPointer = newPointer;
+ this.__lastIsEditing = this.isEditing;
+ this.__lastSelected = this.selected;
+ },
+ isTripleClick: function(newPointer) {
+ return this.__newClickTime - this.__lastClickTime < 500 && this.__lastClickTime - this.__lastLastClickTime < 500 && this.__lastPointer.x === newPointer.x && this.__lastPointer.y === newPointer.y;
+ },
+ _stopEvent: function(e) {
+ e.preventDefault && e.preventDefault();
+ e.stopPropagation && e.stopPropagation();
+ },
+ initCursorSelectionHandlers: function() {
+ this.initMousedownHandler();
+ this.initMouseupHandler();
+ this.initClicks();
+ },
+ initClicks: function() {
+ this.on("mousedblclick", function(options) {
+ this.selectWord(this.getSelectionStartFromPointer(options.e));
+ });
+ this.on("tripleclick", function(options) {
+ this.selectLine(this.getSelectionStartFromPointer(options.e));
+ });
+ },
+ _mouseDownHandler: function(options) {
+ if (!this.canvas || !this.editable || options.e.button && options.e.button !== 1) {
+ return;
+ }
+ var pointer = this.canvas.getPointer(options.e);
+ this.__mousedownX = pointer.x;
+ this.__mousedownY = pointer.y;
+ this.__isMousedown = true;
+ if (this.selected) {
+ this.setCursorByClick(options.e);
+ }
+ if (this.isEditing) {
+ this.__selectionStartOnMouseDown = this.selectionStart;
+ if (this.selectionStart === this.selectionEnd) {
+ this.abortCursorAnimation();
+ }
+ this.renderCursorOrSelection();
+ }
+ },
+ initMousedownHandler: function() {
+ this.on("mousedown", this._mouseDownHandler);
+ },
+ _isObjectMoved: function(e) {
+ var pointer = this.canvas.getPointer(e);
+ return this.__mousedownX !== pointer.x || this.__mousedownY !== pointer.y;
+ },
+ initMouseupHandler: function() {
+ this.on("mouseup", function(options) {
+ this.__isMousedown = false;
+ if (!this.editable || this._isObjectMoved(options.e) || options.e.button && options.e.button !== 1) {
+ return;
+ }
+ if (this.__lastSelected && !this.__corner) {
+ this.enterEditing(options.e);
+ if (this.selectionStart === this.selectionEnd) {
+ this.initDelayedCursor(true);
+ } else {
+ this.renderCursorOrSelection();
+ }
+ }
+ this.selected = true;
+ });
+ },
+ setCursorByClick: function(e) {
+ var newSelection = this.getSelectionStartFromPointer(e), start = this.selectionStart, end = this.selectionEnd;
+ if (e.shiftKey) {
+ this.setSelectionStartEndWithShift(start, end, newSelection);
+ } else {
+ this.selectionStart = newSelection;
+ this.selectionEnd = newSelection;
+ }
+ if (this.isEditing) {
+ this._fireSelectionChanged();
+ this._updateTextarea();
+ }
+ },
+ getSelectionStartFromPointer: function(e) {
+ var mouseOffset = this.getLocalPointer(e), prevWidth = 0, width = 0, height = 0, charIndex = 0, lineIndex = 0, lineLeftOffset, line;
+ for (var i = 0, len = this._textLines.length; i < len; i++) {
+ if (height <= mouseOffset.y) {
+ height += this.getHeightOfLine(i) * this.scaleY;
+ lineIndex = i;
+ if (i > 0) {
+ charIndex += this._textLines[i - 1].length + 1;
+ }
+ } else {
+ break;
+ }
+ }
+ lineLeftOffset = this._getLineLeftOffset(lineIndex);
+ width = lineLeftOffset * this.scaleX;
+ line = this._textLines[lineIndex];
+ for (var j = 0, jlen = line.length; j < jlen; j++) {
+ prevWidth = width;
+ width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX;
+ if (width <= mouseOffset.x) {
+ charIndex++;
+ } else {
+ break;
+ }
+ }
+ return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen);
+ },
+ _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) {
+ var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, distanceBtwNextCharAndCursor = width - mouseOffset.x, offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor || distanceBtwNextCharAndCursor < 0 ? 0 : 1, newSelectionStart = index + offset;
+ if (this.flipX) {
+ newSelectionStart = jlen - newSelectionStart;
+ }
+ if (newSelectionStart > this._text.length) {
+ newSelectionStart = this._text.length;
+ }
+ return newSelectionStart;
}
- this._removeExtraneousStyles();
- },
-
});
-
-/* _TO_SVG_START_ */
-(function() {
- var toFixed = fabric.util.toFixed;
-
- fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ {
-
- /**
- * Returns SVG representation of an instance
- * @param {Function} [reviver] Method for further parsing of svg representation.
- * @return {String} svg representation of an instance
- */
- toSVG: function(reviver) {
- var markup = this._createBaseSVGMarkup(),
- offsets = this._getSVGLeftTopOffsets(),
- textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft);
- this._wrapSVGTextAndBg(markup, textAndBg);
-
- return reviver ? reviver(markup.join('')) : markup.join('');
+fabric.util.object.extend(fabric.IText.prototype, {
+ initHiddenTextarea: function() {
+ this.hiddenTextarea = fabric.document.createElement("textarea");
+ this.hiddenTextarea.setAttribute("autocapitalize", "off");
+ this.hiddenTextarea.setAttribute("autocorrect", "off");
+ this.hiddenTextarea.setAttribute("autocomplete", "off");
+ this.hiddenTextarea.setAttribute("spellcheck", "false");
+ this.hiddenTextarea.setAttribute("data-fabric-hiddentextarea", "");
+ this.hiddenTextarea.setAttribute("wrap", "off");
+ var style = this._calcTextareaPosition();
+ this.hiddenTextarea.style.cssText = "position: absolute; top: " + style.top + "; left: " + style.left + "; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;" + " line-height: 1px; paddingーtop: " + style.fontSize + ";";
+ fabric.document.body.appendChild(this.hiddenTextarea);
+ fabric.util.addListener(this.hiddenTextarea, "keydown", this.onKeyDown.bind(this));
+ fabric.util.addListener(this.hiddenTextarea, "keyup", this.onKeyUp.bind(this));
+ fabric.util.addListener(this.hiddenTextarea, "input", this.onInput.bind(this));
+ fabric.util.addListener(this.hiddenTextarea, "copy", this.copy.bind(this));
+ fabric.util.addListener(this.hiddenTextarea, "cut", this.copy.bind(this));
+ fabric.util.addListener(this.hiddenTextarea, "paste", this.paste.bind(this));
+ fabric.util.addListener(this.hiddenTextarea, "compositionstart", this.onCompositionStart.bind(this));
+ fabric.util.addListener(this.hiddenTextarea, "compositionupdate", this.onCompositionUpdate.bind(this));
+ fabric.util.addListener(this.hiddenTextarea, "compositionend", this.onCompositionEnd.bind(this));
+ if (!this._clickHandlerInitialized && this.canvas) {
+ fabric.util.addListener(this.canvas.upperCanvasEl, "click", this.onClick.bind(this));
+ this._clickHandlerInitialized = true;
+ }
+ },
+ keysMap: {
+ 9: "exitEditing",
+ 27: "exitEditing",
+ 33: "moveCursorUp",
+ 34: "moveCursorDown",
+ 35: "moveCursorRight",
+ 36: "moveCursorLeft",
+ 37: "moveCursorLeft",
+ 38: "moveCursorUp",
+ 39: "moveCursorRight",
+ 40: "moveCursorDown"
+ },
+ ctrlKeysMapUp: {
+ 67: "copy",
+ 88: "cut"
+ },
+ ctrlKeysMapDown: {
+ 65: "selectAll"
+ },
+ onClick: function() {
+ this.hiddenTextarea && this.hiddenTextarea.focus();
+ },
+ onKeyDown: function(e) {
+ if (!this.isEditing || this.inCompositionMode) {
+ return;
+ }
+ if (e.keyCode in this.keysMap) {
+ this[this.keysMap[e.keyCode]](e);
+ } else if (e.keyCode in this.ctrlKeysMapDown && (e.ctrlKey || e.metaKey)) {
+ this[this.ctrlKeysMapDown[e.keyCode]](e);
+ } else {
+ return;
+ }
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ if (e.keyCode >= 33 && e.keyCode <= 40) {
+ this.clearContextTop();
+ this.renderCursorOrSelection();
+ } else {
+ this.canvas && this.canvas.requestRenderAll();
+ }
},
-
- /**
- * @private
- */
- _getSVGLeftTopOffsets: function() {
- return {
- textLeft: -this.width / 2,
- textTop: -this.height / 2,
- lineTop: this.getHeightOfLine(0)
- };
+ onKeyUp: function(e) {
+ if (!this.isEditing || this._copyDone || this.inCompositionMode) {
+ this._copyDone = false;
+ return;
+ }
+ if (e.keyCode in this.ctrlKeysMapUp && (e.ctrlKey || e.metaKey)) {
+ this[this.ctrlKeysMapUp[e.keyCode]](e);
+ } else {
+ return;
+ }
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ this.canvas && this.canvas.requestRenderAll();
},
-
- /**
- * @private
- */
- _wrapSVGTextAndBg: function(markup, textAndBg) {
- var noShadow = true, filter = this.getSvgFilter(),
- style = filter === '' ? '' : ' style="' + filter + '"',
- textDecoration = this.getSvgTextDecoration(this);
- markup.push(
- '\t\n',
- textAndBg.textBgRects.join(''),
- '\t\t',
- textAndBg.textSpans.join(''),
- '\n',
- '\t\n'
- );
+ onInput: function(e) {
+ var fromPaste = this.fromPaste;
+ this.fromPaste = false;
+ e && e.stopPropagation();
+ if (!this.isEditing) {
+ return;
+ }
+ var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText, charCount = this._text.length, nextCharCount = nextText.length, removedText, insertedText, charDiff = nextCharCount - charCount;
+ if (this.hiddenTextarea.value === "") {
+ this.styles = {};
+ this.updateFromTextArea();
+ this.fire("changed");
+ if (this.canvas) {
+ this.canvas.fire("text:changed", {
+ target: this
+ });
+ this.canvas.requestRenderAll();
+ }
+ return;
+ }
+ var textareaSelection = this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value);
+ var backDelete = this.selectionStart > textareaSelection.selectionStart;
+ if (this.selectionStart !== this.selectionEnd) {
+ removedText = this._text.slice(this.selectionStart, this.selectionEnd);
+ charDiff += this.selectionEnd - this.selectionStart;
+ } else if (nextCharCount < charCount) {
+ if (backDelete) {
+ removedText = this._text.slice(this.selectionEnd + charDiff, this.selectionEnd);
+ } else {
+ removedText = this._text.slice(this.selectionStart, this.selectionStart - charDiff);
+ }
+ }
+ insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd);
+ if (removedText && removedText.length) {
+ if (this.selectionStart !== this.selectionEnd) {
+ this.removeStyleFromTo(this.selectionStart, this.selectionEnd);
+ } else if (backDelete) {
+ this.removeStyleFromTo(this.selectionEnd - removedText.length, this.selectionEnd);
+ } else {
+ this.removeStyleFromTo(this.selectionEnd, this.selectionEnd + removedText.length);
+ }
+ }
+ if (insertedText.length) {
+ if (fromPaste && insertedText.join("") === fabric.copiedText) {
+ this.insertNewStyleBlock(insertedText, this.selectionStart, fabric.copiedTextStyle);
+ } else {
+ this.insertNewStyleBlock(insertedText, this.selectionStart);
+ }
+ }
+ this.updateFromTextArea();
+ this.fire("changed");
+ if (this.canvas) {
+ this.canvas.fire("text:changed", {
+ target: this
+ });
+ this.canvas.requestRenderAll();
+ }
},
-
- /**
- * @private
- * @param {Number} textTopOffset Text top offset
- * @param {Number} textLeftOffset Text left offset
- * @return {Object}
- */
- _getSVGTextAndBg: function(textTopOffset, textLeftOffset) {
- var textSpans = [],
- textBgRects = [],
- height = textTopOffset, lineOffset;
- // bounding-box background
- this._setSVGBg(textBgRects);
-
- // text and text-background
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- lineOffset = this._getLineLeftOffset(i);
- if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) {
- this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height);
- }
- this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height);
- height += this.getHeightOfLine(i);
- }
-
- return {
- textSpans: textSpans,
- textBgRects: textBgRects
- };
+ onCompositionStart: function() {
+ this.inCompositionMode = true;
},
-
- /**
- * @private
- */
- _createTextCharSpan: function(_char, styleDecl, left, top) {
- var styleProps = this.getSvgSpanStyles(styleDecl, _char !== _char.trim()),
- fillStyles = styleProps ? 'style="' + styleProps + '"' : '',
- NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
-
- return [
- '',
- fabric.util.string.escapeXml(_char),
- ''
- ].join('');
+ onCompositionEnd: function() {
+ this.inCompositionMode = false;
},
-
- _setSVGTextLineText: function(textSpans, lineIndex, textLeftOffset, textTopOffset) {
- // set proper line offset
- var lineHeight = this.getHeightOfLine(lineIndex),
- isJustify = this.textAlign.indexOf('justify') !== -1,
- actualStyle,
- nextStyle,
- charsToRender = '',
- charBox, style,
- boxWidth = 0,
- line = this._textLines[lineIndex],
- timeToRender;
-
- textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight;
- for (var i = 0, len = line.length - 1; i <= len; i++) {
- timeToRender = i === len || this.charSpacing;
- charsToRender += line[i];
- charBox = this.__charBounds[lineIndex][i];
- if (boxWidth === 0) {
- textLeftOffset += charBox.kernedWidth - charBox.width;
- boxWidth += charBox.width;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- if (isJustify && !timeToRender) {
- if (this._reSpaceAndTab.test(line[i])) {
- timeToRender = true;
- }
- }
- if (!timeToRender) {
- // if we have charSpacing, we render char by char
- actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
- nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
- timeToRender = this._hasStyleChangedForSvg(actualStyle, nextStyle);
- }
- if (timeToRender) {
- style = this._getStyleDeclaration(lineIndex, i) || { };
- textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset));
- charsToRender = '';
- actualStyle = nextStyle;
- textLeftOffset += boxWidth;
- boxWidth = 0;
- }
- }
+ onCompositionUpdate: function(e) {
+ this.compositionStart = e.target.selectionStart;
+ this.compositionEnd = e.target.selectionEnd;
+ this.updateTextareaPosition();
},
-
- _pushTextBgRect: function(textBgRects, color, left, top, width, height) {
- var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
- textBgRects.push(
- '\t\t\n');
+ copy: function() {
+ if (this.selectionStart === this.selectionEnd) {
+ return;
+ }
+ fabric.copiedText = this.getSelectedText();
+ fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true);
+ this._copyDone = true;
},
-
- _setSVGTextLineBg: function(textBgRects, i, leftOffset, textTopOffset) {
- var line = this._textLines[i],
- heightOfLine = this.getHeightOfLine(i) / this.lineHeight,
- boxWidth = 0,
- boxStart = 0,
- charBox, currentColor,
- lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor');
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- charBox = this.__charBounds[i][j];
- currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
- if (currentColor !== lastColor) {
- lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart,
- textTopOffset, boxWidth, heightOfLine);
- boxStart = charBox.left;
- boxWidth = charBox.width;
- lastColor = currentColor;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- }
- currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart,
- textTopOffset, boxWidth, heightOfLine);
+ paste: function() {
+ this.fromPaste = true;
},
-
- /**
- * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values
- * we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1
- *
- * @private
- * @param {*} value
- * @return {String}
- */
- _getFillAttributes: function(value) {
- var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : '';
- if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) {
- return 'fill="' + value + '"';
- }
- return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"';
+ _getClipboardData: function(e) {
+ return e && e.clipboardData || fabric.window.clipboardData;
},
-
- /**
- * @private
- */
- _getSVGLineTopOffset: function(lineIndex) {
- var lineTopOffset = 0, lastHeight = 0;
- for (var j = 0; j < lineIndex; j++) {
- lineTopOffset += this.getHeightOfLine(j);
- }
- lastHeight = this.getHeightOfLine(j);
- return {
- lineTop: lineTopOffset,
- offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult)
- };
+ _getWidthBeforeCursor: function(lineIndex, charIndex) {
+ var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound;
+ if (charIndex > 0) {
+ bound = this.__charBounds[lineIndex][charIndex - 1];
+ widthBeforeCursor += bound.left + bound.width;
+ }
+ return widthBeforeCursor;
},
-
- /**
- * Returns styles-string for svg-export
- * @param {Boolean} skipShadow a boolean to skip shadow filter output
- * @return {String}
- */
- getSvgStyles: function(skipShadow) {
- var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow);
- return svgStyle + ' white-space: pre;';
+ getDownCursorOffset: function(e, isRight) {
+ var selectionProp = this._getSelectionForOffset(e, isRight), cursorLocation = this.get2DCursorLocation(selectionProp), lineIndex = cursorLocation.lineIndex;
+ if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) {
+ return this._text.length - selectionProp;
+ }
+ var charIndex = cursorLocation.charIndex, widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor), textAfterCursor = this._textLines[lineIndex].slice(charIndex);
+ return textAfterCursor.length + indexOnOtherLine + 2;
},
- });
-})();
-/* _TO_SVG_END_ */
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = {});
-
- /**
- * Textbox class, based on IText, allows the user to resize the text rectangle
- * and wraps lines automatically. Textboxes have their Y scaling locked, the
- * user can only change width. Height is adjusted automatically based on the
- * wrapping of lines.
- * @class fabric.Textbox
- * @extends fabric.IText
- * @mixes fabric.Observable
- * @return {fabric.Textbox} thisArg
- * @see {@link fabric.Textbox#initialize} for constructor definition
- */
- fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, {
-
- /**
- * Type of an object
- * @type String
- * @default
- */
- type: 'textbox',
-
- /**
- * Minimum width of textbox, in pixels.
- * @type Number
- * @default
- */
- minWidth: 20,
-
- /**
- * Minimum calculated width of a textbox, in pixels.
- * fixed to 2 so that an empty textbox cannot go to 0
- * and is still selectable without text.
- * @type Number
- * @default
- */
- dynamicMinWidth: 2,
-
- /**
- * Cached array of text wrapping.
- * @type Array
- */
- __cachedLines: null,
-
- /**
- * Override standard Object class values
- */
- lockScalingFlip: true,
-
- /**
- * Override standard Object class values
- * Textbox needs this on false
- */
- noScaleCache: false,
-
- /**
- * Properties which when set cause object to change dimensions
- * @type Object
- * @private
- */
- _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'),
-
- /**
- * Constructor. Some scaling related property values are forced. Visibility
- * of controls is also fixed; only the rotation and width controls are
- * made available.
- * @param {String} text Text string
- * @param {Object} [options] Options object
- * @return {fabric.Textbox} thisArg
- */
- initialize: function(text, options) {
- this.callSuper('initialize', text, options);
+ _getSelectionForOffset: function(e, isRight) {
+ if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) {
+ return this.selectionEnd;
+ } else {
+ return this.selectionStart;
+ }
},
-
- /**
- * Unlike superclass's version of this function, Textbox does not update
- * its width.
- * @private
- * @override
- */
- initDimensions: function() {
- if (this.__skipDimension) {
- return;
- }
- this.isEditing && this.initDelayedCursor();
- this.clearContextTop();
- this._clearCache();
- // clear dynamicMinWidth as it will be different after we re-wrap line
- this.dynamicMinWidth = 0;
- // wrap lines
- this._styleMap = this._generateStyleMap(this._splitText());
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
- if (this.dynamicMinWidth > this.width) {
- this._set('width', this.dynamicMinWidth);
- }
- if (this.textAlign.indexOf('justify') !== -1) {
- // once text is measured we need to make space fatter to make justified text.
- this.enlargeSpaces();
- }
- // clear cache and re-calculate height
- this.height = this.calcTextHeight();
- this.saveState({ propertySet: '_dimensionAffectingProps' });
+ getUpCursorOffset: function(e, isRight) {
+ var selectionProp = this._getSelectionForOffset(e, isRight), cursorLocation = this.get2DCursorLocation(selectionProp), lineIndex = cursorLocation.lineIndex;
+ if (lineIndex === 0 || e.metaKey || e.keyCode === 33) {
+ return -selectionProp;
+ }
+ var charIndex = cursorLocation.charIndex, widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor), textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex);
+ return -this._textLines[lineIndex - 1].length + indexOnOtherLine - textBeforeCursor.length;
},
-
- /**
- * Generate an object that translates the style object so that it is
- * broken up by visual lines (new lines and automatic wrapping).
- * The original text styles object is broken up by actual lines (new lines only),
- * which is only sufficient for Text / IText
- * @private
- */
- _generateStyleMap: function(textInfo) {
- var realLineCount = 0,
- realLineCharCount = 0,
- charCount = 0,
- map = {};
-
- for (var i = 0; i < textInfo.graphemeLines.length; i++) {
- if (textInfo.graphemeText[charCount] === '\n' && i > 0) {
- realLineCharCount = 0;
- charCount++;
- realLineCount++;
+ _getIndexOnLine: function(lineIndex, width) {
+ var line = this._textLines[lineIndex], lineLeftOffset = this._getLineLeftOffset(lineIndex), widthOfCharsOnLine = lineLeftOffset, indexOnLine = 0, charWidth, foundMatch;
+ for (var j = 0, jlen = line.length; j < jlen; j++) {
+ charWidth = this.__charBounds[lineIndex][j].width;
+ widthOfCharsOnLine += charWidth;
+ if (widthOfCharsOnLine > width) {
+ foundMatch = true;
+ var leftEdge = widthOfCharsOnLine - charWidth, rightEdge = widthOfCharsOnLine, offsetFromLeftEdge = Math.abs(leftEdge - width), offsetFromRightEdge = Math.abs(rightEdge - width);
+ indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : j - 1;
+ break;
+ }
}
- else if (this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) {
- // this case deals with space's that are removed from end of lines when wrapping
- realLineCharCount++;
- charCount++;
+ if (!foundMatch) {
+ indexOnLine = line.length - 1;
}
-
- map[i] = { line: realLineCount, offset: realLineCharCount };
-
- charCount += textInfo.graphemeLines[i].length;
- realLineCharCount += textInfo.graphemeLines[i].length;
- }
-
- return map;
- },
-
- /**
- * Returns true if object has a style property or has it ina specified line
- * @param {Number} lineIndex
- * @return {Boolean}
- */
- styleHas: function(property, lineIndex) {
- if (this._styleMap && !this.isWrapping) {
- var map = this._styleMap[lineIndex];
- if (map) {
- lineIndex = map.line;
- }
- }
- return fabric.Text.prototype.styleHas.call(this, property, lineIndex);
+ return indexOnLine;
},
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @private
- */
- _getStyleDeclaration: function(lineIndex, charIndex) {
- if (this._styleMap && !this.isWrapping) {
- var map = this._styleMap[lineIndex];
- if (!map) {
- return null;
- }
- lineIndex = map.line;
- charIndex = map.offset + charIndex;
- }
- return this.callSuper('_getStyleDeclaration', lineIndex, charIndex);
+ moveCursorDown: function(e) {
+ if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
+ return;
+ }
+ this._moveCursorUpOrDown("Down", e);
},
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} style
- * @private
- */
- _setStyleDeclaration: function(lineIndex, charIndex, style) {
- var map = this._styleMap[lineIndex];
- lineIndex = map.line;
- charIndex = map.offset + charIndex;
-
- this.styles[lineIndex][charIndex] = style;
+ moveCursorUp: function(e) {
+ if (this.selectionStart === 0 && this.selectionEnd === 0) {
+ return;
+ }
+ this._moveCursorUpOrDown("Up", e);
+ },
+ _moveCursorUpOrDown: function(direction, e) {
+ var action = "get" + direction + "CursorOffset", offset = this[action](e, this._selectionDirection === "right");
+ if (e.shiftKey) {
+ this.moveCursorWithShift(offset);
+ } else {
+ this.moveCursorWithoutShift(offset);
+ }
+ if (offset !== 0) {
+ this.setSelectionInBoundaries();
+ this.abortCursorAnimation();
+ this._currentCursorOpacity = 1;
+ this.initDelayedCursor();
+ this._fireSelectionChanged();
+ this._updateTextarea();
+ }
+ },
+ moveCursorWithShift: function(offset) {
+ var newSelection = this._selectionDirection === "left" ? this.selectionStart + offset : this.selectionEnd + offset;
+ this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection);
+ return offset !== 0;
+ },
+ moveCursorWithoutShift: function(offset) {
+ if (offset < 0) {
+ this.selectionStart += offset;
+ this.selectionEnd = this.selectionStart;
+ } else {
+ this.selectionEnd += offset;
+ this.selectionStart = this.selectionEnd;
+ }
+ return offset !== 0;
+ },
+ moveCursorLeft: function(e) {
+ if (this.selectionStart === 0 && this.selectionEnd === 0) {
+ return;
+ }
+ this._moveCursorLeftOrRight("Left", e);
},
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @private
- */
- _deleteStyleDeclaration: function(lineIndex, charIndex) {
- var map = this._styleMap[lineIndex];
- lineIndex = map.line;
- charIndex = map.offset + charIndex;
-
- delete this.styles[lineIndex][charIndex];
+ _move: function(e, prop, direction) {
+ var newValue;
+ if (e.altKey) {
+ newValue = this["findWordBoundary" + direction](this[prop]);
+ } else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36) {
+ newValue = this["findLineBoundary" + direction](this[prop]);
+ } else {
+ this[prop] += direction === "Left" ? -1 : 1;
+ return true;
+ }
+ if (typeof newValue !== undefined && this[prop] !== newValue) {
+ this[prop] = newValue;
+ return true;
+ }
},
-
- /**
- * @param {Number} lineIndex
- * @private
- */
- _getLineStyle: function(lineIndex) {
- var map = this._styleMap[lineIndex];
- return this.styles[map.line];
+ _moveLeft: function(e, prop) {
+ return this._move(e, prop, "Left");
},
-
- /**
- * @param {Number} lineIndex
- * @param {Object} style
- * @private
- */
- _setLineStyle: function(lineIndex, style) {
- var map = this._styleMap[lineIndex];
- this.styles[map.line] = style;
+ _moveRight: function(e, prop) {
+ return this._move(e, prop, "Right");
},
-
- /**
- * @param {Number} lineIndex
- * @private
- */
- _deleteLineStyle: function(lineIndex) {
- var map = this._styleMap[lineIndex];
- delete this.styles[map.line];
+ moveCursorLeftWithoutShift: function(e) {
+ var change = true;
+ this._selectionDirection = "left";
+ if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) {
+ change = this._moveLeft(e, "selectionStart");
+ }
+ this.selectionEnd = this.selectionStart;
+ return change;
},
-
- /**
- * Wraps text using the 'width' property of Textbox. First this function
- * splits text on newlines, so we preserve newlines entered by the user.
- * Then it wraps each line using the width of the Textbox by calling
- * _wrapLine().
- * @param {Array} lines The string array of text that is split into lines
- * @param {Number} desiredWidth width you want to wrap to
- * @returns {Array} Array of lines
- */
- _wrapText: function(lines, desiredWidth) {
- var wrapped = [], i;
- this.isWrapping = true;
- for (i = 0; i < lines.length; i++) {
- wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth));
- }
- this.isWrapping = false;
- return wrapped;
+ moveCursorLeftWithShift: function(e) {
+ if (this._selectionDirection === "right" && this.selectionStart !== this.selectionEnd) {
+ return this._moveLeft(e, "selectionEnd");
+ } else if (this.selectionStart !== 0) {
+ this._selectionDirection = "left";
+ return this._moveLeft(e, "selectionStart");
+ }
},
-
- /**
- * Helper function to measure a string of text, given its lineIndex and charIndex offset
- * it gets called when charBounds are not available yet.
- * @param {CanvasRenderingContext2D} ctx
- * @param {String} text
- * @param {number} lineIndex
- * @param {number} charOffset
- * @returns {number}
- * @private
- */
- _measureWord: function(word, lineIndex, charOffset) {
- var width = 0, prevGrapheme, skipLeft = true;
- charOffset = charOffset || 0;
- for (var i = 0, len = word.length; i < len; i++) {
- var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft);
- width += box.kernedWidth;
- prevGrapheme = word[i];
- }
- return width;
+ moveCursorRight: function(e) {
+ if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
+ return;
+ }
+ this._moveCursorLeftOrRight("Right", e);
},
-
- /**
- * Wraps a line of text using the width of the Textbox and a context.
- * @param {Array} line The grapheme array that represent the line
- * @param {Number} lineIndex
- * @param {Number} desiredWidth width you want to wrap the line to
- * @returns {Array} Array of line(s) into which the given text is wrapped
- * to.
- */
- _wrapLine: function(_line, lineIndex, desiredWidth) {
- var lineWidth = 0,
- graphemeLines = [],
- line = [],
- // spaces in different languges?
- words = _line.split(this._reSpaceAndTab),
- word = '',
- offset = 0,
- infix = ' ',
- wordWidth = 0,
- infixWidth = 0,
- largestWordWidth = 0,
- lineJustStarted = true,
- additionalSpace = this._getWidthOfCharSpacing();
- for (var i = 0; i < words.length; i++) {
- // i would avoid resplitting the graphemes
- word = fabric.util.string.graphemeSplit(words[i]);
- wordWidth = this._measureWord(word, lineIndex, offset);
- offset += word.length;
-
- lineWidth += infixWidth + wordWidth - additionalSpace;
-
- if (lineWidth >= desiredWidth && !lineJustStarted) {
- graphemeLines.push(line);
- line = [];
- lineWidth = wordWidth;
- lineJustStarted = true;
+ _moveCursorLeftOrRight: function(direction, e) {
+ var actionName = "moveCursor" + direction + "With";
+ this._currentCursorOpacity = 1;
+ if (e.shiftKey) {
+ actionName += "Shift";
+ } else {
+ actionName += "outShift";
}
-
- if (!lineJustStarted) {
- line.push(infix);
+ if (this[actionName](e)) {
+ this.abortCursorAnimation();
+ this.initDelayedCursor();
+ this._fireSelectionChanged();
+ this._updateTextarea();
}
- line = line.concat(word);
-
- infixWidth = this._measureWord([infix], lineIndex, offset);
- offset++;
- lineJustStarted = false;
- // keep track of largest word
- if (wordWidth > largestWordWidth) {
- largestWordWidth = wordWidth;
- }
- }
-
- i && graphemeLines.push(line);
-
- if (largestWordWidth > this.dynamicMinWidth) {
- this.dynamicMinWidth = largestWordWidth - additionalSpace;
- }
-
- return graphemeLines;
},
-
- /**
- * Detect if the text line is ended with an hard break
- * text and itext do not have wrapping, return false
- * @param {Number} lineIndex text to split
- * @return {Boolean}
- */
- isEndOfWrapping: function(lineIndex) {
- if (!this._styleMap[lineIndex + 1]) {
- // is last line, return true;
- return true;
- }
- if (this._styleMap[lineIndex + 1].line !== this._styleMap[lineIndex].line) {
- // this is last line before a line break, return true;
- return true;
- }
- return false;
+ moveCursorRightWithShift: function(e) {
+ if (this._selectionDirection === "left" && this.selectionStart !== this.selectionEnd) {
+ return this._moveRight(e, "selectionStart");
+ } else if (this.selectionEnd !== this._text.length) {
+ this._selectionDirection = "right";
+ return this._moveRight(e, "selectionEnd");
+ }
},
-
- /**
- * Gets lines of text to render in the Textbox. This function calculates
- * text wrapping on the fly every time it is called.
- * @param {String} text text to split
- * @returns {Array} Array of lines in the Textbox.
- * @override
- */
- _splitTextIntoLines: function(text) {
- var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text),
- graphemeLines = this._wrapText(newText.lines, this.width),
- lines = new Array(graphemeLines.length);
-
- for (var i = 0; i < graphemeLines.length; i++) {
- lines[i] = graphemeLines[i].join('');
- }
- newText.lines = lines;
- newText.graphemeLines = graphemeLines;
- return newText;
+ moveCursorRightWithoutShift: function(e) {
+ var changed = true;
+ this._selectionDirection = "right";
+ if (this.selectionStart === this.selectionEnd) {
+ changed = this._moveRight(e, "selectionStart");
+ this.selectionEnd = this.selectionStart;
+ } else {
+ this.selectionStart = this.selectionEnd;
+ }
+ return changed;
},
-
- getMinWidth: function() {
- return Math.max(this.minWidth, this.dynamicMinWidth);
+ removeChars: function(start, end) {
+ if (typeof end === "undefined") {
+ end = start + 1;
+ }
+ this.removeStyleFromTo(start, end);
+ this._text.splice(start, end - start);
+ this.text = this._text.join("");
+ this.set("dirty", true);
+ if (this._shouldClearDimensionCache()) {
+ this.initDimensions();
+ this.setCoords();
+ }
+ this._removeExtraneousStyles();
},
-
- /**
- * Returns object representation of an instance
- * @method toObject
- * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
- * @return {Object} object representation of an instance
- */
- toObject: function(propertiesToInclude) {
- return this.callSuper('toObject', ['minWidth'].concat(propertiesToInclude));
+ insertChars: function(text, style, start, end) {
+ if (typeof end === "undefined") {
+ end = start;
+ }
+ if (end > start) {
+ this.removeStyleFromTo(start, end);
+ }
+ var graphemes = fabric.util.string.graphemeSplit(text);
+ this.insertNewStyleBlock(graphemes, start, style);
+ this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end));
+ this.text = this._text.join("");
+ this.set("dirty", true);
+ if (this._shouldClearDimensionCache()) {
+ this.initDimensions();
+ this.setCoords();
+ }
+ this._removeExtraneousStyles();
}
- });
-
- /**
- * Returns fabric.Textbox instance from an object representation
- * @static
- * @memberOf fabric.Textbox
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created
- */
- fabric.Textbox.fromObject = function(object, callback) {
- return fabric.Object._fromObject('Textbox', object, callback, 'text');
- };
-})(typeof exports !== 'undefined' ? exports : this);
-
+});
(function() {
+ var toFixed = fabric.util.toFixed;
+ fabric.util.object.extend(fabric.Text.prototype, {
+ toSVG: function(reviver) {
+ var markup = this._createBaseSVGMarkup(), offsets = this._getSVGLeftTopOffsets(), textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft);
+ this._wrapSVGTextAndBg(markup, textAndBg);
+ return reviver ? reviver(markup.join("")) : markup.join("");
+ },
+ _getSVGLeftTopOffsets: function() {
+ return {
+ textLeft: -this.width / 2,
+ textTop: -this.height / 2,
+ lineTop: this.getHeightOfLine(0)
+ };
+ },
+ _wrapSVGTextAndBg: function(markup, textAndBg) {
+ var noShadow = true, filter = this.getSvgFilter(), style = filter === "" ? "" : ' style="' + filter + '"', textDecoration = this.getSvgTextDecoration(this);
+ markup.push("\t\n", textAndBg.textBgRects.join(""), '\t\t", textAndBg.textSpans.join(""), "\n", "\t\n");
+ },
+ _getSVGTextAndBg: function(textTopOffset, textLeftOffset) {
+ var textSpans = [], textBgRects = [], height = textTopOffset, lineOffset;
+ this._setSVGBg(textBgRects);
+ for (var i = 0, len = this._textLines.length; i < len; i++) {
+ lineOffset = this._getLineLeftOffset(i);
+ if (this.textBackgroundColor || this.styleHas("textBackgroundColor", i)) {
+ this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height);
+ }
+ this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height);
+ height += this.getHeightOfLine(i);
+ }
+ return {
+ textSpans: textSpans,
+ textBgRects: textBgRects
+ };
+ },
+ _createTextCharSpan: function(_char, styleDecl, left, top) {
+ var styleProps = this.getSvgSpanStyles(styleDecl, _char !== _char.trim()), fillStyles = styleProps ? 'style="' + styleProps + '"' : "", NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
+ return [ '", fabric.util.string.escapeXml(_char), "" ].join("");
+ },
+ _setSVGTextLineText: function(textSpans, lineIndex, textLeftOffset, textTopOffset) {
+ var lineHeight = this.getHeightOfLine(lineIndex), isJustify = this.textAlign.indexOf("justify") !== -1, actualStyle, nextStyle, charsToRender = "", charBox, style, boxWidth = 0, line = this._textLines[lineIndex], timeToRender;
+ textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight;
+ for (var i = 0, len = line.length - 1; i <= len; i++) {
+ timeToRender = i === len || this.charSpacing;
+ charsToRender += line[i];
+ charBox = this.__charBounds[lineIndex][i];
+ if (boxWidth === 0) {
+ textLeftOffset += charBox.kernedWidth - charBox.width;
+ boxWidth += charBox.width;
+ } else {
+ boxWidth += charBox.kernedWidth;
+ }
+ if (isJustify && !timeToRender) {
+ if (this._reSpaceAndTab.test(line[i])) {
+ timeToRender = true;
+ }
+ }
+ if (!timeToRender) {
+ actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
+ nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
+ timeToRender = this._hasStyleChangedForSvg(actualStyle, nextStyle);
+ }
+ if (timeToRender) {
+ style = this._getStyleDeclaration(lineIndex, i) || {};
+ textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset));
+ charsToRender = "";
+ actualStyle = nextStyle;
+ textLeftOffset += boxWidth;
+ boxWidth = 0;
+ }
+ }
+ },
+ _pushTextBgRect: function(textBgRects, color, left, top, width, height) {
+ var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
+ textBgRects.push("\t\t\n');
+ },
+ _setSVGTextLineBg: function(textBgRects, i, leftOffset, textTopOffset) {
+ var line = this._textLines[i], heightOfLine = this.getHeightOfLine(i) / this.lineHeight, boxWidth = 0, boxStart = 0, charBox, currentColor, lastColor = this.getValueOfPropertyAt(i, 0, "textBackgroundColor");
+ for (var j = 0, jlen = line.length; j < jlen; j++) {
+ charBox = this.__charBounds[i][j];
+ currentColor = this.getValueOfPropertyAt(i, j, "textBackgroundColor");
+ if (currentColor !== lastColor) {
+ lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart, textTopOffset, boxWidth, heightOfLine);
+ boxStart = charBox.left;
+ boxWidth = charBox.width;
+ lastColor = currentColor;
+ } else {
+ boxWidth += charBox.kernedWidth;
+ }
+ }
+ currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart, textTopOffset, boxWidth, heightOfLine);
+ },
+ _getFillAttributes: function(value) {
+ var fillColor = value && typeof value === "string" ? new fabric.Color(value) : "";
+ if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) {
+ return 'fill="' + value + '"';
+ }
+ return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"';
+ },
+ _getSVGLineTopOffset: function(lineIndex) {
+ var lineTopOffset = 0, lastHeight = 0;
+ for (var j = 0; j < lineIndex; j++) {
+ lineTopOffset += this.getHeightOfLine(j);
+ }
+ lastHeight = this.getHeightOfLine(j);
+ return {
+ lineTop: lineTopOffset,
+ offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult)
+ };
+ },
+ getSvgStyles: function(skipShadow) {
+ var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow);
+ return svgStyle + " white-space: pre;";
+ }
+ });
+})();
- /**
- * Override _setObjectScale and add Textbox specific resizing behavior. Resizing
- * a Textbox doesn't scale text, it only changes width and makes text wrap automatically.
- */
- var setObjectScaleOverridden = fabric.Canvas.prototype._setObjectScale;
-
- fabric.Canvas.prototype._setObjectScale = function(localMouse, transform,
- lockScalingX, lockScalingY, by, lockScalingFlip, _dim) {
-
- var t = transform.target;
- if (by === 'x' && t instanceof fabric.Textbox) {
- var tw = t._getTransformedDimensions().x;
- var w = t.width * (localMouse.x / tw);
- if (w >= t.getMinWidth()) {
- t.set('width', w);
- return true;
- }
- }
- else {
- return setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform,
- lockScalingX, lockScalingY, by, lockScalingFlip, _dim);
- }
- };
-
- fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.IText.prototype */ {
- /**
- * @private
- */
- _removeExtraneousStyles: function() {
- for (var prop in this._styleMap) {
- if (!this._textLines[prop]) {
- delete this.styles[this._styleMap[prop].line];
- }
- }
- },
+(function(global) {
+ "use strict";
+ var fabric = global.fabric || (global.fabric = {});
+ fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, {
+ type: "textbox",
+ minWidth: 20,
+ dynamicMinWidth: 2,
+ __cachedLines: null,
+ lockScalingFlip: true,
+ noScaleCache: false,
+ _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat("width"),
+ initDimensions: function() {
+ if (this.__skipDimension) {
+ return;
+ }
+ this.isEditing && this.initDelayedCursor();
+ this.clearContextTop();
+ this._clearCache();
+ this.dynamicMinWidth = 0;
+ this._styleMap = this._generateStyleMap(this._splitText());
+ if (this.dynamicMinWidth > this.width) {
+ this._set("width", this.dynamicMinWidth);
+ }
+ if (this.textAlign.indexOf("justify") !== -1) {
+ this.enlargeSpaces();
+ }
+ this.height = this.calcTextHeight();
+ this.saveState({
+ propertySet: "_dimensionAffectingProps"
+ });
+ },
+ _generateStyleMap: function(textInfo) {
+ var realLineCount = 0, realLineCharCount = 0, charCount = 0, map = {};
+ for (var i = 0; i < textInfo.graphemeLines.length; i++) {
+ if (textInfo.graphemeText[charCount] === "\n" && i > 0) {
+ realLineCharCount = 0;
+ charCount++;
+ realLineCount++;
+ } else if (this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) {
+ realLineCharCount++;
+ charCount++;
+ }
+ map[i] = {
+ line: realLineCount,
+ offset: realLineCharCount
+ };
+ charCount += textInfo.graphemeLines[i].length;
+ realLineCharCount += textInfo.graphemeLines[i].length;
+ }
+ return map;
+ },
+ styleHas: function(property, lineIndex) {
+ if (this._styleMap && !this.isWrapping) {
+ var map = this._styleMap[lineIndex];
+ if (map) {
+ lineIndex = map.line;
+ }
+ }
+ return fabric.Text.prototype.styleHas.call(this, property, lineIndex);
+ },
+ isEmptyStyles: function(lineIndex) {
+ var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false;
+ var map = this._styleMap[lineIndex];
+ var mapNextLine = this._styleMap[lineIndex + 1];
+ if (map) {
+ lineIndex = map.line;
+ offset = map.offset;
+ }
+ if (mapNextLine) {
+ nextLineIndex = mapNextLine.line;
+ shouldLimit = nextLineIndex === lineIndex;
+ nextOffset = mapNextLine.offset;
+ }
+ obj = typeof lineIndex === "undefined" ? this.styles : {
+ line: this.styles[lineIndex]
+ };
+ for (var p1 in obj) {
+ for (var p2 in obj[p1]) {
+ if (p2 >= offset && (!shouldLimit || p2 < nextOffset)) {
+ for (var p3 in obj[p1][p2]) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ },
+ _getStyleDeclaration: function(lineIndex, charIndex) {
+ if (this._styleMap && !this.isWrapping) {
+ var map = this._styleMap[lineIndex];
+ if (!map) {
+ return null;
+ }
+ lineIndex = map.line;
+ charIndex = map.offset + charIndex;
+ }
+ return this.callSuper("_getStyleDeclaration", lineIndex, charIndex);
+ },
+ _setStyleDeclaration: function(lineIndex, charIndex, style) {
+ var map = this._styleMap[lineIndex];
+ lineIndex = map.line;
+ charIndex = map.offset + charIndex;
+ this.styles[lineIndex][charIndex] = style;
+ },
+ _deleteStyleDeclaration: function(lineIndex, charIndex) {
+ var map = this._styleMap[lineIndex];
+ lineIndex = map.line;
+ charIndex = map.offset + charIndex;
+ delete this.styles[lineIndex][charIndex];
+ },
+ _getLineStyle: function(lineIndex) {
+ var map = this._styleMap[lineIndex];
+ return this.styles[map.line];
+ },
+ _setLineStyle: function(lineIndex, style) {
+ var map = this._styleMap[lineIndex];
+ this.styles[map.line] = style;
+ },
+ _deleteLineStyle: function(lineIndex) {
+ var map = this._styleMap[lineIndex];
+ delete this.styles[map.line];
+ },
+ _wrapText: function(lines, desiredWidth) {
+ var wrapped = [], i;
+ this.isWrapping = true;
+ for (i = 0; i < lines.length; i++) {
+ wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth));
+ }
+ this.isWrapping = false;
+ return wrapped;
+ },
+ _measureWord: function(word, lineIndex, charOffset) {
+ var width = 0, prevGrapheme, skipLeft = true;
+ charOffset = charOffset || 0;
+ for (var i = 0, len = word.length; i < len; i++) {
+ var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft);
+ width += box.kernedWidth;
+ prevGrapheme = word[i];
+ }
+ return width;
+ },
+ _wrapLine: function(_line, lineIndex, desiredWidth) {
+ var lineWidth = 0, graphemeLines = [], line = [], words = _line.split(this._reSpaceAndTab), word = "", offset = 0, infix = " ", wordWidth = 0, infixWidth = 0, largestWordWidth = 0, lineJustStarted = true, additionalSpace = this._getWidthOfCharSpacing();
+ for (var i = 0; i < words.length; i++) {
+ word = fabric.util.string.graphemeSplit(words[i]);
+ wordWidth = this._measureWord(word, lineIndex, offset);
+ offset += word.length;
+ lineWidth += infixWidth + wordWidth - additionalSpace;
+ if (lineWidth >= desiredWidth && !lineJustStarted) {
+ graphemeLines.push(line);
+ line = [];
+ lineWidth = wordWidth;
+ lineJustStarted = true;
+ }
+ if (!lineJustStarted) {
+ line.push(infix);
+ }
+ line = line.concat(word);
+ infixWidth = this._measureWord([ infix ], lineIndex, offset);
+ offset++;
+ lineJustStarted = false;
+ if (wordWidth > largestWordWidth) {
+ largestWordWidth = wordWidth;
+ }
+ }
+ i && graphemeLines.push(line);
+ if (largestWordWidth > this.dynamicMinWidth) {
+ this.dynamicMinWidth = largestWordWidth - additionalSpace;
+ }
+ return graphemeLines;
+ },
+ isEndOfWrapping: function(lineIndex) {
+ if (!this._styleMap[lineIndex + 1]) {
+ return true;
+ }
+ if (this._styleMap[lineIndex + 1].line !== this._styleMap[lineIndex].line) {
+ return true;
+ }
+ return false;
+ },
+ _splitTextIntoLines: function(text) {
+ var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text), graphemeLines = this._wrapText(newText.lines, this.width), lines = new Array(graphemeLines.length);
+ for (var i = 0; i < graphemeLines.length; i++) {
+ lines[i] = graphemeLines[i].join("");
+ }
+ newText.lines = lines;
+ newText.graphemeLines = graphemeLines;
+ return newText;
+ },
+ getMinWidth: function() {
+ return Math.max(this.minWidth, this.dynamicMinWidth);
+ },
+ toObject: function(propertiesToInclude) {
+ return this.callSuper("toObject", [ "minWidth" ].concat(propertiesToInclude));
+ }
+ });
+ fabric.Textbox.fromObject = function(object, callback) {
+ return fabric.Object._fromObject("Textbox", object, callback, "text");
+ };
+})(typeof exports !== "undefined" ? exports : this);
- });
+(function() {
+ var setObjectScaleOverridden = fabric.Canvas.prototype._setObjectScale;
+ fabric.Canvas.prototype._setObjectScale = function(localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip, _dim) {
+ var t = transform.target;
+ if (by === "x" && t instanceof fabric.Textbox) {
+ var tw = t._getTransformedDimensions().x;
+ var w = t.width * (localMouse.x / tw);
+ if (w >= t.getMinWidth()) {
+ t.set("width", w);
+ return true;
+ }
+ } else {
+ return setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip, _dim);
+ }
+ };
+ fabric.util.object.extend(fabric.Textbox.prototype, {
+ _removeExtraneousStyles: function() {
+ for (var prop in this._styleMap) {
+ if (!this._textLines[prop]) {
+ delete this.styles[this._styleMap[prop].line];
+ }
+ }
+ }
+ });
})();
+if (typeof define === "function" && define.amd) {
+ define([], function() {
+ return fabric;
+ });
+}
\ No newline at end of file
diff --git a/dist/fabric.min.js b/dist/fabric.min.js
index 91ba733a0df..3008839d228 100644
--- a/dist/fabric.min.js
+++ b/dist/fabric.min.js
@@ -1 +1 @@
-function resizeCanvasIfNeeded(t){var e=t.targetCanvas,i=e.width,r=e.height,n=t.destinationWidth,s=t.destinationHeight;i===n&&r===s||(e.width=n,e.height=s)}function copyGLTo2DDrawImage(t,e){var i=t.canvas,r=e.targetCanvas,n=r.getContext("2d");n.translate(0,r.height),n.scale(1,-1);var s=i.height-r.height;n.drawImage(i,0,s,r.width,r.height,0,0,r.width,r.height)}function copyGLTo2DPutImageData(t,e){var i=e.targetCanvas.getContext("2d"),r=e.destinationWidth,n=e.destinationHeight,s=r*n*4,o=new Uint8Array(this.imageBuffer,0,s),a=new Uint8ClampedArray(this.imageBuffer,0,s);t.readPixels(0,0,r,n,t.RGBA,t.UNSIGNED_BYTE,o);var h=new ImageData(a,r,n);i.putImageData(h,0,0)}var fabric=fabric||{version:"2.0.3"};"undefined"!=typeof exports&&(exports.fabric=fabric),"undefined"!=typeof document&&"undefined"!=typeof window?(fabric.document=document,fabric.window=window):(fabric.document=require("jsdom").jsdom(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"),{features:{FetchExternalResources:["img"]}}),fabric.jsdomImplForWrapper=require("jsdom/lib/jsdom/living/generated/utils").implForWrapper,fabric.nodeCanvas=require("jsdom/lib/jsdom/utils").Canvas,fabric.window=fabric.document.defaultView,DOMParser=require("xmldom").DOMParser),fabric.isTouchSupported="ontouchstart"in fabric.window,fabric.isLikelyNode="undefined"!=typeof Buffer&&"undefined"==typeof window,fabric.SHARED_ATTRIBUTES=["display","transform","fill","fill-opacity","fill-rule","opacity","stroke","stroke-dasharray","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","id","paint-order","instantiated_by_use"],fabric.DPI=96,fabric.reNum="(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:e[-+]?\\d+)?)",fabric.fontPaths={},fabric.iMatrix=[1,0,0,1,0,0],fabric.canvasModule="canvas",fabric.perfLimitSizeTotal=2097152,fabric.maxCacheSideLimit=4096,fabric.minCacheSideLimit=256,fabric.charWidthsCache={},fabric.textureSize=2048,fabric.enableGLFiltering=!0,fabric.devicePixelRatio=fabric.window.devicePixelRatio||fabric.window.webkitDevicePixelRatio||fabric.window.mozDevicePixelRatio||1,fabric.browserShadowBlurConstant=1,fabric.initFilterBackend=function(){return fabric.enableGLFiltering&&fabric.isWebglSupported&&fabric.isWebglSupported(fabric.textureSize)?(console.log("max texture size: "+fabric.maxTextureSize),new fabric.WebglFilterBackend({tileSize:fabric.textureSize})):fabric.Canvas2dFilterBackend?new fabric.Canvas2dFilterBackend:void 0},"undefined"!=typeof document&&"undefined"!=typeof window&&(window.fabric=fabric),function(){function t(t,e){if(this.__eventListeners[t]){var i=this.__eventListeners[t];e?i[i.indexOf(e)]=!1:fabric.util.array.fill(i,!1)}}function e(t,e){if(this.__eventListeners||(this.__eventListeners={}),1===arguments.length)for(var i in t)this.on(i,t[i]);else this.__eventListeners[t]||(this.__eventListeners[t]=[]),this.__eventListeners[t].push(e);return this}function i(e,i){if(this.__eventListeners){if(0===arguments.length)for(e in this.__eventListeners)t.call(this,e);else if(1===arguments.length&&"object"==typeof arguments[0])for(var r in e)t.call(this,r,e[r]);else t.call(this,e,i);return this}}function r(t,e){if(this.__eventListeners){var i=this.__eventListeners[t];if(i){for(var r=0,n=i.length;r-1},complexity:function(){return this.getObjects().reduce(function(t,e){return t+=e.complexity?e.complexity():0},0)}},fabric.CommonMethods={_setOptions:function(t){for(var e in t)this.set(e,t[e])},_initGradient:function(t,e){!t||!t.colorStops||t instanceof fabric.Gradient||this.set(e,new fabric.Gradient(t))},_initPattern:function(t,e,i){!t||!t.source||t instanceof fabric.Pattern?i&&i():this.set(e,new fabric.Pattern(t,i))},_initClipping:function(t){if(t.clipTo&&"string"==typeof t.clipTo){var e=fabric.util.getFunctionBody(t.clipTo);void 0!==e&&(this.clipTo=new Function("ctx",e))}},_setObject:function(t){for(var e in t)this._set(e,t[e])},set:function(t,e){return"object"==typeof t?this._setObject(t):"function"==typeof e&&"clipTo"!==t?this._set(t,e(this.get(t))):this._set(t,e),this},_set:function(t,e){this[t]=e},toggle:function(t){var e=this.get(t);return"boolean"==typeof e&&this.set(t,!e),this},get:function(t){return this[t]}},function(t){var e=Math.sqrt,i=Math.atan2,r=Math.pow,n=Math.abs,s=Math.PI/180,o=Math.PI/2;fabric.util={cos:function(t){if(0===t)return 1;t<0&&(t=-t);switch(t/o){case 1:case 3:return 0;case 2:return-1}return Math.cos(t)},sin:function(t){if(0===t)return 0;var e=1;switch(t<0&&(e=-1),t/o){case 1:return e;case 2:return 0;case 3:return-e}return Math.sin(t)},removeFromArray:function(t,e){var i=t.indexOf(e);return-1!==i&&t.splice(i,1),t},getRandomInt:function(t,e){return Math.floor(Math.random()*(e-t+1))+t},degreesToRadians:function(t){return t*s},radiansToDegrees:function(t){return t/s},rotatePoint:function(t,e,i){t.subtractEquals(e);var r=fabric.util.rotateVector(t,i);return new fabric.Point(r.x,r.y).addEquals(e)},rotateVector:function(t,e){var i=fabric.util.sin(e),r=fabric.util.cos(e);return{x:t.x*r-t.y*i,y:t.x*i+t.y*r}},transformPoint:function(t,e,i){return i?new fabric.Point(e[0]*t.x+e[2]*t.y,e[1]*t.x+e[3]*t.y):new fabric.Point(e[0]*t.x+e[2]*t.y+e[4],e[1]*t.x+e[3]*t.y+e[5])},makeBoundingBoxFromPoints:function(t){var e=[t[0].x,t[1].x,t[2].x,t[3].x],i=fabric.util.array.min(e),r=fabric.util.array.max(e)-i,n=[t[0].y,t[1].y,t[2].y,t[3].y],s=fabric.util.array.min(n);return{left:i,top:s,width:r,height:fabric.util.array.max(n)-s}},invertTransform:function(t){var e=1/(t[0]*t[3]-t[1]*t[2]),i=[e*t[3],-e*t[1],-e*t[2],e*t[0]],r=fabric.util.transformPoint({x:t[4],y:t[5]},i,!0);return i[4]=-r.x,i[5]=-r.y,i},toFixed:function(t,e){return parseFloat(Number(t).toFixed(e))},parseUnit:function(t,e){var i=/\D{0,2}$/.exec(t),r=parseFloat(t);switch(e||(e=fabric.Text.DEFAULT_SVG_FONT_SIZE),i[0]){case"mm":return r*fabric.DPI/25.4;case"cm":return r*fabric.DPI/2.54;case"in":return r*fabric.DPI;case"pt":return r*fabric.DPI/72;case"pc":return r*fabric.DPI/72*12;case"em":return r*e;default:return r}},falseFunction:function(){return!1},getKlass:function(t,e){return t=fabric.util.string.camelize(t.charAt(0).toUpperCase()+t.slice(1)),fabric.util.resolveNamespace(e)[t]},getSvgAttributes:function(t){var e=["instantiated_by_use","style","id","class"];switch(t){case"linearGradient":e=e.concat(["x1","y1","x2","y2","gradientUnits","gradientTransform"]);break;case"radialGradient":e=e.concat(["gradientUnits","gradientTransform","cx","cy","r","fx","fy","fr"]);break;case"stop":e=e.concat(["offset","stop-color","stop-opacity"])}return e},resolveNamespace:function(e){if(!e)return fabric;var i,r=e.split("."),n=r.length,s=t||fabric.window;for(i=0;ir;)(r+=a[d++%f])>l&&(r=l),t[g?"lineTo":"moveTo"](r,0),g=!g;t.restore()},createCanvasElement:function(){return fabric.document.createElement("canvas")},createImage:function(){return fabric.document.createElement("img")},clipContext:function(t,e){e.save(),e.beginPath(),t.clipTo(e),e.clip()},multiplyTransformMatrices:function(t,e,i){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],i?0:t[0]*e[4]+t[2]*e[5]+t[4],i?0:t[1]*e[4]+t[3]*e[5]+t[5]]},qrDecompose:function(t){var n=i(t[1],t[0]),o=r(t[0],2)+r(t[1],2),a=e(o),h=(t[0]*t[3]-t[2]*t[1])/a,c=i(t[0]*t[2]+t[1]*t[3],o);return{angle:n/s,scaleX:a,scaleY:h,skewX:c/s,skewY:0,translateX:t[4],translateY:t[5]}},customTransformMatrix:function(t,e,i){var r=[1,0,n(Math.tan(i*s)),1],o=[n(t),0,0,n(e)];return fabric.util.multiplyTransformMatrices(o,r,!0)},resetObjectTransform:function(t){t.scaleX=1,t.scaleY=1,t.skewX=0,t.skewY=0,t.flipX=!1,t.flipY=!1,t.rotate(0)},getFunctionBody:function(t){return(String(t).match(/function[^{]*\{([\s\S]*)\}/)||{})[1]},isTransparent:function(t,e,i,r){r>0&&(e>r?e-=r:e=0,i>r?i-=r:i=0);var n,s,o=!0,a=t.getImageData(e,i,2*r||1,2*r||1),h=a.data.length;for(n=3;n0?P-=2*f:1===c&&P<0&&(P+=2*f);for(var M=Math.ceil(Math.abs(P/f*2)),F=[],I=P/M,L=8/3*Math.sin(I/4)*Math.sin(I/4)/Math.sin(I/2),R=A+I,B=0;B=n?s-n:2*Math.PI-(n-s)}function i(t,e,i,r,n,a,h,c){var l=o.call(arguments);if(s[l])return s[l];var u,f,d,g,p,v,m,b,_=Math.sqrt,y=Math.min,x=Math.max,C=Math.abs,S=[],w=[[],[]];f=6*t-12*i+6*n,u=-3*t+9*i-9*n+3*h,d=3*i-3*t;for(var T=0;T<2;++T)if(T>0&&(f=6*e-12*r+6*a,u=-3*e+9*r-9*a+3*c,d=3*r-3*e),C(u)<1e-12){if(C(f)<1e-12)continue;0<(g=-d/f)&&g<1&&S.push(g)}else(m=f*f-4*d*u)<0||(0<(p=(-f+(b=_(m)))/(2*u))&&p<1&&S.push(p),0<(v=(-f-b)/(2*u))&&v<1&&S.push(v));for(var O,k,D,j=S.length,E=j;j--;)O=(D=1-(g=S[j]))*D*D*t+3*D*D*g*i+3*D*g*g*n+g*g*g*h,w[0][j]=O,k=D*D*D*e+3*D*D*g*r+3*D*g*g*a+g*g*g*c,w[1][j]=k;w[0][E]=t,w[1][E]=e,w[0][E+1]=h,w[1][E+1]=c;var A=[{x:y.apply(null,w[0]),y:y.apply(null,w[1])},{x:x.apply(null,w[0]),y:x.apply(null,w[1])}];return s[l]=A,A}var r={},n={},s={},o=Array.prototype.join;fabric.util.drawArc=function(e,i,r,n){for(var s=n[0],o=n[1],a=n[2],h=n[3],c=n[4],l=[[],[],[],[]],u=t(n[5]-i,n[6]-r,s,o,h,c,a),f=0,d=u.length;f=e})}}}(),function(){function t(e,i,r){if(r)if(!fabric.isLikelyNode&&i instanceof Element)e=i;else if(i instanceof Array){e=[];for(var n=0,s=i.length;n/g,">")},graphemeSplit:function(t){var e,i=0,r=[];for(i=0,e;i57343)return t.charAt(e);if(55296<=i&&i<=56319){if(t.length<=e+1)throw"High surrogate without following low surrogate";var r=t.charCodeAt(e+1);if(56320>r||r>57343)throw"High surrogate without following low surrogate";return t.charAt(e)+t.charAt(e+1)}if(0===e)throw"Low surrogate without preceding high surrogate";var n=t.charCodeAt(e-1);if(55296>n||n>56319)throw"Low surrogate without preceding high surrogate";return!1}(t,i))&&r.push(e);return r}}}(),function(){function t(){}function e(t){for(var e=null,r=this;r.constructor.superclass;){var n=r.constructor.superclass.prototype[t];if(r[t]!==n){e=n;break}r=r.constructor.superclass.prototype}return e?arguments.length>1?e.apply(this,i.call(arguments,1)):e.call(this):console.log("tried to callSuper "+t+", method not found in prototype chain",this)}var i=Array.prototype.slice,r=function(){},n=function(){for(var t in{toString:1})if("toString"===t)return!1;return!0}(),s=function(t,e,i){for(var r in e)r in t.prototype&&"function"==typeof t.prototype[r]&&(e[r]+"").indexOf("callSuper")>-1?t.prototype[r]=function(t){return function(){var r=this.constructor.superclass;this.constructor.superclass=i;var n=e[t].apply(this,arguments);if(this.constructor.superclass=r,"initialize"!==t)return n}}(r):t.prototype[r]=e[r],n&&(e.toString!==Object.prototype.toString&&(t.prototype.toString=e.toString),e.valueOf!==Object.prototype.valueOf&&(t.prototype.valueOf=e.valueOf))};fabric.util.createClass=function(){function n(){this.initialize.apply(this,arguments)}var o=null,a=i.call(arguments,0);"function"==typeof a[0]&&(o=a.shift()),n.superclass=o,n.subclasses=[],o&&(t.prototype=o.prototype,n.prototype=new t,o.subclasses.push(n));for(var h=0,c=a.length;h=.9999?"":"alpha(opacity="+100*e+")",i.filter=i.filter.replace(r,e)):i.filter+=" alpha(opacity="+100*e+")",t}),fabric.util.setStyle=function(t,e){var i=t.style;if(!i)return t;if("string"==typeof e)return t.style.cssText+=";"+e,e.indexOf("opacity")>-1?n(t,e.match(/opacity:\s*(\d?\.?\d*)/)[1]):t;for(var r in e)"opacity"===r?n(t,e[r]):i["float"===r||"cssFloat"===r?void 0===i.styleFloat?"cssFloat":"styleFloat":r]=e[r];return t}}(),function(){function t(t,e){var i=fabric.document.createElement(t);for(var r in e)"class"===r?i.className=e[r]:"for"===r?i.htmlFor=e[r]:i.setAttribute(r,e[r]);return i}function e(t){for(var e=0,i=0,r=fabric.document.documentElement,n=fabric.document.body||{scrollLeft:0,scrollTop:0};t&&(t.parentNode||t.host)&&((t=t.parentNode||t.host)===fabric.document?(e=n.scrollLeft||r.scrollLeft||0,i=n.scrollTop||r.scrollTop||0):(e+=t.scrollLeft||0,i+=t.scrollTop||0),1!==t.nodeType||"fixed"!==t.style.position););return{left:e,top:i}}var i,r=Array.prototype.slice,n=function(t){return r.call(t,0)};try{i=n(fabric.document.childNodes)instanceof Array}catch(t){}i||(n=function(t){for(var e=new Array(t.length),i=t.length;i--;)e[i]=t[i];return e});var s;s=fabric.document.defaultView&&fabric.document.defaultView.getComputedStyle?function(t,e){var i=fabric.document.defaultView.getComputedStyle(t,null);return i?i[e]:void 0}:function(t,e){var i=t.style[e];return!i&&t.currentStyle&&(i=t.currentStyle[e]),i},function(){var t=fabric.document.documentElement.style,e="userSelect"in t?"userSelect":"MozUserSelect"in t?"MozUserSelect":"WebkitUserSelect"in t?"WebkitUserSelect":"KhtmlUserSelect"in t?"KhtmlUserSelect":"";fabric.util.makeElementUnselectable=function(t){return void 0!==t.onselectstart&&(t.onselectstart=fabric.util.falseFunction),e?t.style[e]="none":"string"==typeof t.unselectable&&(t.unselectable="on"),t},fabric.util.makeElementSelectable=function(t){return void 0!==t.onselectstart&&(t.onselectstart=null),e?t.style[e]="":"string"==typeof t.unselectable&&(t.unselectable=""),t}}(),function(){fabric.util.getScript=function(t,e){var i=fabric.document.getElementsByTagName("head")[0],r=fabric.document.createElement("script"),n=!0;r.onload=r.onreadystatechange=function(t){if(n){if("string"==typeof this.readyState&&"loaded"!==this.readyState&&"complete"!==this.readyState)return;n=!1,e(t||fabric.window.event),r=r.onload=r.onreadystatechange=null}},r.src=t,i.appendChild(r)}}(),fabric.util.getById=function(t){return"string"==typeof t?fabric.document.getElementById(t):t},fabric.util.toArray=n,fabric.util.makeElement=t,fabric.util.addClass=function(t,e){t&&-1===(" "+t.className+" ").indexOf(" "+e+" ")&&(t.className+=(t.className?" ":"")+e)},fabric.util.wrapElement=function(e,i,r){return"string"==typeof i&&(i=t(i,r)),e.parentNode&&e.parentNode.replaceChild(i,e),i.appendChild(e),i},fabric.util.getScrollLeftTop=e,fabric.util.getElementOffset=function(t){var i,r,n=t&&t.ownerDocument,o={left:0,top:0},a={left:0,top:0},h={borderLeftWidth:"left",borderTopWidth:"top",paddingLeft:"left",paddingTop:"top"};if(!n)return a;for(var c in h)a[h[c]]+=parseInt(s(t,c),10)||0;return i=n.documentElement,void 0!==t.getBoundingClientRect&&(o=t.getBoundingClientRect()),r=e(t),{left:o.left+r.left-(i.clientLeft||0)+a.left,top:o.top+r.top-(i.clientTop||0)+a.top}},fabric.util.getElementStyle=s,fabric.util.getNodeCanvas=function(t){var e=fabric.jsdomImplForWrapper(t);return e._canvas||e._image}}(),function(){function t(){}var e=function(){for(var t=[function(){return new ActiveXObject("Microsoft.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP.3.0")},function(){return new XMLHttpRequest}],e=t.length;e--;)try{if(t[e]())return t[e]}catch(t){}}();fabric.util.request=function(i,r){r||(r={});var n=r.method?r.method.toUpperCase():"GET",s=r.onComplete||function(){},o=e(),a=r.body||r.parameters;return o.onreadystatechange=function(){4===o.readyState&&(s(o),o.onreadystatechange=t)},"GET"===n&&(a=null,"string"==typeof r.parameters&&(i=function(t,e){return t+(/\?/.test(t)?"&":"?")+e}(i,r.parameters))),o.open(n,i,!0),"POST"!==n&&"PUT"!==n||o.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),o.send(a),o}}(),fabric.log=function(){},fabric.warn=function(){},"undefined"!=typeof console&&["log","warn"].forEach(function(t){void 0!==console[t]&&"function"==typeof console[t].apply&&(fabric[t]=function(){return console[t].apply(console,arguments)})}),function(){function t(){return!1}function e(){return i.apply(fabric.window,arguments)}var i=fabric.window.requestAnimationFrame||fabric.window.webkitRequestAnimationFrame||fabric.window.mozRequestAnimationFrame||fabric.window.oRequestAnimationFrame||fabric.window.msRequestAnimationFrame||function(t){return fabric.window.setTimeout(t,1e3/60)},r=fabric.window.cancelAnimationFrame||fabric.window.clearTimeout;fabric.util.animate=function(i){e(function(r){i||(i={});var n,s=r||+new Date,o=i.duration||500,a=s+o,h=i.onChange||t,c=i.abort||t,l=i.onComplete||t,u=i.easing||function(t,e,i,r){return-i*Math.cos(t/r*(Math.PI/2))+i+e},f="startValue"in i?i.startValue:0,d="endValue"in i?i.endValue:100,g=i.byValue||d-f;i.onStart&&i.onStart(),function t(r){if(c())l(d,1,1);else{var p=(n=r||+new Date)>a?o:n-s,v=p/o,m=u(p,f,g,o),b=Math.abs((m-f)/g);h(m,b,v),n>a?i.onComplete&&i.onComplete():e(t)}}(s)})},fabric.util.requestAnimFrame=e,fabric.util.cancelAnimFrame=function(){return r.apply(fabric.window,arguments)}}(),function(){fabric.util.animateColor=function(t,e,i,r){var n=new fabric.Color(t).getSource(),s=new fabric.Color(e).getSource();r=r||{},fabric.util.animate(fabric.util.object.extend(r,{duration:i||500,startValue:n,endValue:s,byValue:s,easing:function(t,e,i,n){return function(t,e,i){var r="rgba("+parseInt(t[0]+i*(e[0]-t[0]),10)+","+parseInt(t[1]+i*(e[1]-t[1]),10)+","+parseInt(t[2]+i*(e[2]-t[2]),10);return r+=","+(t&&e?parseFloat(t[3]+i*(e[3]-t[3])):1),r+=")"}(e,i,r.colorEasing?r.colorEasing(t,n):1-Math.cos(t/n*(Math.PI/2)))}}))}}(),function(){function t(t,e,i,r){return t-1&&a>-1&&a-1&&(e="stroke")}else n=s?e.map(d):d(e,r);else e="";return!s&&isNaN(n)?e:n}function r(t){return new RegExp("^("+t.join("|")+")\\b","i")}function n(t,e){var i,r,n,s,o=[];for(n=0,s=e.length;na?a:o),1===o&&1===a&&0===h&&0===l&&0===g&&0===p)return x;if((g||p)&&(C=" translate("+d(g)+" "+d(p)+") "),r=C+" matrix("+o+" 0 0 "+a+" "+h*o+" "+l*a+") ","svg"===t.nodeName){for(n=t.ownerDocument.createElement("g");t.firstChild;)n.appendChild(t.firstChild);t.appendChild(n)}else r=(n=t).getAttribute("transform")+r;return n.setAttribute("transform",r),x}var c=t.fabric||(t.fabric={}),l=c.util.object.extend,u=c.util.object.clone,f=c.util.toFixed,d=c.util.parseUnit,g=c.util.multiplyTransformMatrices,p={cx:"left",x:"left",r:"radius",cy:"top",y:"top",display:"visible",visibility:"visible",transform:"transformMatrix","fill-opacity":"fillOpacity","fill-rule":"fillRule","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","paint-order":"paintFirst","stroke-dasharray":"strokeDashArray","stroke-linecap":"strokeLineCap","stroke-linejoin":"strokeLineJoin","stroke-miterlimit":"strokeMiterLimit","stroke-opacity":"strokeOpacity","stroke-width":"strokeWidth","text-decoration":"textDecoration","text-anchor":"textAnchor",opacity:"opacity"},v={stroke:"strokeOpacity",fill:"fillOpacity"};c.svgValidTagNamesRegEx=r(["path","circle","polygon","polyline","ellipse","rect","line","image","text","linearGradient","radialGradient","stop"]),c.svgViewBoxElementsRegEx=r(["symbol","image","marker","pattern","view","svg"]),c.svgInvalidAncestorsRegEx=r(["pattern","defs","symbol","metadata","clipPath","mask","desc"]),c.svgValidParentsRegEx=r(["symbol","g","a","svg"]),c.cssRules={},c.gradientDefs={},c.parseTransformAttribute=function(){function t(t,e,i){t[i]=Math.tan(c.util.degreesToRadians(e[0]))}var e=[1,0,0,1,0,0],i=c.reNum,r="(?:\\s+,?\\s*|,\\s*)",n="(?:"+("(?:(matrix)\\s*\\(\\s*("+i+")"+r+"("+i+")"+r+"("+i+")"+r+"("+i+")"+r+"("+i+")"+r+"("+i+")\\s*\\))")+"|"+("(?:(translate)\\s*\\(\\s*("+i+")(?:"+r+"("+i+"))?\\s*\\))")+"|"+("(?:(scale)\\s*\\(\\s*("+i+")(?:"+r+"("+i+"))?\\s*\\))")+"|"+("(?:(rotate)\\s*\\(\\s*("+i+")(?:"+r+"("+i+")"+r+"("+i+"))?\\s*\\))")+"|"+("(?:(skewX)\\s*\\(\\s*("+i+")\\s*\\))")+"|"+("(?:(skewY)\\s*\\(\\s*("+i+")\\s*\\))")+")",s="^\\s*(?:"+("(?:"+n+"(?:"+r+"*"+n+")*)")+"?)\\s*$",o=new RegExp(s),a=new RegExp(n,"g");return function(i){var r=e.concat(),s=[];if(!i||i&&!o.test(i))return r;i.replace(a,function(i){var o=new RegExp(n).exec(i).filter(function(t){return!!t}),a=o[1],h=o.slice(2).map(parseFloat);switch(a){case"translate":!function(t,e){t[4]=e[0],2===e.length&&(t[5]=e[1])}(r,h);break;case"rotate":h[0]=c.util.degreesToRadians(h[0]),function(t,e){var i=c.util.cos(e[0]),r=c.util.sin(e[0]),n=0,s=0;3===e.length&&(n=e[1],s=e[2]),t[0]=i,t[1]=r,t[2]=-r,t[3]=i,t[4]=n-(i*n-r*s),t[5]=s-(r*n+i*s)}(r,h);break;case"scale":!function(t,e){var i=e[0],r=2===e.length?e[1]:e[0];t[0]=i,t[3]=r}(r,h);break;case"skewX":t(r,h,2);break;case"skewY":t(r,h,1);break;case"matrix":r=h}s.push(r.concat()),r=e.concat()});for(var h=s[0];s.length>1;)s.shift(),h=c.util.multiplyTransformMatrices(h,s[0]);return h}}();var m=new RegExp("^\\s*("+c.reNum+"+)\\s*,?\\s*("+c.reNum+"+)\\s*,?\\s*("+c.reNum+"+)\\s*,?\\s*("+c.reNum+"+)\\s*$");c.parseSVGDocument=function(t,e,i,r){if(t){!function(t){for(var e=n(t,["use","svg:use"]),i=0;e.length&&i/i,""))),n&&n.documentElement||e&&e(null),c.parseSVGDocument(n.documentElement,function(t,i,r,n){e&&e(t,i,r,n)},i,r)}})},loadSVGFromString:function(t,e,i,r){t=t.trim();var n;if("undefined"!=typeof DOMParser){var s=new DOMParser;s&&s.parseFromString&&(n=s.parseFromString(t,"text/xml"))}else c.window.ActiveXObject&&((n=new ActiveXObject("Microsoft.XMLDOM")).async="false",n.loadXML(t.replace(//i,"")));c.parseSVGDocument(n.documentElement,function(t,i,r,n){e(t,i,r,n)},i,r)}})}("undefined"!=typeof exports?exports:this),fabric.ElementsParser=function(t,e,i,r,n){this.elements=t,this.callback=e,this.options=i,this.reviver=r,this.svgUid=i&&i.svgUid||0,this.parsingOptions=n},fabric.ElementsParser.prototype.parse=function(){this.instances=new Array(this.elements.length),this.numElements=this.elements.length,this.createObjects()},fabric.ElementsParser.prototype.createObjects=function(){for(var t=0,e=this.elements.length;tt.x&&this.y>t.y},gte:function(t){return this.x>=t.x&&this.y>=t.y},lerp:function(t,i){return void 0===i&&(i=.5),i=Math.max(Math.min(1,i),0),new e(this.x+(t.x-this.x)*i,this.y+(t.y-this.y)*i)},distanceFrom:function(t){var e=this.x-t.x,i=this.y-t.y;return Math.sqrt(e*e+i*i)},midPointFrom:function(t){return this.lerp(t)},min:function(t){return new e(Math.min(this.x,t.x),Math.min(this.y,t.y))},max:function(t){return new e(Math.max(this.x,t.x),Math.max(this.y,t.y))},toString:function(){return this.x+","+this.y},setXY:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setFromPoint:function(t){return this.x=t.x,this.y=t.y,this},swap:function(t){var e=this.x,i=this.y;this.x=t.x,this.y=t.y,t.x=e,t.y=i},clone:function(){return new e(this.x,this.y)}})}("undefined"!=typeof exports?exports:this),function(t){"use strict";function e(t){this.status=t,this.points=[]}var i=t.fabric||(t.fabric={});i.Intersection?i.warn("fabric.Intersection is already defined"):(i.Intersection=e,i.Intersection.prototype={constructor:e,appendPoint:function(t){return this.points.push(t),this},appendPoints:function(t){return this.points=this.points.concat(t),this}},i.Intersection.intersectLineLine=function(t,r,n,s){var o,a=(s.x-n.x)*(t.y-n.y)-(s.y-n.y)*(t.x-n.x),h=(r.x-t.x)*(t.y-n.y)-(r.y-t.y)*(t.x-n.x),c=(s.y-n.y)*(r.x-t.x)-(s.x-n.x)*(r.y-t.y);if(0!==c){var l=a/c,u=h/c;0<=l&&l<=1&&0<=u&&u<=1?(o=new e("Intersection")).appendPoint(new i.Point(t.x+l*(r.x-t.x),t.y+l*(r.y-t.y))):o=new e}else o=new e(0===a||0===h?"Coincident":"Parallel");return o},i.Intersection.intersectLinePolygon=function(t,i,r){var n,s,o,a,h=new e,c=r.length;for(a=0;a0&&(h.status="Intersection"),h},i.Intersection.intersectPolygonPolygon=function(t,i){var r,n=new e,s=t.length;for(r=0;r0&&(n.status="Intersection"),n},i.Intersection.intersectPolygonRectangle=function(t,r,n){var s=r.min(n),o=r.max(n),a=new i.Point(o.x,s.y),h=new i.Point(s.x,o.y),c=e.intersectLinePolygon(s,a,t),l=e.intersectLinePolygon(a,o,t),u=e.intersectLinePolygon(o,h,t),f=e.intersectLinePolygon(h,s,t),d=new e;return d.appendPoints(c.points),d.appendPoints(l.points),d.appendPoints(u.points),d.appendPoints(f.points),d.points.length>0&&(d.status="Intersection"),d})}("undefined"!=typeof exports?exports:this),function(t){"use strict";function e(t){t?this._tryParsingColor(t):this.setSource([0,0,0,1])}function i(t,e,i){return i<0&&(i+=1),i>1&&(i-=1),i<1/6?t+6*(e-t)*i:i<.5?e:i<2/3?t+(e-t)*(2/3-i)*6:t}var r=t.fabric||(t.fabric={});r.Color?r.warn("fabric.Color is already defined."):(r.Color=e,r.Color.prototype={_tryParsingColor:function(t){var i;t in e.colorNameMap&&(t=e.colorNameMap[t]),"transparent"===t&&(i=[255,255,255,0]),i||(i=e.sourceFromHex(t)),i||(i=e.sourceFromRgb(t)),i||(i=e.sourceFromHsl(t)),i||(i=[0,0,0,1]),i&&this.setSource(i)},_rgbToHsl:function(t,e,i){t/=255,e/=255,i/=255;var n,s,o,a=r.util.array.max([t,e,i]),h=r.util.array.min([t,e,i]);if(o=(a+h)/2,a===h)n=s=0;else{var c=a-h;switch(s=o>.5?c/(2-a-h):c/(a+h),a){case t:n=(e-i)/c+(e1?1:o,s){var a=s.split(/\s*;\s*/);for(""===a[a.length-1]&&a.pop(),n=a.length;n--;){var h=a[n].split(/\s*:\s*/),c=h[0].trim(),l=h[1].trim();"stop-color"===c?e=l:"stop-opacity"===c&&(r=l)}}return e||(e=t.getAttribute("stop-color")||"rgb(0,0,0)"),r||(r=t.getAttribute("stop-opacity")),e=new fabric.Color(e),i=e.getAlpha(),r=isNaN(parseFloat(r))?1:parseFloat(r),r*=i,{offset:o,color:e.toRgb(),opacity:r}}function e(t,e,i){var r,n=0,s=1,o="";for(var a in e)"Infinity"===e[a]?e[a]=1:"-Infinity"===e[a]&&(e[a]=0),r=parseFloat(e[a],10),s="string"==typeof e[a]&&/^(\d+\.\d+)%|(\d+)%$/.test(e[a])?.01:1,"x1"===a||"x2"===a||"r2"===a?(s*="objectBoundingBox"===i?t.width:1,n="objectBoundingBox"===i?t.left||0:0):"y1"!==a&&"y2"!==a||(s*="objectBoundingBox"===i?t.height:1,n="objectBoundingBox"===i?t.top||0:0),e[a]=r*s+n;if("ellipse"===t.type&&null!==e.r2&&"objectBoundingBox"===i&&t.rx!==t.ry){var h=t.ry/t.rx;o=" scale(1, "+h+")",e.y1&&(e.y1/=h),e.y2&&(e.y2/=h)}return o}var i=fabric.util.object.clone;fabric.Gradient=fabric.util.createClass({offsetX:0,offsetY:0,initialize:function(t){t||(t={});var e={};this.id=fabric.Object.__uid++,this.type=t.type||"linear",e={x1:t.coords.x1||0,y1:t.coords.y1||0,x2:t.coords.x2||0,y2:t.coords.y2||0},"radial"===this.type&&(e.r1=t.coords.r1||0,e.r2=t.coords.r2||0),this.coords=e,this.colorStops=t.colorStops.slice(),t.gradientTransform&&(this.gradientTransform=t.gradientTransform),this.offsetX=t.offsetX||this.offsetX,this.offsetY=t.offsetY||this.offsetY},addColorStop:function(t){for(var e in t){var i=new fabric.Color(t[e]);this.colorStops.push({offset:parseFloat(e),color:i.toRgb(),opacity:i.getAlpha()})}return this},toObject:function(t){var e={type:this.type,coords:this.coords,colorStops:this.colorStops,offsetX:this.offsetX,offsetY:this.offsetY,gradientTransform:this.gradientTransform?this.gradientTransform.concat():this.gradientTransform};return fabric.util.populateWithProperties(this,e,t),e},toSVG:function(t){var e,r,n,s,o=i(this.coords,!0),a=i(this.colorStops,!0),h=o.r1>o.r2,c=t.width/2,l=t.height/2;a.sort(function(t,e){return t.offset-e.offset}),"path"===t.type&&(c-=t.pathOffset.x,l-=t.pathOffset.y);for(var u in o)"x1"===u||"x2"===u?o[u]+=this.offsetX-c:"y1"!==u&&"y2"!==u||(o[u]+=this.offsetY-l);if(s='id="SVGID_'+this.id+'" gradientUnits="userSpaceOnUse"',this.gradientTransform&&(s+=' gradientTransform="matrix('+this.gradientTransform.join(" ")+')" '),"linear"===this.type?n=["\n']:"radial"===this.type&&(n=["\n']),"radial"===this.type){if(h)for((a=a.concat()).reverse(),e=0,r=a.length;e0){var d=f/Math.max(o.r1,o.r2);for(e=0,r=a.length;e\n')}return n.push("linear"===this.type?"\n":"\n"),n.join("")},toLive:function(t){var e,i,r,n=fabric.util.object.clone(this.coords);if(this.type){for("linear"===this.type?e=t.createLinearGradient(n.x1,n.y1,n.x2,n.y2):"radial"===this.type&&(e=t.createRadialGradient(n.x1,n.y1,n.r1,n.x2,n.y2,n.r2)),i=0,r=this.colorStops.length;i\n\n\n'},setOptions:function(t){for(var e in t)this[e]=t[e]},toLive:function(t){var e="function"==typeof this.source?this.source():this.source;if(!e)return"";if(void 0!==e.src){if(!e.complete)return"";if(0===e.naturalWidth||0===e.naturalHeight)return""}return t.createPattern(e,this.repeat)}})}(),function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.toFixed;e.Shadow?e.warn("fabric.Shadow is already defined."):(e.Shadow=e.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,initialize:function(t){"string"==typeof t&&(t=this._parseShadow(t));for(var i in t)this[i]=t[i];this.id=e.Object.__uid++},_parseShadow:function(t){var i=t.trim(),r=e.Shadow.reOffsetsAndBlur.exec(i)||[];return{color:(i.replace(e.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)").trim(),offsetX:parseInt(r[1],10)||0,offsetY:parseInt(r[2],10)||0,blur:parseInt(r[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(t){var r=40,n=40,s=e.Object.NUM_FRACTION_DIGITS,o=e.util.rotateVector({x:this.offsetX,y:this.offsetY},e.util.degreesToRadians(-t.angle));return t.width&&t.height&&(r=100*i((Math.abs(o.x)+this.blur)/t.width,s)+20,n=100*i((Math.abs(o.y)+this.blur)/t.height,s)+20),t.flipX&&(o.x*=-1),t.flipY&&(o.y*=-1),'\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY,affectStroke:this.affectStroke};var t={},i=e.Shadow.prototype;return["color","blur","offsetX","offsetY","affectStroke"].forEach(function(e){this[e]!==i[e]&&(t[e]=this[e])},this),t}}),e.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/)}("undefined"!=typeof exports?exports:this),function(){"use strict";if(fabric.StaticCanvas)fabric.warn("fabric.StaticCanvas is already defined.");else{var t=fabric.util.object.extend,e=fabric.util.getElementOffset,i=fabric.util.removeFromArray,r=fabric.util.toFixed,n=fabric.util.transformPoint,s=fabric.util.invertTransform,o=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass(fabric.CommonMethods,{initialize:function(t,e){e||(e={}),this.renderAndResetBound=this.renderAndReset.bind(this),this.requestRenderAllBound=this.requestRenderAll.bind(this),this._initStatic(t,e)},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!1,renderOnAddRemove:!0,clipTo:null,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,viewportTransform:fabric.iMatrix.concat(),backgroundVpt:!0,overlayVpt:!0,onBeforeScaleRotate:function(){},enableRetinaScaling:!0,vptCoords:{},skipOffscreen:!0,_initStatic:function(t,e){var i=this.requestRenderAllBound;this._objects=[],this._createLowerCanvas(t),this._initOptions(e),this._setImageSmoothing(),this.interactive||this._initRetinaScaling(),e.overlayImage&&this.setOverlayImage(e.overlayImage,i),e.backgroundImage&&this.setBackgroundImage(e.backgroundImage,i),e.backgroundColor&&this.setBackgroundColor(e.backgroundColor,i),e.overlayColor&&this.setOverlayColor(e.overlayColor,i),this.calcOffset()},_isRetinaScaling:function(){return 1!==fabric.devicePixelRatio&&this.enableRetinaScaling},getRetinaScaling:function(){return this._isRetinaScaling()?fabric.devicePixelRatio:1},_initRetinaScaling:function(){this._isRetinaScaling()&&(this.lowerCanvasEl.setAttribute("width",this.width*fabric.devicePixelRatio),this.lowerCanvasEl.setAttribute("height",this.height*fabric.devicePixelRatio),this.contextContainer.scale(fabric.devicePixelRatio,fabric.devicePixelRatio))},calcOffset:function(){return this._offset=e(this.lowerCanvasEl),this},setOverlayImage:function(t,e,i){return this.__setBgOverlayImage("overlayImage",t,e,i)},setBackgroundImage:function(t,e,i){return this.__setBgOverlayImage("backgroundImage",t,e,i)},setOverlayColor:function(t,e){return this.__setBgOverlayColor("overlayColor",t,e)},setBackgroundColor:function(t,e){return this.__setBgOverlayColor("backgroundColor",t,e)},_setImageSmoothing:function(){var t=this.getContext();t.imageSmoothingEnabled=t.imageSmoothingEnabled||t.webkitImageSmoothingEnabled||t.mozImageSmoothingEnabled||t.msImageSmoothingEnabled||t.oImageSmoothingEnabled,t.imageSmoothingEnabled=this.imageSmoothingEnabled},__setBgOverlayImage:function(t,e,i,r){return"string"==typeof e?fabric.util.loadImage(e,function(e){e&&(this[t]=new fabric.Image(e,r)),i&&i(e)},this,r&&r.crossOrigin):(r&&e.setOptions(r),this[t]=e,i&&i(e)),this},__setBgOverlayColor:function(t,e,i){return this[t]=e,this._initGradient(e,t),this._initPattern(e,t,i),this},_createCanvasElement:function(){var t=fabric.util.createCanvasElement();if(!t)throw o;if(t.style||(t.style={}),void 0===t.getContext)throw o;return t},_initOptions:function(t){this._setOptions(t),this.width=this.width||parseInt(this.lowerCanvasEl.width,10)||0,this.height=this.height||parseInt(this.lowerCanvasEl.height,10)||0,this.lowerCanvasEl.style&&(this.lowerCanvasEl.width=this.width,this.lowerCanvasEl.height=this.height,this.lowerCanvasEl.style.width=this.width+"px",this.lowerCanvasEl.style.height=this.height+"px",this.viewportTransform=this.viewportTransform.slice())},_createLowerCanvas:function(t){t&&t.getContext?this.lowerCanvasEl=t:this.lowerCanvasEl=fabric.util.getById(t)||this._createCanvasElement(),fabric.util.addClass(this.lowerCanvasEl,"lower-canvas"),this.interactive&&this._applyCanvasStyle(this.lowerCanvasEl),this.contextContainer=this.lowerCanvasEl.getContext("2d")},getWidth:function(){return this.width},getHeight:function(){return this.height},setWidth:function(t,e){return this.setDimensions({width:t},e)},setHeight:function(t,e){return this.setDimensions({height:t},e)},setDimensions:function(t,e){var i;e=e||{};for(var r in t)i=t[r],e.cssOnly||(this._setBackstoreDimension(r,t[r]),i+="px"),e.backstoreOnly||this._setCssDimension(r,i);return this._isCurrentlyDrawing&&this.freeDrawingBrush&&this.freeDrawingBrush._setBrushStyles(),this._initRetinaScaling(),this._setImageSmoothing(),this.calcOffset(),e.cssOnly||this.requestRenderAll(),this},_setBackstoreDimension:function(t,e){return this.lowerCanvasEl[t]=e,this.upperCanvasEl&&(this.upperCanvasEl[t]=e),this.cacheCanvasEl&&(this.cacheCanvasEl[t]=e),this[t]=e,this},_setCssDimension:function(t,e){return this.lowerCanvasEl.style[t]=e,this.upperCanvasEl&&(this.upperCanvasEl.style[t]=e),this.wrapperEl&&(this.wrapperEl.style[t]=e),this},getZoom:function(){return this.viewportTransform[0]},setViewportTransform:function(t){var e,i,r,n=this._activeObject;for(this.viewportTransform=t,i=0,r=this._objects.length;i"),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,n=e.width||this.width,s=e.height||this.height,o='viewBox="0 0 '+this.width+" "+this.height+'" ',a=fabric.Object.NUM_FRACTION_DIGITS;e.viewBox?o='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,o='viewBox="'+r(-i[4]/i[0],a)+" "+r(-i[5]/i[3],a)+" "+r(this.width/i[0],a)+" "+r(this.height/i[3],a)+'" '),t.push("