diff --git a/src/elements_parser.js b/src/elements_parser.js index 30aa7eedb26..0fa77b69352 100644 --- a/src/elements_parser.js +++ b/src/elements_parser.js @@ -4,6 +4,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver) { this.options = options; this.reviver = reviver; this.svgUid = (options && options.svgUid) || 0; + this.fillers = ['fill', 'stroke']; }; fabric.ElementsParser.prototype.parse = function() { @@ -40,41 +41,55 @@ fabric.ElementsParser.prototype.createObject = function(el, index) { }; fabric.ElementsParser.prototype._createObject = function(klass, el, index) { + var callback = this.createCallback(index, el); if (klass.async) { - klass.fromElement(el, this.createCallback(index, el), this.options); + klass.fromElement(el, callback, this.options); } else { var obj = klass.fromElement(el, this.options); - this.resolveGradient(obj, 'fill'); - this.resolveGradient(obj, 'stroke'); - this.reviver && this.reviver(el, obj); - this.instances[index] = obj; - this.checkIfDone(); + callback(obj); } }; fabric.ElementsParser.prototype.createCallback = function(index, el) { var _this = this; return function(obj) { - _this.resolveGradient(obj, 'fill'); - _this.resolveGradient(obj, 'stroke'); - _this.reviver && _this.reviver(el, obj); - _this.instances[index] = obj; - _this.checkIfDone(); + _this.resolveFillers(obj, function(_obj) { + _this.reviver && _this.reviver(el, _obj); + _this.instances[index] = _obj; + _this.checkIfDone(); + }); }; }; -fabric.ElementsParser.prototype.resolveGradient = function(obj, property) { - - var instanceFillValue = obj.get(property); - if (!(/^url\(/).test(instanceFillValue)) { - return; - } - var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - if (fabric.gradientDefs[this.svgUid][gradientId]) { - obj.set(property, - fabric.Gradient.fromElement(fabric.gradientDefs[this.svgUid][gradientId], obj)); - } +fabric.ElementsParser.prototype.resolveFillers = function(obj, callback) { + var counter, + _callback = function(_obj) { + counter++; + if (counter === 2) { + callback(_obj); + } + }; + this.fillers.forEach(function(filler) { + var instanceFillValue = obj.get(filler); + if (!(/^url\(/).test(instanceFillValue)) { + if (counter) { + _callback(obj); + return; + } + } + var fillerId = instanceFillValue.slice(5, instanceFillValue.length - 1); + if (fabric.gradientDefs[this.svgUid][fillerId]) { + obj.set(filler, fabric.Gradient.fromElement(fabric.gradientDefs[this.svgUid][fillerId], obj)); + _callback(obj); + } + else if (fabric.patternDefs[this.svgUid][fillerId]) { + fabric.Pattern.fromElement(fabric.patternDefs[this.svgUid][fillerId], obj, function(_pattern) { + obj.set(filler, _pattern); + _callback(obj); + }); + } + }); }; fabric.ElementsParser.prototype.checkIfDone = function() { diff --git a/src/parser.js b/src/parser.js index baec5f83209..70cbe25c2c9 100644 --- a/src/parser.js +++ b/src/parser.js @@ -17,7 +17,7 @@ reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/i, reViewBoxTagNames = /^(symbol|image|marker|pattern|view|svg)$/i, - reNotAllowedAncestors = /^(?:pattern|defs|symbol|metadata)$/i, + reNotAllowedAncestors = /^(?:pattern|defs|symbol|metadata|clippath)$/i, reAllowedParents = /^(symbol|g|a|svg)$/i, attributesMap = { @@ -52,6 +52,7 @@ fabric.cssRules = { }; fabric.gradientDefs = { }; + fabric.patternDefs = { }; function normalizeAttr(attr) { // transform attribute names @@ -636,13 +637,20 @@ return; } - fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); + var gradientTags = ['linearGradient', 'radialGradient', 'svg:linearGradient', 'svg:radialGradient']; + var patternTags = ['pattern', 'svg:pattern']; + + fabric.gradientDefs[svgUid] = fabric.getFillerDefs(doc, gradientTags); fabric.cssRules[svgUid] = fabric.getCSSRules(doc); + fabric.patternDefs[svgUid] = fabric.getFillerDefs(doc, patternTags); // Precedence of rules: style > class > attribute fabric.parseElements(elements, function(instances) { fabric.documentParsingTime = new Date() - startTime; if (callback) { callback(instances, options); + delete fabric.gradientDefs[svgUid]; + delete fabric.cssRules[svgUid]; + delete fabric.patterns[svgUid]; } }, clone(options), reviver); }; @@ -752,22 +760,17 @@ }, /** - * Parses an SVG document, returning all of the gradient declarations found in it + * Parses an SVG document, returning all of the gradient or patterns declarations found in it * @static * @function * @memberOf fabric * @param {SVGDocument} doc SVG document to parse - * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + * @return {Object} Gradient or Patterns definitions; key corresponds to element id, value -- to gradient definition element */ - getGradientDefs: function(doc) { - var tagArray = [ - 'linearGradient', - 'radialGradient', - 'svg:linearGradient', - 'svg:radialGradient'], - elList = _getMultipleNodes(doc, tagArray), + getFillerDefs: function(doc, tagArray) { + var elList = _getMultipleNodes(doc, tagArray), el, j = 0, id, xlink, - gradientDefs = { }, idsToXlinkMap = { }; + defs = { }, idsToXlinkMap = { }; j = elList.length; @@ -778,17 +781,17 @@ if (xlink) { idsToXlinkMap[id] = xlink.substr(1); } - gradientDefs[id] = el; + defs[id] = el; } for (id in idsToXlinkMap) { - var el2 = gradientDefs[idsToXlinkMap[id]].cloneNode(true); - el = gradientDefs[id]; + var el2 = defs[idsToXlinkMap[id]].cloneNode(true); + el = defs[id]; while (el2.firstChild) { el.appendChild(el2.firstChild); } } - return gradientDefs; + return defs; }, /** diff --git a/src/pattern.class.js b/src/pattern.class.js index 7e772823007..7d8b9f2f4ad 100644 --- a/src/pattern.class.js +++ b/src/pattern.class.js @@ -1,171 +1,278 @@ -/** - * Pattern class - * @class fabric.Pattern - * @see {@link http://fabricjs.com/patterns|Pattern demo} - * @see {@link http://fabricjs.com/dynamic-patterns|DynamicPattern demo} - * @see {@link fabric.Pattern#initialize} for constructor definition - */ -fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { +(function() { - /** - * Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) - * @type String - * @default - */ - repeat: 'repeat', + 'use strict'; - /** - * Pattern horizontal offset from object's left/top corner - * @type Number - * @default - */ - offsetX: 0, + var toFixed = fabric.util.toFixed; /** - * Pattern vertical offset from object's left/top corner - * @type Number - * @default + * Pattern class + * @class fabric.Pattern + * @see {@link http://fabricjs.com/patterns|Pattern demo} + * @see {@link http://fabricjs.com/dynamic-patterns|DynamicPattern demo} + * @see {@link fabric.Pattern#initialize} for constructor definition */ - offsetY: 0, - /** - * Constructor - * @param {Object} [options] Options object - * @return {fabric.Pattern} thisArg - */ - initialize: function(options) { - options || (options = { }); - this.id = fabric.Object.__uid++; + fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { - if (options.source) { - if (typeof options.source === 'string') { - // function string - if (typeof fabric.util.getFunctionBody(options.source) !== 'undefined') { - this.source = new Function(fabric.util.getFunctionBody(options.source)); - } - else { - // img src string - var _this = this; - this.source = fabric.util.createImage(); - fabric.util.loadImage(options.source, function(img) { - _this.source = img; - }); - } + /** + * Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) + * @type String + * @default + */ + repeat: 'repeat', + + /** + * Pattern horizontal offset from object's left/top corner + * @type Number + * @default + */ + offsetX: 0, + + /** + * Pattern vertical offset from object's left/top corner + * @type Number + * @default + */ + offsetY: 0, + + /** + * Pattern rotation angle, relative to center of object where the pattern is applied + * @type Number + * @default + */ + angle: 0, + + /** + * Pattern scale factor on X axis + * @type Number + * @default + */ + scaleX: 1, + + /** + * Pattern scale factor on Y axis + * @type Number + * @default + */ + scaleY: 1, + + /** + * Determines if a pattern scale with object container + * @type Boolean + * @default + */ + scaleWithObject: true, + + /** + * pattern transformation matrix, imported from SVGs file + * @type [Array] array of 6 numbers + * @default + */ + patternTransform: null, + + /** + * Constructor + * @param {Object} [options] Options object + * @param {Function} [callback] function to invoke after callback init. + * @return {fabric.Pattern} thisArg + */ + initialize: function(options, callback) { + options || (options = { }); + + this.id = fabric.Object.__uid++; + this.setOptions(options); + if (!options.source || (options.source && options.source !== 'string')) { + callback && callback(this); + return; + } + // function string + if (typeof fabric.util.getFunctionBody(options.source) !== 'undefined') { + this.source = new Function(fabric.util.getFunctionBody(options.source)); + callback && callback(this); } else { - // img element - this.source = options.source; + // img src string + var _this = this; + this.source = fabric.util.createImage(); + fabric.util.loadImage(options.source, function(img) { + _this.source = img; + callback && callback(_this); + }); } - } - if (options.repeat) { - this.repeat = options.repeat; - } - if (options.offsetX) { - this.offsetX = options.offsetX; - } - if (options.offsetY) { - this.offsetY = options.offsetY; - } - }, + }, - /** - * Returns object representation of a pattern - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of a pattern instance - */ - toObject: function(propertiesToInclude) { + /** + * Returns object representation of a pattern + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of a pattern instance + */ + toObject: function(propertiesToInclude) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + source, object; - var source, object; + // callback + if (typeof this.source === 'function') { + source = String(this.source); + } + // element + else if (typeof this.source.src === 'string') { + source = this.source.src; + } + // element + else if (typeof this.source === 'object' && this.source.toDataURL) { + source = this.source.toDataURL(); + } - // callback - if (typeof this.source === 'function') { - source = String(this.source); - } - // element - else if (typeof this.source.src === 'string') { - source = this.source.src; - } - // element - else if (typeof this.source === 'object' && this.source.toDataURL) { - source = this.source.toDataURL(); - } + object = { + type: 'pattern', + source: source, + repeat: this.repeat, + offsetX: toFixed(this.offsetX, NUM_FRACTION_DIGITS), + offsetY: toFixed(this.offsetY, NUM_FRACTION_DIGITS), + angle: toFixed(this.angle, NUM_FRACTION_DIGITS), + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + scaleWithObject: this.scaleWithObject, + patternTransform: this.patternTransform ? this.patternTransform.concat() : null, + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); - object = { - source: source, - repeat: this.repeat, - offsetX: this.offsetX, - offsetY: this.offsetY - }; - fabric.util.populateWithProperties(this, object, propertiesToInclude); + return object; + }, - return object; - }, + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a pattern + * @param {fabric.Object} object + * @return {String} SVG representation of a pattern + */ + toSVG: function(object) { + var patternSource = typeof this.source === 'function' ? this.source() : this.source, + patternWidth = patternSource.width / object.getWidth(), + patternHeight = patternSource.height / object.getHeight(), + patternOffsetX = this.offsetX / object.getWidth(), + patternOffsetY = this.offsetY / object.getHeight(), + patternImgSrc = ''; + if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { + patternHeight = 1; + } + if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { + patternWidth = 1; + } + if (patternSource.src) { + patternImgSrc = patternSource.src; + } + else if (patternSource.toDataURL) { + patternImgSrc = patternSource.toDataURL(); + } - /* _TO_SVG_START_ */ - /** - * Returns SVG representation of a pattern - * @param {fabric.Object} object - * @return {String} SVG representation of a pattern - */ - toSVG: function(object) { - var patternSource = typeof this.source === 'function' ? this.source() : this.source, - patternWidth = patternSource.width / object.getWidth(), - patternHeight = patternSource.height / object.getHeight(), - patternOffsetX = this.offsetX / object.getWidth(), - patternOffsetY = this.offsetY / object.getHeight(), - patternImgSrc = ''; - if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { - patternHeight = 1; - } - if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { - patternWidth = 1; - } - if (patternSource.src) { - patternImgSrc = patternSource.src; - } - else if (patternSource.toDataURL) { - patternImgSrc = patternSource.toDataURL(); - } + return '\n' + + '\n' + + '\n'; + }, + /* _TO_SVG_END_ */ - return '\n' + - '\n' + - '\n'; - }, - /* _TO_SVG_END_ */ + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, - /** - * Returns an instance of CanvasPattern - * @param {CanvasRenderingContext2D} ctx Context to create pattern - * @return {CanvasPattern} - */ - toLive: function(ctx) { - var source = typeof this.source === 'function' - ? this.source() - : this.source; - - // if the image failed to load, return, and allow rest to continue loading - if (!source) { - return ''; - } + /** + * Returns an instance of CanvasPattern + * @param {CanvasRenderingContext2D} ctx Context to create pattern + * @param {Object} object for wich the pattern is created for + * @param {Boolean} enforceTransform enforce transform on the pattern source ( used for fillText ) + * @return {CanvasPattern} + */ + toLive: function(ctx, object, enforceTransform) { + var source = typeof this.source === 'function' ? this.source() : this.source; - // if an image - if (typeof source.src !== 'undefined') { - if (!source.complete) { + // if the image failed to load, return, and allow rest to continue loading + if (!source) { return ''; } - if (source.naturalWidth === 0 || source.naturalHeight === 0) { - return ''; + + // if an image + if (typeof source.src !== 'undefined') { + if (!source.complete) { + return ''; + } + if (source.naturalWidth === 0 || source.naturalHeight === 0) { + return ''; + } + } + if (!enforceTransform) { + return ctx.createPattern(source, this.repeat); + } + + var width = source.width, height = source.height, options, + scaleX = this.scaleX, scaleY = this.scaleY, + canvas = fabric.util.createCanvasElement(), + ctx = canvas.getContext('2d'); + if (this.patternTransform) { + var options = fabric.util.qrDecompose(this.patternTransform); + scaleX *= Math.abs(options.scaleX); + scaleY *= Math.abs(options.scaleY); + } + if (!this.scaleWithObject) { + scaleX /= object.scaleX; + scaleY /= object.scaleY; } + // in case of pattern rotation we create a pattern as big as the object + // because we cannot really find a repetable pattern after rotating the original pattern + canvas.width = this.angle ? object.width : Math.min(width * scaleX, object.width); + canvas.height = this.angle ? object.height : Math.min(height * scaleY, object.height); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(canvas.width, 0); + ctx.lineTo(canvas.width, canvas.height); + ctx.lineTo(0, canvas.height); + ctx.lineTo(0, 0); + ctx.translate(object.width / 2, object.height / 2); + fabric.Object.prototype._applyPatternGradientTransform.call(object, ctx, this); + ctx.fillStyle = ctx.createPattern(source, this.repeat); + ctx.fill(); + return ctx.createPattern(canvas, this.repeat); + } + }); + + fabric.util.object.extend(fabric.Pattern, { + + /* _FROM_SVG_START_ */ + /** + * Returns {@link fabric.Pattern} instance from an SVG element + * @static + * @memberOf fabric.Pattern + * @param {SVGPatternElement} el SVG pattern element with childnodes + * @param {fabric.Object} instance object that is stroked/filled with pattern + * @param {function} callback to invoke when the pattern is loaded + * @see http://www.w3.org/TR/SVG/pservers.html#PatternElement + */ + fromElement: function(el, instance, callback) { + + /** + * @example: + * + * + * + * + * + * + * OR + */ + + var pattern = new fabric.Pattern({ }, callback); } - return ctx.createPattern(source, this.repeat); - } -}); + /* _FROM_SVG_END_ */ + }); +})(); diff --git a/src/shapes/circle.class.js b/src/shapes/circle.class.js index 676523622fd..401178a83d1 100644 --- a/src/shapes/circle.class.js +++ b/src/shapes/circle.class.js @@ -237,12 +237,11 @@ * @memberOf fabric.Circle * @param {Object} object Object to create an instance from * @param {function} [callback] invoked with new instance as first argument + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first * @return {Object} Instance of fabric.Circle */ - fabric.Circle.fromObject = function(object, callback) { - var circle = new fabric.Circle(object); - callback && callback(circle); - return circle; + fabric.Circle.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Circle', object, callback, forceAsync); }; })(typeof exports !== 'undefined' ? exports : this); diff --git a/src/shapes/ellipse.class.js b/src/shapes/ellipse.class.js index ccce29be5b6..4829a965c48 100644 --- a/src/shapes/ellipse.class.js +++ b/src/shapes/ellipse.class.js @@ -205,12 +205,11 @@ * @memberOf fabric.Ellipse * @param {Object} object Object to create an instance from * @param {function} [callback] invoked with new instance as first argument + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first * @return {fabric.Ellipse} */ - fabric.Ellipse.fromObject = function(object, callback) { - var ellipse = new fabric.Ellipse(object); - callback && callback(ellipse); - return ellipse; + fabric.Ellipse.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Ellipse', object, callback, forceAsync); }; })(typeof exports !== 'undefined' ? exports : this); diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index f68c53cfbdf..ad4b62825bd 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -948,8 +948,8 @@ this.set(prop, options[prop]); } this._initGradient(options); - this._initPattern(options); this._initClipping(options); + this._initPattern(options); }, /** @@ -1007,7 +1007,7 @@ backgroundColor: this.backgroundColor, fillRule: this.fillRule, globalCompositeOperation: this.globalCompositeOperation, - transformMatrix: this.transformMatrix ? this.transformMatrix.concat() : this.transformMatrix, + transformMatrix: this.transformMatrix ? this.transformMatrix.concat() : null, skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS), skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS) }; @@ -1323,7 +1323,7 @@ ctx.lineJoin = this.strokeLineJoin; ctx.miterLimit = this.strokeMiterLimit; ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx, this) + ? this.stroke.toLive(ctx, this, this instanceof fabric.Text) : this.stroke; } }, @@ -1331,7 +1331,7 @@ _setFillStyles: function(ctx) { if (this.fill) { ctx.fillStyle = this.fill.toLive - ? this.fill.toLive(ctx, this) + ? this.fill.toLive(ctx, this, this instanceof fabric.Text) : this.fill; } }, @@ -1429,6 +1429,31 @@ ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; }, + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} filler fabric.Pattern or fabric.Gradient + */ + _applyPatternGradientTransform: function(ctx, filler) { + if (!filler.toLive) { + return; + } + var transform = filler.gradientTransform || filler.patternTransform; + if (transform) { + ctx.transform.apply(ctx, transform); + } + var offsetX = -this.width / 2 + filler.offsetX || 0, + offsetY = -this.height / 2 + filler.offsetY || 0; + ctx.translate(offsetX, offsetY); + if (!filler.scaleWithObject) { + ctx.scale(1 / this.scaleX, 1 / this.scaleY); + } + ctx.scale(filler.scaleX, filler.scaleY); + if (filler.angle) { + ctx.rotate(degreesToRadians(filler.angle)); + } + }, + /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -1439,15 +1464,7 @@ } ctx.save(); - if (this.fill.gradientTransform) { - var g = this.fill.gradientTransform; - ctx.transform.apply(ctx, g); - } - if (this.fill.toLive) { - ctx.translate( - -this.width / 2 + this.fill.offsetX || 0, - -this.height / 2 + this.fill.offsetY || 0); - } + this._applyPatternGradientTransform(ctx, this.fill); if (this.fillRule === 'evenodd') { ctx.fill('evenodd'); } @@ -1473,15 +1490,7 @@ ctx.save(); this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke); - if (this.stroke.gradientTransform) { - var g = this.stroke.gradientTransform; - ctx.transform.apply(ctx, g); - } - if (this.stroke.toLive) { - ctx.translate( - -this.width / 2 + this.stroke.offsetX || 0, - -this.height / 2 + this.stroke.offsetY || 0); - } + this._applyPatternGradientTransform(ctx, this.stroke); ctx.stroke(); ctx.restore(); }, @@ -1848,7 +1857,7 @@ objectLeftTop = this._getLeftTopCoords(); if (this.angle) { pClicked = fabric.util.rotatePoint( - pClicked, objectLeftTop, fabric.util.degreesToRadians(-this.angle)); + pClicked, objectLeftTop, degreesToRadians(-this.angle)); } return { x: pClicked.x - objectLeftTop.x, @@ -1889,6 +1898,22 @@ */ fabric.Object.NUM_FRACTION_DIGITS = 2; + fabric.Object._fromObject = function(className, object, callback, forceAsync) { + if (forceAsync) { + fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { + object.fill = patterns[0]; + object.stroke = patterns[1]; + var instance = new fabric[className](object); + callback && callback(instance); + }); + } + else { + var instance = new fabric[className](object); + callback && callback(instance); + return instance; + } + }; + /** * Unique id used internally when creating SVG elements * @static diff --git a/src/shapes/rect.class.js b/src/shapes/rect.class.js index f656b360753..815084c4a90 100644 --- a/src/shapes/rect.class.js +++ b/src/shapes/rect.class.js @@ -64,7 +64,6 @@ initialize: function(options) { this.callSuper('initialize', options); this._initRxRy(); - }, /** @@ -225,12 +224,11 @@ * @memberOf fabric.Rect * @param {Object} object Object to create an instance from * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first * @return {Object} instance of fabric.Rect */ - fabric.Rect.fromObject = function(object, callback) { - var rect = new fabric.Rect(object); - callback && callback(rect); - return rect; + fabric.Rect.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Rect', object, callback, forceAsync); }; })(typeof exports !== 'undefined' ? exports : this); diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 275ff5535d4..9420347e56c 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -476,10 +476,11 @@ */ _renderChars: function(method, ctx, chars, left, top) { // remove Text word from method var - var shortM = method.slice(0, -4), char, width; - if (this[shortM].toLive) { - var offsetX = -this.width / 2 + this[shortM].offsetX || 0, - offsetY = -this.height / 2 + this[shortM].offsetY || 0; + var shortM = method.slice(0, -4), char, width, + filler = this[shortM]; + if (filler.toLive) { + var offsetX = -this.width / 2 + filler.offsetX || 0, + offsetY = -this.height / 2 + filler.offsetY || 0; ctx.save(); ctx.translate(offsetX, offsetY); left -= offsetX; @@ -498,7 +499,7 @@ else { ctx[method](chars, left, top); } - this[shortM].toLive && ctx.restore(); + filler.toLive && ctx.restore(); }, /** diff --git a/src/shapes/triangle.class.js b/src/shapes/triangle.class.js index 791b0cf1b09..44357111999 100644 --- a/src/shapes/triangle.class.js +++ b/src/shapes/triangle.class.js @@ -108,17 +108,16 @@ }); /** - * Returns fabric.Triangle instance from an object representation + * Returns {@link fabric.Ellipse} instance from an object representation * @static - * @memberOf fabric.Triangle + * @memberOf fabric.Ellipse * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Triangle instance is created - * @return {Object} instance of Canvas.Triangle + * @param {function} [callback] invoked with new instance as first argument + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {fabric.Ellipse} */ - fabric.Triangle.fromObject = function(object, callback) { - var triangle = new fabric.Triangle(object); - callback && callback(triangle); - return triangle; + fabric.Triangle.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Triangle', object, callback, forceAsync); }; })(typeof exports !== 'undefined' ? exports : this); diff --git a/src/util/misc.js b/src/util/misc.js index 7b74fc69676..e2169c942e1 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -312,7 +312,8 @@ var enlivenedObjects = [], numLoadedObjects = 0, - numTotalObjects = objects.length; + numTotalObjects = objects.length, + forceAsync = true; if (!numTotalObjects) { callback && callback(enlivenedObjects); @@ -326,18 +327,53 @@ return; } var klass = fabric.util.getKlass(o.type, namespace); - if (klass.async) { - klass.fromObject(o, function (obj, error) { - if (!error) { - enlivenedObjects[index] = obj; - reviver && reviver(o, enlivenedObjects[index]); - } + klass.fromObject(o, function (obj, error) { + if (!error) { + enlivenedObjects[index] = obj; + reviver && reviver(o, obj); + } + onLoaded(); + }, forceAsync); + }); + }, + + /** + * Create and wait for loading of patterns + * @static + * @memberOf fabric.util + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * @param {String} namespace Namespace to get klass "Class" object from + * @param {Function} reviver Method for further parsing of object elements, + * called after each fabric object created. + */ + enlivenPatterns: function(patterns, callback) { + patterns = patterns || []; + + function onLoaded() { + if (++numLoadedPatterns === numPatterns) { + callback && callback(enlivenedPatterns); + } + } + + var enlivenedPatterns = [], + numLoadedPatterns = 0, + numPatterns = patterns.length; + + if (!numPatterns) { + callback && callback(enlivenedPatterns); + return; + } + + patterns.forEach(function (p, index) { + if (p && p.source) { + new fabric.Pattern(p, function(pattern) { + enlivenedPatterns[index] = pattern; onLoaded(); }); } else { - enlivenedObjects[index] = klass.fromObject(o); - reviver && reviver(o, enlivenedObjects[index]); + enlivenedPatterns[index] = p; onLoaded(); } }); diff --git a/test/unit/canvas.js b/test/unit/canvas.js index 319b80e9e80..28f68aa61e7 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -700,7 +700,7 @@ ok(!canvas.isEmpty()); }); - test('loadFromJSON with json string', function() { + asyncTest('loadFromJSON with json string', function() { ok(typeof canvas.loadFromJSON == 'function'); canvas.loadFromJSON(PATH_JSON, function(){ @@ -725,10 +725,11 @@ equal(obj.get('flipY'), false); equal(obj.get('opacity'), 1); ok(obj.get('path').length > 0); + start(); }); }); - test('loadFromJSON with json object', function() { + asyncTest('loadFromJSON with json object', function() { ok(typeof canvas.loadFromJSON == 'function'); canvas.loadFromJSON(JSON.parse(PATH_JSON), function(){ @@ -753,10 +754,11 @@ equal(obj.get('flipY'), false); equal(obj.get('opacity'), 1); ok(obj.get('path').length > 0); + start(); }); }); - test('loadFromJSON with reviver function', function() { + asyncTest('loadFromJSON with reviver function', function() { ok(typeof canvas.loadFromJSON == 'function'); function reviver(obj, instance) { @@ -790,10 +792,11 @@ equal(obj.get('opacity'), 1); equal(obj.get('customID'), 'fabric_1'); ok(obj.get('path').length > 0); + start(); }, reviver); }); - test('loadFromJSON with no objects', function() { + asyncTest('loadFromJSON with no objects', function() { var canvas1 = fabric.document.createElement('canvas'), canvas2 = fabric.document.createElement('canvas'), c1 = new fabric.Canvas(canvas1, { backgroundColor: 'green', overlayColor: 'yellow' }), @@ -807,11 +810,11 @@ ok(fired, 'Callback should be fired even if no objects'); equal(c2.backgroundColor, 'green', 'Color should be set properly'); equal(c2.overlayColor, 'yellow', 'Color should be set properly'); - + start(); }); }); - test('loadFromJSON without "objects" property', function() { + asyncTest('loadFromJSON without "objects" property', function() { var canvas1 = fabric.document.createElement('canvas'), canvas2 = fabric.document.createElement('canvas'), c1 = new fabric.Canvas(canvas1, { backgroundColor: 'green', overlayColor: 'yellow' }), @@ -828,11 +831,11 @@ ok(fired, 'Callback should be fired even if no "objects" property exists'); equal(c2.backgroundColor, 'green', 'Color should be set properly'); equal(c2.overlayColor, 'yellow', 'Color should be set properly'); - + start(); }); }); - test('loadFromJSON with empty fabric.Group', function() { + asyncTest('loadFromJSON with empty fabric.Group', function() { var canvas1 = fabric.document.createElement('canvas'), canvas2 = fabric.document.createElement('canvas'), c1 = new fabric.Canvas(canvas1), @@ -848,6 +851,7 @@ fired = true; ok(fired, 'Callback should be fired even if empty fabric.Group exists'); + start(); }); }); @@ -897,7 +901,7 @@ }); }); - test('loadFromJSON with custom properties on Canvas with no async object', function() { + asyncTest('loadFromJSON with custom properties on Canvas with no async object', function() { var serialized = JSON.parse(PATH_JSON); serialized.controlsAboveOverlay = true; serialized.preserveObjectStacking = true; @@ -907,10 +911,8 @@ ok(!canvas.isEmpty(), 'canvas is not empty'); equal(canvas.controlsAboveOverlay, true); equal(canvas.preserveObjectStacking, true); + start(); }); - // if no async object the callback is called syncronously - equal(canvas.controlsAboveOverlay, true); - equal(canvas.preserveObjectStacking, true); }); asyncTest('loadFromJSON with custom properties on Canvas with image', function() { diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index e913ededa85..8d14d76a5bf 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -923,7 +923,7 @@ ok(!canvas.isEmpty()); }); - test('loadFromJSON with json string', function() { + asyncTest('loadFromJSON with json string', function() { ok(typeof canvas.loadFromJSON == 'function'); canvas.loadFromJSON(PATH_JSON, function(){ @@ -946,12 +946,12 @@ equal(obj.get('flipX'), false); equal(obj.get('flipY'), false); equal(obj.get('opacity'), 1); - ok(obj.get('path').length > 0); + start(); }); }); - test('loadFromJSON with json object', function() { + asyncTest('loadFromJSON with json object', function() { ok(typeof canvas.loadFromJSON == 'function'); canvas.loadFromJSON(JSON.parse(PATH_JSON), function(){ @@ -977,6 +977,7 @@ equal(obj.get('opacity'), 1); ok(obj.get('path').length > 0); + start(); }); }); @@ -992,7 +993,7 @@ }); }); - test('loadFromJSON custom properties', function() { + asyncTest('loadFromJSON custom properties', function() { var rect = new fabric.Rect({ width: 10, height: 20 }); rect.padding = 123; rect.foo = 'bar'; @@ -1011,6 +1012,7 @@ equal(obj.padding, 123, 'padding on object is set properly'); equal(obj.foo, 'bar', '"foo" property on object is set properly'); + start(); }); });