From 9422fd39bebb7e2f07fcc44e29eec71f61b6eff5 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 13 Nov 2016 21:00:10 +0100 Subject: [PATCH] Objectcaching (#3317) --- src/mixins/itext_key_behavior.mixin.js | 9 +- src/mixins/object_interactivity.mixin.js | 20 +-- src/mixins/stateful.mixin.js | 26 +++- src/parser.js | 11 +- src/shapes/circle.class.js | 7 +- src/shapes/ellipse.class.js | 7 +- src/shapes/group.class.js | 50 ++++--- src/shapes/image.class.js | 9 ++ src/shapes/itext.class.js | 3 +- src/shapes/line.class.js | 19 ++- src/shapes/object.class.js | 175 ++++++++++++++++++++--- src/shapes/path.class.js | 8 +- src/shapes/path_group.class.js | 48 ++++--- src/shapes/polygon.class.js | 2 +- src/shapes/rect.class.js | 2 - src/shapes/text.class.js | 103 +++++++------ src/shapes/textbox.class.js | 6 +- src/shapes/triangle.class.js | 7 +- src/static_canvas.class.js | 4 +- src/util/lang_object.js | 4 +- test/unit/canvas_static.js | 5 +- test/unit/group.js | 25 +++- test/unit/stateful.js | 14 +- 23 files changed, 377 insertions(+), 187 deletions(-) diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index 27664386..30ac2711 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -198,9 +198,12 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } fabric.copiedText = selectedText; - fabric.copiedTextStyle = this.getSelectionStyles( - this.selectionStart, - this.selectionEnd); + fabric.copiedTextStyle = fabric.util.object.clone( + this.getSelectionStyles( + this.selectionStart, + this.selectionEnd + ) + ); e.stopImmediatePropagation(); e.preventDefault(); this._copyDone = true; diff --git a/src/mixins/object_interactivity.mixin.js b/src/mixins/object_interactivity.mixin.js index fe097110..e674f399 100644 --- a/src/mixins/object_interactivity.mixin.js +++ b/src/mixins/object_interactivity.mixin.js @@ -113,24 +113,8 @@ */ _getNonTransformedDimensions: function() { var strokeWidth = this.strokeWidth, - w = this.width, - h = this.height, - addStrokeToW = true, - addStrokeToH = true; - - if (this.type === 'line' && this.strokeLineCap === 'butt') { - addStrokeToH = w; - addStrokeToW = h; - } - - if (addStrokeToH) { - h += h < 0 ? -strokeWidth : strokeWidth; - } - - if (addStrokeToW) { - w += w < 0 ? -strokeWidth : strokeWidth; - } - + w = this.width + strokeWidth, + h = this.height + strokeWidth; return { x: w, y: h }; }, diff --git a/src/mixins/stateful.mixin.js b/src/mixins/stateful.mixin.js index a51d2cb2..6a1fa6d1 100644 --- a/src/mixins/stateful.mixin.js +++ b/src/mixins/stateful.mixin.js @@ -1,6 +1,7 @@ (function() { - var extend = fabric.util.object.extend; + var extend = fabric.util.object.extend, + originalSet = 'stateProperties'; /* Depends on `stateProperties` @@ -13,7 +14,7 @@ extend(origin[destination], tmpObj, deep); } - function _isEqual(origValue, currentValue) { + function _isEqual(origValue, currentValue, firstPass) { if (!fabric.isLikelyNode && origValue instanceof Element) { // avoid checking deep html elements return origValue === currentValue; @@ -29,6 +30,9 @@ }); } else if (origValue instanceof Object) { + if (!firstPass && Object.keys(origValue).length !== Object.keys(currentValue).length) { + return false; + } for (var key in origValue) { if (!_isEqual(origValue[key], currentValue[key])) { return false; @@ -46,10 +50,13 @@ /** * Returns true if object state (one of its state properties) was changed + * @param {String} [propertySet] optional name for the set of property we want to save * @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called */ - hasStateChanged: function() { - return !_isEqual(this.originalState, this); + hasStateChanged: function(propertySet) { + propertySet = propertySet || originalSet; + propertySet = '_' + propertySet; + return !_isEqual(this[propertySet], this, true); }, /** @@ -58,9 +65,11 @@ * @return {fabric.Object} thisArg */ saveState: function(options) { - saveProps(this, 'originalState', this.stateProperties); + var propertySet = options && options.propertySet || originalSet, + destination = '_' + propertySet; + saveProps(this, destination, this[propertySet]); if (options && options.stateProperties) { - saveProps(this, 'originalState', options.stateProperties); + saveProps(this, destination, options.stateProperties); } return this; }, @@ -71,7 +80,10 @@ * @return {fabric.Object} thisArg */ setupState: function(options) { - this.originalState = { }; + options = options || { }; + var propertySet = options.propertySet || originalSet; + options.propertySet = propertySet; + this['_' + propertySet] = { }; this.saveState(options); return this; } diff --git a/src/parser.js b/src/parser.js index dff4b444..baec5f83 100644 --- a/src/parser.js +++ b/src/parser.js @@ -69,9 +69,14 @@ value = ''; } else if (attr === 'strokeDashArray') { - value = value.replace(/,/g, ' ').split(/\s+/).map(function(n) { - return parseFloat(n); - }); + if (value === 'none') { + value = null; + } + else { + value = value.replace(/,/g, ' ').split(/\s+/).map(function(n) { + return parseFloat(n); + }); + } } else if (attr === 'transformMatrix') { if (parentAttributes && parentAttributes.transformMatrix) { diff --git a/src/shapes/circle.class.js b/src/shapes/circle.class.js index df56730d..10def22a 100644 --- a/src/shapes/circle.class.js +++ b/src/shapes/circle.class.js @@ -53,13 +53,8 @@ * @return {fabric.Circle} thisArg */ initialize: function(options) { - options = options || { }; - this.callSuper('initialize', options); - this.set('radius', options.radius || 0); - - this.startAngle = options.startAngle || this.startAngle; - this.endAngle = options.endAngle || this.endAngle; + this.set('radius', options && options.radius || 0); }, /** diff --git a/src/shapes/ellipse.class.js b/src/shapes/ellipse.class.js index d6226590..ac9ffbcc 100644 --- a/src/shapes/ellipse.class.js +++ b/src/shapes/ellipse.class.js @@ -47,12 +47,9 @@ * @return {fabric.Ellipse} thisArg */ initialize: function(options) { - options = options || { }; - this.callSuper('initialize', options); - - this.set('rx', options.rx || 0); - this.set('ry', options.ry || 0); + this.set('rx', options && options.rx || 0); + this.set('ry', options && options.ry || 0); }, /** diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 521d6a69..3cc8aeb7 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -164,6 +164,7 @@ this.forEachObject(this._setObjectActive, this); this._calcBounds(); this._updateObjectsCoords(); + this.dirty = true; return this; }, @@ -190,7 +191,7 @@ this.remove(object); this._calcBounds(); this._updateObjectsCoords(); - + this.dirty = true; return this; }, @@ -198,6 +199,7 @@ * @private */ _onObjectAdded: function(object) { + this.dirty = true; object.group = this; object._set('canvas', this.canvas); }, @@ -206,6 +208,7 @@ * @private */ _onObjectRemoved: function(object) { + this.dirty = true; delete object.group; object.set('active', false); }, @@ -264,27 +267,40 @@ * @param {CanvasRenderingContext2D} ctx context to render instance on */ render: function(ctx) { - // do not render if object is not visible - if (!this.visible) { - return; - } - - ctx.save(); - if (this.transformMatrix) { - ctx.transform.apply(ctx, this.transformMatrix); - } - this.transform(ctx); - this._setShadow(ctx); - this.clipTo && fabric.util.clipContext(this, ctx); this._transformDone = true; - // the array is now sorted in order of highest first, so start from end + this.callSuper('render', ctx); + this._transformDone = false; + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + drawObject: function(ctx) { for (var i = 0, len = this._objects.length; i < len; i++) { this._renderObject(this._objects[i], ctx); } + }, - this.clipTo && ctx.restore(); - ctx.restore(); - this._transformDone = false; + /** + * Check if cache is dirty + */ + isCacheDirty: function() { + if (this.callSuper('isCacheDirty')) { + return true + } + if (!this.statefullCache) { + return false; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].isCacheDirty(true)) { + var dim = this._getNonTransformedDimensions(); + this._cacheContext.clearRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y); + return true + } + } + return false; }, /** diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 71c228d8..902827f7 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -112,6 +112,15 @@ */ stateProperties: stateProperties, + /** + * When `true`, object is cached on an additional canvas. + * default to false for images + * since 1.7.0 + * @type Boolean + * @default + */ + objectCaching: false, + /** * Constructor * @param {HTMLImageElement | String} element Image element diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index e68e6e68..f6818e0c 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -811,7 +811,7 @@ leftOffset = this._getLeftOffset(), topOffset = this._getTopOffset(), line, _char, style; - + ctx.save(); for (var i = 0, len = this._textLines.length; i < len; i++) { heightOfLine = this._getHeightOfLine(ctx, i); line = this._textLines[i]; @@ -842,6 +842,7 @@ } lineTopOffset += heightOfLine; } + ctx.restore(); }, /** diff --git a/src/shapes/line.class.js b/src/shapes/line.class.js index 2cc26590..6b21f58b 100644 --- a/src/shapes/line.class.js +++ b/src/shapes/line.class.js @@ -62,8 +62,6 @@ * @return {fabric.Line} thisArg */ initialize: function(points, options) { - options = options || { }; - if (!points) { points = [0, 0, 0, 0]; } @@ -206,6 +204,23 @@ return extend(this.callSuper('toObject', propertiesToInclude), this.calcLinePoints()); }, + /* + * Calculate object dimensions from its properties + * @private + */ + _getNonTransformedDimensions: function() { + var dim = this.callSuper('_getNonTransformedDimensions'); + if (this.strokeLineCap === 'butt') { + if (dim.x === 0) { + dim.y -= this.strokeWidth; + } + if (dim.y === 0) { + dim.x -= this.strokeWidth; + } + } + return dim; + }, + /** * Recalculates line points given width and height * @private diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 644ece7c..a82fac20 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -7,7 +7,8 @@ toFixed = fabric.util.toFixed, capitalize = fabric.util.string.capitalize, degreesToRadians = fabric.util.degreesToRadians, - supportsLineDash = fabric.StaticCanvas.supports('setLineDash'); + supportsLineDash = fabric.StaticCanvas.supports('setLineDash'), + objectCaching = !fabric.isLikelyNode; if (fabric.Object) { return; @@ -743,7 +744,6 @@ * @type Boolean * @default */ - lockScalingFlip: false, /** @@ -752,8 +752,39 @@ * @type Boolean * @default */ + excludeFromExport: false, - excludeFromExport: false, + /** + * When `true`, object is cached on an additional canvas. + * default to true + * since 1.7.0 + * @type Boolean + * @default + */ + objectCaching: objectCaching, + + /** + * When `true`, object properties are checked for cache invalidation. In some particular + * situation you may want this to be disabled ( spray brush, very big pathgroups, groups) + * or if your application does not allow you to modify properties for groups child you want + * to disable it for groups. + * default to true + * since 1.7.0 + * @type Boolean + * @default + */ + statefullCache: false, + + /** + * When `true`, cache does not get updated during scaling. The picture will get blocky if scaled + * too much and will be redrawn with correct details at the end of scaling. + * this setting is performance and application dependant. + * default to false + * since 1.7.0 + * @type Boolean + * @default + */ + noScaleCache: true, /** * List of properties to consider when checking if state @@ -761,21 +792,80 @@ * as well as for history (undo/redo) purposes * @type Array */ - stateProperties: ( + stateProperties: ( 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + 'stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit ' + 'angle opacity fill fillRule globalCompositeOperation shadow clipTo visible backgroundColor ' + 'skewX skewY' ).split(' '), + /** + * List of properties to consider when checking if cache needs refresh + * @type Array + */ + cacheProperties: ( + 'fill stroke strokeWidth strokeDashArray width height stroke strokeWidth strokeDashArray' + + ' strokeLineCap strokeLineJoin strokeMiterLimit fillRule backgroundColor' + ).split(' '), + /** * Constructor * @param {Object} [options] Options object */ initialize: function(options) { + options = options || { }; if (options) { this.setOptions(options); } + if (this.objectCaching) { + this._createCacheCanvas(); + this.setupState({ propertySet: 'cacheProperties' }); + } + }, + + /** + * Create a the canvas used to keep the cached copy of the object + * @private + */ + _createCacheCanvas: function() { + this._cacheCanvas = fabric.document.createElement('canvas'); + this._cacheContext = this._cacheCanvas.getContext('2d'); + this._updateCacheCanvas(); + }, + + /** + * Update width and height of the canvas for cache + * returns true or false if canvas needed resize. + * @private + * @return {Boolean} true if the canvas has been resized + */ + _updateCacheCanvas: function() { + if (this.noScaleCache && this.canvas && this.canvas._currentTransform) { + var action = this.canvas._currentTransform.action; + if (action.slice(0, 5) === 'scale') { + return false; + } + } + var zoom = this.getViewportTransform()[0], + objectScale = this.getObjectScaling(), + dim = this._getNonTransformedDimensions(), + retina = this.canvas && this.canvas._isRetinaScaling() ? fabric.devicePixelRatio : 1, + zoomX = objectScale.scaleX * zoom * retina, + zoomY = objectScale.scaleY * zoom * retina; + if (zoomX !== this.zoomX || zoomY !== this.zoomY) { + var width = dim.x * zoomX, + height = dim.y * zoomY; + this._cacheCanvas.width = width; + this._cacheCanvas.height = height; + this._cacheContext.translate(width / 2, height / 2); + this._cacheContext.scale(zoomX, zoomY); + this.cacheWidth = width; + this.cacheHeight = height; + this.zoomX = zoomX; + this.zoomY = zoomY; + return true + } + return false }, /** @@ -1087,9 +1177,7 @@ if ((this.width === 0 && this.height === 0) || !this.visible) { return; } - ctx.save(); - //setup fill rule for current object this._setupCompositeOperation(ctx); this.drawSelectionBackground(ctx); @@ -1098,21 +1186,71 @@ } this._setOpacity(ctx); this._setShadow(ctx); - this._renderBackground(ctx); - this._setStrokeStyles(ctx); - this._setFillStyles(ctx); if (this.transformMatrix) { ctx.transform.apply(ctx, this.transformMatrix); } this.clipTo && fabric.util.clipContext(this, ctx); - this._render(ctx, noTransform); + if (this.objectCaching && !this.group) { + if (this.isCacheDirty(noTransform)) { + this.saveState({ propertySet: 'cacheProperties' }); + this.drawObject(this._cacheContext, noTransform); + this.dirty = false; + } + this.drawCacheOnCanvas(ctx); + } + else { + this.drawObject(ctx, noTransform); + noTransform && this.saveState({ propertySet: 'cacheProperties' }); + } this.clipTo && ctx.restore(); - ctx.restore(); }, /** - * Draws a background for the object big as its width and height; + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + drawObject: function(ctx, noTransform) { + this._renderBackground(ctx); + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); + this._render(ctx, noTransform); + }, + + /** + * Paint the cached copy of the object on the target context. + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas: function(ctx) { + ctx.scale(1 / this.zoomX, 1 / this.zoomY); + ctx.drawImage(this._cacheCanvas, -this.cacheWidth / 2, -this.cacheHeight / 2); + }, + + /** + * Check if cache is dirty + * @param {Boolean} skipCanvas skip canvas checks because this object is painted + * on parent canvas. + */ + isCacheDirty: function(skipCanvas) { + if (!skipCanvas && this._updateCacheCanvas()) { + // in this case the context is already cleared. + return true; + } + else { + if (this.dirty || (this.statefullCache && this.hasStateChanged('cacheProperties'))) { + if (!skipCanvas) { + var dim = this._getNonTransformedDimensions(); + this._cacheContext.clearRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y); + } + return true; + } + } + return false; + }, + + /** + * Draws a background for the object big as its untrasformed dimensions * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ @@ -1120,14 +1258,14 @@ if (!this.backgroundColor) { return; } - + var dim = this._getNonTransformedDimensions(); ctx.fillStyle = this.backgroundColor; ctx.fillRect( - -this.width / 2, - -this.height / 2, - this.width, - this.height + -dim.x / 2, + -dim.y / 2, + dim.x, + dim.y ); // if there is background color no other shadows // should be casted @@ -1139,9 +1277,6 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ _setOpacity: function(ctx) { - if (this.group) { - this.group._setOpacity(ctx); - } ctx.globalAlpha *= this.opacity; }, diff --git a/src/shapes/path.class.js b/src/shapes/path.class.js index 2a2af928..433056d3 100644 --- a/src/shapes/path.class.js +++ b/src/shapes/path.class.js @@ -75,7 +75,9 @@ initialize: function(path, options) { options = options || { }; - this.setOptions(options); + if (options) { + this.setOptions(options); + } if (!path) { path = []; @@ -101,6 +103,10 @@ if (options.sourcePath) { this.setSourcePath(options.sourcePath); } + if (this.objectCaching) { + this._createCacheCanvas(); + this.setupState({ propertySet: 'cacheProperties' }); + } }, /** diff --git a/src/shapes/path_group.class.js b/src/shapes/path_group.class.js index 056b2fba..47b69e0c 100644 --- a/src/shapes/path_group.class.js +++ b/src/shapes/path_group.class.js @@ -19,7 +19,7 @@ * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#path_and_pathgroup} * @see {@link fabric.PathGroup#initialize} for constructor definition */ - fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @lends fabric.PathGroup.prototype */ { + fabric.PathGroup = fabric.util.createClass(fabric.Object, /** @lends fabric.PathGroup.prototype */ { /** * Type of an object @@ -56,10 +56,13 @@ } this.setOptions(options); this.setCoords(); - if (options.sourcePath) { this.setSourcePath(options.sourcePath); } + if (this.objectCaching) { + this._createCacheCanvas(); + this.setupState({ propertySet: 'cacheProperties' }); + } }, /** @@ -93,32 +96,39 @@ }, /** - * Renders this group on a specified context - * @param {CanvasRenderingContext2D} ctx Context to render this instance on + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed */ - render: function(ctx) { - // do not render if object is not visible - if (!this.visible) { - return; - } - + drawObject: function(ctx) { ctx.save(); - - if (this.transformMatrix) { - ctx.transform.apply(ctx, this.transformMatrix); - } - this.transform(ctx); - - this._setShadow(ctx); - this.clipTo && fabric.util.clipContext(this, ctx); ctx.translate(-this.width / 2, -this.height / 2); for (var i = 0, l = this.paths.length; i < l; ++i) { this.paths[i].render(ctx, true); } - this.clipTo && ctx.restore(); ctx.restore(); }, + /** + * Check if cache is dirty + */ + isCacheDirty: function() { + if (this.callSuper('isCacheDirty')) { + return true + } + if (!this.statefullCache) { + return false; + } + for (var i = 0, len = this.paths.length; i < len; i++) { + if (this.paths[i].isCacheDirty(true)) { + var dim = this._getNonTransformedDimensions(); + this._cacheContext.clearRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y); + return true + } + } + return false; + }, + /** * Sets certain property to a certain value * @param {String} prop diff --git a/src/shapes/polygon.class.js b/src/shapes/polygon.class.js index ed4feacb..7b48f66e 100644 --- a/src/shapes/polygon.class.js +++ b/src/shapes/polygon.class.js @@ -56,7 +56,7 @@ * @return {fabric.Polygon} thisArg */ initialize: function(points, options) { - options = options || { }; + options = options || {}; this.points = points || []; this.callSuper('initialize', options); this._calcDimensions(); diff --git a/src/shapes/rect.class.js b/src/shapes/rect.class.js index 0077d368..000546f7 100644 --- a/src/shapes/rect.class.js +++ b/src/shapes/rect.class.js @@ -62,8 +62,6 @@ * @return {Object} thisArg */ initialize: function(options) { - options = options || { }; - this.callSuper('initialize', options); this._initRxRy(); diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 64ff0bc8..ace31fa1 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -23,9 +23,24 @@ 'textAlign', 'fontStyle', 'lineHeight', - 'textBackgroundColor' + 'textBackgroundColor', + 'charSpacing' ); + var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); + cacheProperties.push( + 'fontFamily', + 'fontWeight', + 'fontSize', + 'text', + 'textDecoration', + 'textAlign', + 'fontStyle', + 'lineHeight', + 'textBackgroundColor', + 'charSpacing', + 'styles' + ); /** * Text class * @class fabric.Text @@ -41,17 +56,16 @@ * @type Object * @private */ - _dimensionAffectingProps: { - fontSize: true, - fontWeight: true, - fontFamily: true, - fontStyle: true, - lineHeight: true, - text: true, - charSpacing: true, - textAlign: true, - strokeWidth: false, - }, + _dimensionAffectingProps: [ + 'fontSize', + 'fontWeight', + 'fontFamily', + 'fontStyle', + 'lineHeight', + 'text', + 'charSpacing', + 'textAlign' + ], /** * @private @@ -290,6 +304,12 @@ */ stateProperties: stateProperties, + /** + * List of properties to consider when checking if cache needs refresh + * @type Array + */ + cacheProperties: cacheProperties, + /** * 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 @@ -336,9 +356,10 @@ options = options || { }; this.text = text; this.__skipDimension = true; - this.setOptions(options); + this.callSuper('initialize', options); this.__skipDimension = false; this._initDimensions(); + this.setupState({ propertySet: '_dimensionAffectingProps' }); }, /** @@ -377,16 +398,13 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { - this.clipTo && fabric.util.clipContext(this, ctx); - this._setOpacity(ctx); - this._setShadow(ctx); - this._setupCompositeOperation(ctx); - this._renderTextBackground(ctx); - this._setStrokeStyles(ctx); - this._setFillStyles(ctx); + this._setTextStyles(ctx); + if (this.group && this.group.type === 'path-group') { + ctx.translate(this.left, this.top); + } + this._renderTextLinesBackground(ctx); this._renderText(ctx); this._renderTextDecoration(ctx); - this.clipTo && ctx.restore(); }, /** @@ -629,15 +647,6 @@ return this.fontSize * this._fontSizeMult; }, - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderTextBackground: function(ctx) { - this._renderBackground(ctx); - this._renderTextLinesBackground(ctx); - }, - /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -647,7 +656,7 @@ return; } var lineTopOffset = 0, heightOfLine, - lineWidth, lineLeftOffset; + lineWidth, lineLeftOffset, originalFill = ctx.fillStye; ctx.fillStyle = this.textBackgroundColor; for (var i = 0, len = this._textLines.length; i < len; i++) { @@ -664,6 +673,7 @@ } lineTopOffset += heightOfLine; } + ctx.fillStyle = originalFill; // if there is text background color no // other shadows should be casted this._removeShadow(ctx); @@ -695,17 +705,15 @@ /** * @private */ - _shouldClearCache: function() { + _shouldClearDimensionCache: function() { var shouldClear = false; if (this._forceClearCache) { this._forceClearCache = false; return true; } - for (var prop in this._dimensionAffectingProps) { - if (this['__' + prop] !== this[prop]) { - this['__' + prop] = this[prop]; - shouldClear = true; - } + shouldClear = this.hasStateChanged('_dimensionAffectingProps'); + if (shouldClear) { + this.saveState({ propertySet: '_dimensionAffectingProps' }); } return shouldClear; }, @@ -836,25 +844,10 @@ if (!this.visible) { return; } - - ctx.save(); - this._setTextStyles(ctx); - - if (this._shouldClearCache()) { + if (this._shouldClearDimensionCache()) { this._initDimensions(ctx); } - this.drawSelectionBackground(ctx); - if (!noTransform) { - this.transform(ctx); - } - if (this.transformMatrix) { - ctx.transform.apply(ctx, this.transformMatrix); - } - if (this.group && this.group.type === 'path-group') { - ctx.translate(this.left, this.top); - } - this._render(ctx); - ctx.restore(); + this.callSuper('render', ctx, noTransform); }, /** @@ -1086,7 +1079,7 @@ _set: function(key, value) { this.callSuper('_set', key, value); - if (key in this._dimensionAffectingProps) { + if (this._dimensionAffectingProps.indexOf(key) > -1) { this._initDimensions(); this.setCoords(); } diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js index 870c174a..962ed8ab 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -66,12 +66,12 @@ * @return {fabric.Textbox} thisArg */ initialize: function(text, options) { - this.ctx = fabric.util.createCanvasElement().getContext('2d'); + this.callSuper('initialize', text, options); this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); - + this.ctx = this.objectCaching ? this._cacheContext : fabric.util.createCanvasElement().getContext('2d'); // add width to this list of props that effect line wrapping. - this._dimensionAffectingProps.width = true; + this._dimensionAffectingProps.push('width'); }, /** diff --git a/src/shapes/triangle.class.js b/src/shapes/triangle.class.js index ef5797e3..791b0cf1 100644 --- a/src/shapes/triangle.class.js +++ b/src/shapes/triangle.class.js @@ -31,12 +31,9 @@ * @return {Object} thisArg */ initialize: function(options) { - options = options || { }; - this.callSuper('initialize', options); - - this.set('width', options.width || 100) - .set('height', options.height || 100); + this.set('width', options && options.width || 100) + .set('height', options && options.height || 100); }, /** diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index a7691a70..b353e372 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -93,7 +93,7 @@ * @type Boolean * @default */ - stateful: true, + stateful: false, /** * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove} should also re-render canvas. @@ -645,7 +645,7 @@ * @return {Number} */ getZoom: function () { - return Math.sqrt(this.viewportTransform[0] * this.viewportTransform[3]); + return this.viewportTransform[0]; }, /** diff --git a/src/util/lang_object.js b/src/util/lang_object.js index 8961651a..887260ae 100644 --- a/src/util/lang_object.js +++ b/src/util/lang_object.js @@ -23,7 +23,9 @@ } else if (source instanceof Object) { for (var property in source) { - destination[property] = clone(source[property], deep) + if (source.hasOwnProperty(property)) { + destination[property] = clone(source[property], deep) + } } } else { diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 035f34c5..fa241505 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -140,7 +140,8 @@ var Canvas = fabric.Canvas; fabric.Canvas = null; var el = fabric.document.createElement('canvas'); - el.width = 600; el.height = 600; + el.width = 600; + el.height = 600; var canvas = this.canvas = fabric.isLikelyNode ? fabric.createCanvasForNode() : new fabric.StaticCanvas(el), canvas2 = this.canvas2 = fabric.isLikelyNode ? fabric.createCanvasForNode() : new fabric.StaticCanvas(el); @@ -180,7 +181,7 @@ ok('overlayVpt' in canvas); equal(canvas.includeDefaultValues, true); - equal(canvas.stateful, true); + equal(canvas.stateful, false); equal(canvas.renderOnAddRemove, true); equal(canvas.controlsAboveOverlay, false); equal(canvas.allowTouchScrolling, false); diff --git a/test/unit/group.js b/test/unit/group.js index a9266709..53ab9d57 100644 --- a/test/unit/group.js +++ b/test/unit/group.js @@ -516,7 +516,7 @@ test('test group transformMatrix', function() { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1}), - rect2 = new fabric.Rect({ top: 4, left: 4, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1}), group = new fabric.Group([rect1, rect2], {opacity: 1, fill: 'blue', strokeWidth: 0}), isTransparent = fabric.util.isTransparent, ctx = canvas.contextContainer; @@ -526,14 +526,25 @@ equal(isTransparent(ctx, 1, 1, 0), false, '1,1 is opaque'); equal(isTransparent(ctx, 2, 2, 0), false, '2,2 is opaque'); equal(isTransparent(ctx, 3, 3, 0), true, '3,3 is transparent'); - equal(isTransparent(ctx, 4, 4, 0), false, '4,4 is opaque'); - group.transformMatrix = [2, 0, 0, 2, 1, 1]; + equal(isTransparent(ctx, 4, 4, 0), true, '4,4 is transparent'); + equal(isTransparent(ctx, 5, 5, 0), false, '5,5 is opaque'); + equal(isTransparent(ctx, 6, 6, 0), false, '6,6 is opaque'); + equal(isTransparent(ctx, 7, 7, 0), true, '7,7 is transparent'); + group.transformMatrix = [2, 0, 0, 2, 2, 2]; canvas.renderAll(); - equal(isTransparent(ctx, 0, 0, 0), true, '0,0 is transparent'); - equal(isTransparent(ctx, 1, 1, 0), true, '1,1 is transparent'); - equal(isTransparent(ctx, 2, 2, 0), true, '2,2 is transparent'); + equal(isTransparent(ctx, 0, 0, 0), false, '0,0 is opaque'); + equal(isTransparent(ctx, 1, 1, 0), false, '1,1 is opaque'); + equal(isTransparent(ctx, 2, 2, 0), false, '2,2 is opaque'); equal(isTransparent(ctx, 3, 3, 0), false, '3,3 is opaque'); - equal(isTransparent(ctx, 4, 4, 0), false, '4,4 is opaque'); + equal(isTransparent(ctx, 4, 4, 0), true, '4,4 is transparent'); + equal(isTransparent(ctx, 5, 5, 0), true, '5,5 is transparent'); + equal(isTransparent(ctx, 6, 6, 0), true, '6,6 is transparent'); + equal(isTransparent(ctx, 7, 7, 0), true, '7,7 is transparent'); + equal(isTransparent(ctx, 8, 8, 0), false, '8,8 is opaque'); + equal(isTransparent(ctx, 9, 9, 0), false, '9,9 is opaque'); + equal(isTransparent(ctx, 10, 10, 0), false, '10,10 is opaque'); + equal(isTransparent(ctx, 11, 11, 0), false, '11,11 is opaque'); + equal(isTransparent(ctx, 12, 12, 0), true, '12,12 is transparent'); }); // asyncTest('cloning group with image', function() { // var rect = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10 }), diff --git a/test/unit/stateful.js b/test/unit/stateful.js index 93d506ae..a0638814 100644 --- a/test/unit/stateful.js +++ b/test/unit/stateful.js @@ -20,8 +20,8 @@ cObj.set('left', 123).set('top', 456); cObj.saveState(); cObj.set('left', 223).set('top', 556); - equal(cObj.originalState.left, 123); - equal(cObj.originalState.top, 456); + equal(cObj._stateProperties.left, 123); + equal(cObj._stateProperties.top, 456); }); test('saveState with extra props', function() { @@ -32,19 +32,19 @@ var extraProps = ['prop1', 'prop2']; var options = { stateProperties: extraProps }; cObj.setupState(options); - equal(cObj.originalState.prop1, 'a', 'it saves the extra props'); - equal(cObj.originalState.prop2, 'b', 'it saves the extra props'); + equal(cObj._stateProperties.prop1, 'a', 'it saves the extra props'); + equal(cObj._stateProperties.prop2, 'b', 'it saves the extra props'); cObj.prop1 = 'c'; ok(cObj.hasStateChanged(), 'it detects changes in extra props'); - equal(cObj.originalState.left, 123, 'normal props are still there'); + equal(cObj._stateProperties.left, 123, 'normal props are still there'); }); test('saveState with array', function() { var cObj = new fabric.Text('Hello'); cObj.set('textDecoration', ['underline']); cObj.setupState(); - deepEqual(cObj.textDecoration, cObj.originalState.textDecoration, 'textDecoration in state is deepEqual'); - notEqual(cObj.textDecoration, cObj.originalState.textDecoration, 'textDecoration in not same Object'); + deepEqual(cObj.textDecoration, cObj._stateProperties.textDecoration, 'textDecoration in state is deepEqual'); + notEqual(cObj.textDecoration, cObj._stateProperties.textDecoration, 'textDecoration in not same Object'); cObj.textDecoration[0] = 'overline'; ok(cObj.hasStateChanged(), 'hasStateChanged detects changes in nested props');