From 5017bc66266392a6d83841e68e6fd87e1ae417d3 Mon Sep 17 00:00:00 2001 From: Kienz Date: Sun, 3 Nov 2013 13:09:49 +0100 Subject: [PATCH] [BACK_INCOMPAT] `fabric.StaticCanvas#backgroundImage` and `fabric.StaticCanvas#overlayImage` are `fabric.Image` instances. `fabric.StaticCanvas#backgroundImageOpacity`, `fabric.StaticCanvas#backgroundImageStretch`, `fabric.StaticCanvas#overlayImageLeft` and `fabric.StaticCanvas#overlayImageTop` were removed. `backgroundImage` and `overlayImage` are now `fabric.Image` instances New property `overlayColor` (analog to `backgroundColor`) - should be set with `canvas.setOverlayColor` `backgroundImageOpacity` was removed => use `fabric.Image#opacity` `overlayImageLeft ` was removed => use `fabric.Image#left` `overlayImageTop ` was removed => use `fabric.Image#top` `backgroundImageStretch` was removed => use `fabric.Image#width` and `fabric.Image#height`. If you scale your canvas you have to adjust the backgroundImage width/height manually. Update SVG output. Add 2nd parameter `firstLetterOnly` to `fabric.util.string.capitalize` => only first letter is transformed to uppercase (other letters stay untouched) Add `preserveAspectRatio="none"` to `fabric.Image#toSVG` (otherwise streched backgroundImage and overlayImage didn't work as expected) Update examples - TODO: Update jsfiddles for `setBackgroundImage` and `setOverlayImage` Add unit test Closes issue #270 --- CHANGELOG.md | 2 + src/mixins/canvas_serialization.mixin.js | 96 ++--- src/parser.js | 32 +- src/shapes/image.class.js | 1 + src/static_canvas.class.js | 466 ++++++++++++++--------- src/util/lang_string.js | 6 +- test/unit/canvas.js | 41 +- test/unit/canvas_static.js | 183 +++++++-- 8 files changed, 536 insertions(+), 291 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d23f97f1..466afb4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ **Edge** +- [BACK_INCOMPAT] `fabric.StaticCanvas#backgroundImage` and `fabric.StaticCanvas#overlayImage` are `fabric.Image` instances. `fabric.StaticCanvas#backgroundImageOpacity`, `fabric.StaticCanvas#backgroundImageStretch`, `fabric.StaticCanvas#overlayImageLeft` and `fabric.StaticCanvas#overlayImageTop` were removed. + - [BACK_INCOMPAT] `fabric.Text#backgroundColor` is now `fabric.Object#backgroundColor` - [BACK_INCOMPAT] Remove `fabric.Object#toGrayscale` and `fabric.Object#overlayFill` since they're too specific diff --git a/src/mixins/canvas_serialization.mixin.js b/src/mixins/canvas_serialization.mixin.js index ec50af64..86bb3ad8 100644 --- a/src/mixins/canvas_serialization.mixin.js +++ b/src/mixins/canvas_serialization.mixin.js @@ -50,72 +50,74 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var _this = this; this._enlivenObjects(serialized.objects, function () { - _this._setBgOverlayImages(serialized, callback); + _this._setBgOverlay(serialized, callback); }, reviver); return this; }, - _setBgOverlayImages: function(serialized, callback) { - + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { var _this = this, - backgroundPatternLoaded, - backgroundImageLoaded, - overlayImageLoaded; + loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } var cbIfLoaded = function () { - callback && backgroundImageLoaded && overlayImageLoaded && backgroundPatternLoaded && callback(); + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + _this.renderAll(); + callback && callback(); + } }; - if (serialized.backgroundImage) { - this.setBackgroundImage(serialized.backgroundImage, function() { + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); - _this.backgroundImageOpacity = serialized.backgroundImageOpacity; - _this.backgroundImageStretch = serialized.backgroundImageStretch; + cbIfLoaded(); + }, - _this.renderAll(); + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; - backgroundImageLoaded = true; + if (!value) { + loaded[property] = true; + return; + } - cbIfLoaded(); + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.Image.fromObject(value, function(img) { + _this[property] = img; + loaded[property] = true; + callback && callback(); }); } else { - backgroundImageLoaded = true; - } - - if (serialized.overlayImage) { - this.setOverlayImage(serialized.overlayImage, function() { - - _this.overlayImageLeft = serialized.overlayImageLeft || 0; - _this.overlayImageTop = serialized.overlayImageTop || 0; - - _this.renderAll(); - overlayImageLoaded = true; - - cbIfLoaded(); + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); }); } - else { - overlayImageLoaded = true; - } - - if (serialized.background) { - this.setBackgroundColor(serialized.background, function() { - - _this.renderAll(); - backgroundPatternLoaded = true; - - cbIfLoaded(); - }); - } - else { - backgroundPatternLoaded = true; - } - - if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background) { - callback && callback(); - } }, /** diff --git a/src/parser.js b/src/parser.js index 951a100c..e00388ba 100644 --- a/src/parser.js +++ b/src/parser.js @@ -805,23 +805,31 @@ * @return {String} */ function createSVGRefElementsMarkup(canvas) { - var markup = ''; + var markup = [ ]; - if (canvas.backgroundColor && canvas.backgroundColor.source) { - markup = [ - '', '' - ].join(''); + ); } - - return markup; } /** diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 85b13cd9..9d18d9a9 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -201,6 +201,7 @@ '" transform="translate(' + (-this.width/2) + ' ' + (-this.height/2) + ')', '" width="', this.width, '" height="', this.height, + '" preserveAspectRatio="none"', '">' ); diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 6a5cbacf..62c67cd9 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -44,56 +44,41 @@ }, /** - * Background color of canvas instance - * @type String + * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. + * @type {(String|fabric.Pattern)} * @default */ backgroundColor: '', /** - * Background image of canvas instance - * Should be set via {@link fabric.StaticCanvas#setBackgroundImage} - * @type String + * Background image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundImage}. + * Backwards incompatibility note: The "backgroundImageOpacity" and "backgroundImageStretch" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#opacity}, {@link fabric.Image#width} and {@link fabric.Image#height}. + * @type fabric.Image * @default */ - backgroundImage: '', + backgroundImage: null, /** - * Opacity of the background image of the canvas instance - * @type Float + * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} + * @since 1.3.9 + * @type {(String|fabric.Pattern)} * @default */ - backgroundImageOpacity: 1, + overlayColor: '', /** - * Indicates whether the background image should be stretched to fit the - * dimensions of the canvas instance. - * @type Boolean + * Overlay image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayImage}. + * Backwards incompatibility note: The "overlayImageLeft" and "overlayImageTop" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#left} and {@link fabric.Image#top}. + * @type fabric.Image * @default */ - backgroundImageStretch: true, - - /** - * Overlay image of canvas instance - * Should be set via {@link fabric.StaticCanvas#setOverlayImage} - * @type String - * @default - */ - overlayImage: '', - - /** - * Left offset of overlay image (if present) - * @type Number - * @default - */ - overlayImageLeft: 0, - - /** - * Top offset of overlay image (if present) - * @type Number - * @default - */ - overlayImageTop: 0, + overlayImage: null, /** * Indicates whether toObject/toDatalessObject should include default values @@ -168,6 +153,9 @@ if (options.backgroundColor) { this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this)); } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, this.renderAll.bind(this)); + } this.calcOffset(); }, @@ -184,74 +172,115 @@ /** * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas - * @param {String} url url of an image to set overlay to + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to * @param {Function} callback callback to invoke when image is loaded and set as an overlay - * @param {Object} [options] Optional options to set for the overlay image - * @param {Number} [options.overlayImageLeft] {@link fabric.StaticCanvas#overlayImageLeft|Left offset} of overlay image - * @param {Number} [options.overlayImageTop] {@link fabric.StaticCanvas#overlayImageTop|Top offset} of overlay image + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} - * @example Normal overlayImage - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas)); - * @example Displaced overlayImage (left and top != 0) + * @example Normal overlayImage with left/top = 0 * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * overlayImageLeft: 100, - * overlayImageTop: 100 + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' * }); */ - setOverlayImage: function (url, callback, options) { // TODO (kangax): test callback - fabric.util.loadImage(url, function(img) { - this.overlayImage = img; - if (options && ('overlayImageLeft' in options)) { - this.overlayImageLeft = options.overlayImageLeft; - } - if (options && ('overlayImageTop' in options)) { - this.overlayImageTop = options.overlayImageTop; - } - callback && callback(); - }, this); - - return this; + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); }, /** * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas - * @param {String} url URL of an image to set background to - * @param {Function} callback callback to invoke when image is loaded and set as background - * @param {Object} [options] Optional options to set for the background image - * @param {Float} [options.backgroundImageOpacity] {@link fabric.StaticCanvas#backgroundImageOpacity|Opacity} of the background image of the canvas instance - * @param {Boolean} [options.backgroundImageStretch] Indicates whether the background image should be {@link fabric.StaticCanvas#backgroundImageStretch|strechted} to fit the canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/YH9yD/|jsFiddle demo} - * @example Normal backgroundImage - * canvas.setBackgroundImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas)); - * @example Stretched backgroundImage with opacity - * canvas.setBackgroundImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * backgroundImageOpacity: 0.5, - * backgroundImageStretch: true + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' * }); */ - setBackgroundImage: function (url, callback, options) { - fabric.util.loadImage(url, function(img) { - this.backgroundImage = img; - if (options && ('backgroundImageOpacity' in options)) { - this.backgroundImageOpacity = options.backgroundImageOpacity; - } - if (options && ('backgroundImageStretch' in options)) { - this.backgroundImageStretch = options.backgroundImageStretch; - } - callback && callback(); - }, this); + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, - return this; + /** + * Sets {@link fabric.StaticCanvas#overlayColor|background color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); }, /** * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas - * @param {String|fabric.Pattern} backgroundColor Color or pattern to set background color to - * @param {Function} callback callback to invoke when background color is set + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} @@ -261,20 +290,61 @@ * canvas.setBackgroundColor({ * source: 'http://fabricjs.com/assets/escheresque_ste.png' * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); */ setBackgroundColor: function(backgroundColor, callback) { - if (backgroundColor.source) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img) { + this[property] = new fabric.Image(img, options); + callback && callback(); + }, this); + } + else { + this[property] = image; + callback && callback(); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String)} color Object with pattern information or color value + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + if (color.source) { var _this = this; - fabric.util.loadImage(backgroundColor.source, function(img) { - _this.backgroundColor = new fabric.Pattern({ + fabric.util.loadImage(color.source, function(img) { + _this[property] = new fabric.Pattern({ source: img, - repeat: backgroundColor.repeat + repeat: color.repeat, + offsetX: color.offsetX, + offsetY: color.offsetY }); callback && callback(); }); } else { - this.backgroundColor = backgroundColor; + this[property] = color; callback && callback(); } @@ -576,9 +646,7 @@ canvasToDrawOn.restore(); } - if (this.overlayImage) { - canvasToDrawOn.drawImage(this.overlayImage, this.overlayImageLeft, this.overlayImageTop); - } + this._renderOverlay(canvasToDrawOn); if (this.controlsAboveOverlay && this.interactive) { this.drawControls(canvasToDrawOn); @@ -589,16 +657,26 @@ return this; }, - _renderObjects: function(canvasToDrawOn, activeGroup) { + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup + */ + _renderObjects: function(ctx, activeGroup) { for (var i = 0, length = this._objects.length; i < length; ++i) { if (!activeGroup || (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { - this._draw(canvasToDrawOn, this._objects[i]); + this._draw(ctx, this._objects[i]); } } }, - _renderActiveGroup: function(canvasToDrawOn, activeGroup) { + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Group} activeGroup + */ + _renderActiveGroup: function(ctx, activeGroup) { // delegate rendering to group selection (if one exists) if (activeGroup) { @@ -611,42 +689,50 @@ } }); activeGroup._set('objects', sortedObjects); - this._draw(canvasToDrawOn, activeGroup); - } - }, - - _renderBackground: function(canvasToDrawOn) { - if (this.backgroundColor) { - canvasToDrawOn.fillStyle = this.backgroundColor.toLive - ? this.backgroundColor.toLive(canvasToDrawOn) - : this.backgroundColor; - - canvasToDrawOn.fillRect( - this.backgroundColor.offsetX || 0, - this.backgroundColor.offsetY || 0, - this.width, - this.height); - } - if (typeof this.backgroundImage === 'object') { - this._renderBackroundImage(canvasToDrawOn); + this._draw(ctx, activeGroup); } }, /** * @private - * @param {CanvasRenderingContext2D} canvasToDrawOn Context to render on + * @param {CanvasRenderingContext2D} ctx Context to render on */ - _renderBackroundImage: function(canvasToDrawOn) { - canvasToDrawOn.save(); - canvasToDrawOn.globalAlpha = this.backgroundImageOpacity; + _renderBackground: function(ctx) { + if (this.backgroundColor) { + ctx.fillStyle = this.backgroundColor.toLive + ? this.backgroundColor.toLive(ctx) + : this.backgroundColor; - if (this.backgroundImageStretch) { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height); + ctx.fillRect( + this.backgroundColor.offsetX || 0, + this.backgroundColor.offsetY || 0, + this.width, + this.height); } - else { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0); + if (this.backgroundImage) { + this.backgroundImage.render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + if (this.overlayColor) { + ctx.fillStyle = this.overlayColor.toLive + ? this.overlayColor.toLive(ctx) + : this.overlayColor; + + ctx.fillRect( + this.overlayColor.offsetX || 0, + this.overlayColor.offsetY || 0, + this.width, + this.height); + } + if (this.overlayImage) { + this.overlayImage.render(ctx); } - canvasToDrawOn.restore(); }, /** @@ -777,22 +863,13 @@ instance.includeDefaultValues = originalValue; } return object; - }, this), - background: (this.backgroundColor && this.backgroundColor.toObject) - ? this.backgroundColor.toObject() - : this.backgroundColor + }, this) }; - if (this.backgroundImage) { - data.backgroundImage = this.backgroundImage.src; - data.backgroundImageOpacity = this.backgroundImageOpacity; - data.backgroundImageStretch = this.backgroundImageStretch; - } - if (this.overlayImage) { - data.overlayImage = this.overlayImage.src; - data.overlayImageLeft = this.overlayImageLeft; - data.overlayImageTop = this.overlayImageTop; - } + + extend(data, this.__serializeBgOverlay()); + fabric.util.populateWithProperties(this, data, propertiesToInclude); + if (activeGroup) { this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); activeGroup.forEachObject(function(o) { o.set('active', true) }); @@ -800,6 +877,31 @@ return data; }, + /** + * @private + */ + __serializeBgOverlay: function() { + var data = { + background: (this.backgroundColor && this.backgroundColor.toObject) + ? this.backgroundColor.toObject() + : this.backgroundColor + }; + + if (this.overlayColor) { + data.overlay = this.overlayColor.toObject + ? this.overlayColor.toObject() + : this.overlayColor; + } + if (this.backgroundImage) { + data.backgroundImage = this.backgroundImage.toObject(); + } + if (this.overlayImage) { + data.overlayImage = this.overlayImage.toObject(); + } + + return data; + }, + /* _TO_SVG_START_ */ /** * Returns SVG representation of canvas @@ -843,14 +945,23 @@ this._setSVGPreamble(markup, options); this._setSVGHeader(markup, options); - this._setSVGBackgroundColor(markup); - this._setSVGBackgroundImage(markup); - this._setSVGOverlayImage(markup); + + this._setSVGBgOverlayColor(markup, 'backgroundColor'); + this._setSVGBgOverlayImage(markup, 'backgroundImage'); + this._setSVGObjects(markup, reviver); + this._setSVGBgOverlayColor(markup, 'overlayColor'); + this._setSVGBgOverlayImage(markup, 'overlayImage'); + + markup.push(''); + return markup.join(''); }, + /** + * @private + */ _setSVGPreamble: function(markup, options) { if (!options.suppressPreamble) { markup.push( @@ -861,6 +972,9 @@ } }, + /** + * @private + */ _setSVGHeader: function(markup, options) { markup.push( '' - ); - } - }, - - _setSVGBackgroundImage: function(markup) { - if (this.backgroundImage) { - markup.push( - '' - ); - } - }, - - _setSVGOverlayImage: function(markup) { - if (this.overlayImage) { - markup.push( - '' - ); - } - }, - + /** + * @private + */ _setSVGObjects: function(markup, reviver) { var activeGroup = this.getActiveGroup(); if (activeGroup) { @@ -941,7 +1019,39 @@ o.set('active', true); }); } - markup.push(''); + }, + + /** + * @private + */ + _setSVGBgOverlayImage: function(markup, property) { + if (this[property] && this[property].toSVG) { + markup.push(this[property].toSVG()); + } + }, + + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + if (this[property] && this[property].source) { + markup.push( + '' + ); + } + else if (this[property] && property === 'overlayColor') { + markup.push( + '' + ); + } }, /* _TO_SVG_END_ */ diff --git a/src/util/lang_string.js b/src/util/lang_string.js index 86df0664..e5586da5 100644 --- a/src/util/lang_string.js +++ b/src/util/lang_string.js @@ -30,10 +30,12 @@ function camelize(string) { * Capitalizes a string * @memberOf fabric.util.string * @param {String} string String to capitalize + * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized and other letters stay untouched, + * if false first letter is capitalized and other letters are converted to lowercase. * @return {String} Capitalized version of a string */ -function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); +function capitalize(string, firstLetterOnly) { + return string.charAt(0).toUpperCase() + (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); } /** diff --git a/test/unit/canvas.js b/test/unit/canvas.js index 95f4b272..76e688cb 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -19,7 +19,7 @@ ' -3.56, 6.891, -7.481, 8.848], ["c", -4.689, 2.336, -9.084, -0.802, -11.277, -2.868], ["l",'+ ' -1.948, 3.104], ["l", -1.628, -1.333], ["l", 3.138, -4.689], ["c", 0.025, 0, 9, 1.932, 9, 1.932], '+ '["c", 0.877, -9.979, 2.893, -12.905, 4.942, -15.621], ["C", 17.878, 21.775, 18.713, 17.397, 18.511, '+ - '13.99], ["z", null]]}], "background": "#ff5555"}'; + '13.99], ["z", null]]}], "background": "#ff5555","overlay": "rgba(0,0,0,0.2)"}'; var PATH_OBJ_JSON = '{"type": "path", "originX": "center", "originY": "center", "left": 268, "top": 266, "width": 51, "height": 49,'+ ' "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 1, "scaleX": 1, "scaleY": 1, '+ @@ -46,7 +46,7 @@ var RECT_JSON = '{"objects":[{"type":"rect","originX":"center","originY":"center","left":0,"top":0,"width":10,"height":10,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ '"shadow":null,'+ - '"visible":true,"clipTo":null,"backgroundColor":"","rx":0,"ry":0,"x":0,"y":0}],"background":"#ff5555"}'; + '"visible":true,"clipTo":null,"backgroundColor":"","rx":0,"ry":0,"x":0,"y":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}'; var el = fabric.document.createElement('canvas'); el.width = 600; el.height = 600; @@ -68,7 +68,7 @@ canvas.clear(); canvas.setActiveGroup(null); canvas.backgroundColor = fabric.Canvas.prototype.backgroundColor; - canvas.backgroundImage = ''; + canvas.overlayColor = fabric.Canvas.prototype.overlayColor; canvas.calcOffset(); upperCanvasEl.style.display = 'none'; } @@ -248,19 +248,12 @@ ok(typeof canvas.toJSON == 'function'); equal(JSON.stringify(canvas.toJSON()), EMPTY_JSON); canvas.backgroundColor = '#ff5555'; - equal(JSON.stringify(canvas.toJSON()), '{"objects":[],"background":"#ff5555"}', '`background` value should be reflected in json'); + canvas.overlayColor = 'rgba(0,0,0,0.2)'; + equal(JSON.stringify(canvas.toJSON()), '{"objects":[],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}', '`background` and `overlayColor` value should be reflected in json'); canvas.add(makeRect()); deepEqual(JSON.stringify(canvas.toJSON()), RECT_JSON); }); - test('toDatalessJSON', function() { - var path = new fabric.Path('M 100 100 L 300 100 L 200 300 z', { - sourcePath: 'http://example.com/' - }); - canvas.add(path); - equal(JSON.stringify(canvas.toDatalessJSON()), PATH_DATALESS_JSON); - }); - test('toJSON with active group', function() { var rect = new fabric.Rect({ width: 50, height: 50, left: 100, top: 100 }); var circle = new fabric.Circle({ radius: 50, left: 50, top: 50 }); @@ -273,6 +266,14 @@ equal(json, jsonWithActiveGroup); }); + test('toDatalessJSON', function() { + var path = new fabric.Path('M 100 100 L 300 100 L 200 300 z', { + sourcePath: 'http://example.com/' + }); + canvas.add(path); + equal(JSON.stringify(canvas.toDatalessJSON()), PATH_DATALESS_JSON); + }); + test('toObject', function() { ok(typeof canvas.toObject == 'function'); var expectedObject = { @@ -318,6 +319,7 @@ ok(!canvas.isEmpty(), 'canvas is not empty'); equal(obj.type, 'path', 'first object is a path object'); equal(canvas.backgroundColor, '#ff5555', 'backgroundColor is populated properly'); + equal(canvas.overlayColor, 'rgba(0,0,0,0.2)', 'overlayColor is populated properly'); equal(obj.get('left'), 268); equal(obj.get('top'), 266); @@ -345,6 +347,7 @@ ok(!canvas.isEmpty(), 'canvas is not empty'); equal(obj.type, 'path', 'first object is a path object'); equal(canvas.backgroundColor, '#ff5555', 'backgroundColor is populated properly'); + equal(canvas.overlayColor, 'rgba(0,0,0,0.2)', 'overlayColor is populated properly'); equal(obj.get('left'), 268); equal(obj.get('top'), 266); @@ -380,6 +383,7 @@ ok(!canvas.isEmpty(), 'canvas is not empty'); equal(obj.type, 'path', 'first object is a path object'); equal(canvas.backgroundColor, '#ff5555', 'backgroundColor is populated properly'); + equal(canvas.overlayColor, 'rgba(0,0,0,0.2)', 'overlayColor is populated properly'); equal(obj.get('left'), 268); equal(obj.get('top'), 266); @@ -400,20 +404,19 @@ }); asyncTest('loadFromJSON with no objects', function() { - var c1 = new fabric.Canvas('c1', { backgroundColor: 'green' }), - c2 = new fabric.Canvas('c2', { backgroundColor: 'red' }); + var c1 = new fabric.Canvas('c1', { backgroundColor: 'green', overlayColor: 'yellow' }), + c2 = new fabric.Canvas('c2', { backgroundColor: 'red', overlayColor: 'orange' }); var json = c1.toJSON(); var fired = false; c2.loadFromJSON(json, function() { fired = true; - }); - setTimeout(function() { 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(); - }, 500); + }); }); asyncTest('loadFromJSON with empty fabric.Group', function() { @@ -428,12 +431,10 @@ var fired = false; c2.loadFromJSON(json, function() { fired = true; - }); - setTimeout(function() { ok(fired, 'Callback should be fired even if empty fabric.Group exists'); start(); - }, 500); + }); }); asyncTest('loadFromJSON with async content', function() { diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 34c00fe0..c45e1d7a 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -23,7 +23,7 @@ ' -3.56, 6.891, -7.481, 8.848], ["c", -4.689, 2.336, -9.084, -0.802, -11.277, -2.868], ["l",'+ ' -1.948, 3.104], ["l", -1.628, -1.333], ["l", 3.138, -4.689], ["c", 0.025, 0, 9, 1.932, 9, 1.932], '+ '["c", 0.877, -9.979, 2.893, -12.905, 4.942, -15.621], ["C", 17.878, 21.775, 18.713, 17.397, 18.511, '+ - '13.99], ["z", null]]}], "background": "#ff5555"}'; + '13.99], ["z", null]]}], "background": "#ff5555", "overlay":"rgba(0,0,0,0.2)"}'; var PATH_DATALESS_JSON = '{"objects":[{"type":"path","originX":"center","originY":"center","left":200,"top":200,"width":200,"height":200,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ @@ -33,13 +33,97 @@ var RECT_JSON = '{"objects":[{"type":"rect","originX":"center","originY":"center","left":0,"top":0,"width":10,"height":10,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ - '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","rx":0,"ry":0,"x":0,"y":0}],"background":"#ff5555"}'; + '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","rx":0,"ry":0,"x":0,"y":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}'; var RECT_JSON_WITH_PADDING = '{"objects":[{"type":"rect","originX":"center","originY":"center","left":0,"top":0,"width":10,"height":20,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","padding":123,"foo":"bar","rx":0,"ry":0,"x":0,"y":0}],"background":""}'; + function getAbsolutePath(path) { + var isAbsolute = /^https?:/.test(path); + if (isAbsolute) return path; + var imgEl = _createImageElement(); + imgEl.src = path; + var src = imgEl.src; + imgEl = null; + return src; + } + + var IMG_SRC = fabric.isLikelyNode ? (__dirname + '/../fixtures/test_image.gif') : getAbsolutePath('../fixtures/test_image.gif'), + IMG_WIDTH = 276, + IMG_HEIGHT = 110; + + var REFERENCE_IMG_OBJECT = { + 'type': 'image', + 'originX': 'center', + 'originY': 'center', + 'left': 0, + 'top': 0, + 'width': IMG_WIDTH, // node-canvas doesn't seem to allow setting width/height on image objects + 'height': IMG_HEIGHT, // or does it now? + 'fill': 'rgb(0,0,0)', + 'stroke': null, + 'strokeWidth': 1, + 'strokeDashArray': null, + 'strokeLineCap': 'butt', + 'strokeLineJoin': 'miter', + 'strokeMiterLimit': 10, + 'scaleX': 1, + 'scaleY': 1, + 'angle': 0, + 'flipX': false, + 'flipY': false, + 'opacity': 1, + 'src': fabric.isLikelyNode ? undefined : IMG_SRC, + 'shadow': null, + 'visible': true, + 'backgroundColor': '', + 'clipTo': null, + 'filters': [] + }; + + function _createImageElement() { + return fabric.isLikelyNode ? new (require('canvas').Image) : fabric.document.createElement('img'); + } + + function _createImageObject(width, height, callback) { + var elImage = _createImageElement(); + elImage.width = width; + elImage.height = height; + setSrc(elImage, IMG_SRC, function() { + callback(new fabric.Image(elImage)); + }); + } + + function createImageObject(callback) { + return _createImageObject(IMG_WIDTH, IMG_HEIGHT, callback) + } + + function setSrc(img, src, callback) { + if (fabric.isLikelyNode) { + require('fs').readFile(src, function(err, imgData) { + if (err) throw err; + img.src = imgData; + callback && callback(); + }); + } + else { + img.src = src; + callback && callback(); + } + } + + function fixImageDimension(imgObj) { + // workaround for node-canvas sometimes producing images with width/height and sometimes not + if (imgObj.width === 0) { + imgObj.width = IMG_WIDTH; + } + if (imgObj.height === 0) { + imgObj.height = IMG_HEIGHT; + } + } + // force creation of static canvas // TODO: fix this var Canvas = fabric.Canvas; @@ -61,6 +145,7 @@ teardown: function() { canvas.clear(); canvas.backgroundColor = fabric.StaticCanvas.prototype.backgroundColor; + canvas.overlayColor = fabric.StaticCanvas.prototype.overlayColor; canvas.calcOffset(); } }); @@ -352,11 +437,62 @@ ok(typeof canvas.toJSON == 'function'); equal(JSON.stringify(canvas.toJSON()), '{"objects":[],"background":""}'); canvas.backgroundColor = '#ff5555'; - equal(JSON.stringify(canvas.toJSON()), '{"objects":[],"background":"#ff5555"}', '`background` value should be reflected in json'); + canvas.overlayColor = 'rgba(0,0,0,0.2)'; + equal(JSON.stringify(canvas.toJSON()), '{"objects":[],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}', '`background` and `overlay` value should be reflected in json'); canvas.add(makeRect()); deepEqual(JSON.stringify(canvas.toJSON()), RECT_JSON); }); + test('toJSON custom properties non-existence check', function() { + var rect = new fabric.Rect({ width: 10, height: 20 }); + rect.padding = 123; + canvas.add(rect); + rect.foo = 'bar'; + + canvas.bar = 456; + + var data = canvas.toJSON(['padding', 'foo', 'bar', 'baz']); + ok('padding' in data.objects[0]); + ok('foo' in data.objects[0], 'foo shouldn\'t be included if it\'s not in an object'); + ok(!('bar' in data.objects[0]), 'bar shouldn\'t be included if it\'s not in an object'); + ok(!('baz' in data.objects[0]), 'bar shouldn\'t be included if it\'s not in an object'); + ok(!('foo' in data)); + ok(!('baz' in data)); + ok('bar' in data); + }); + + asyncTest('toJSON backgroundImage', function() { + createImageObject(function(image) { + + canvas.backgroundImage = image; + + var json = canvas.toJSON(); + + fixImageDimension(json.backgroundImage); + deepEqual(json.backgroundImage, REFERENCE_IMG_OBJECT); + + canvas.backgroundImage = null; + + start(); + }); + }); + + asyncTest('toJSON overlayImage', function() { + createImageObject(function(image) { + + canvas.overlayImage = image; + + var json = canvas.toJSON(); + + fixImageDimension(json.overlayImage); + deepEqual(json.overlayImage, REFERENCE_IMG_OBJECT); + + canvas.overlayImage = null; + + start(); + }); + }); + test('toDatalessJSON', function() { var path = new fabric.Path('M 100 100 L 300 100 L 200 300 z', { sourcePath: 'http://example.com/' @@ -458,6 +594,7 @@ ok(!canvas.isEmpty(), 'canvas is not empty'); equal(obj.type, 'path', 'first object is a path object'); equal(canvas.backgroundColor, '#ff5555', 'backgroundColor is populated properly'); + equal(canvas.overlayColor, 'rgba(0,0,0,0.2)', 'overlayColor is populated properly'); equal(obj.get('left'), 268); equal(obj.get('top'), 266); @@ -499,22 +636,19 @@ }); }); - test('toJSON custom properties non-existence check', function() { - var rect = new fabric.Rect({ width: 10, height: 20 }); - rect.padding = 123; - canvas.add(rect); - rect.foo = 'bar'; + asyncTest('loadFromJSON with text', function() { + var json = '{"objects":[{"type":"text","left":150,"top":200,"width":128,"height":64.32,"fill":"#000000","stroke":"","strokeWidth":"","scaleX":0.8,"scaleY":0.8,"angle":0,"flipX":false,"flipY":false,"opacity":1,"text":"NAME HERE","fontSize":24,"fontWeight":"","fontFamily":"Delicious_500","fontStyle":"","lineHeight":"","textDecoration":"","textAlign":"center","path":"","strokeStyle":"","backgroundColor":""}],"background":"#ffffff"}'; + canvas.loadFromJSON(json, function() { - canvas.bar = 456; + canvas.renderAll(); - var data = canvas.toJSON(['padding', 'foo', 'bar', 'baz']); - ok('padding' in data.objects[0]); - ok('foo' in data.objects[0], 'foo shouldn\'t be included if it\'s not in an object'); - ok(!('bar' in data.objects[0]), 'bar shouldn\'t be included if it\'s not in an object'); - ok(!('baz' in data.objects[0]), 'bar shouldn\'t be included if it\'s not in an object'); - ok(!('foo' in data)); - ok(!('baz' in data)); - ok('bar' in data); + equal('text', canvas.item(0).type); + equal(150, canvas.item(0).left); + equal(200, canvas.item(0).top) + equal('NAME HERE', canvas.item(0).text); + + start(); + }); }); test('remove', function() { @@ -895,19 +1029,4 @@ deepEqual(objectsAdded[3], circle3); }); - asyncTest('loadFromJSON with text', function() { - var json = '{"objects":[{"type":"text","left":150,"top":200,"width":128,"height":64.32,"fill":"#000000","stroke":"","strokeWidth":"","scaleX":0.8,"scaleY":0.8,"angle":0,"flipX":false,"flipY":false,"opacity":1,"text":"NAME HERE","fontSize":24,"fontWeight":"","fontFamily":"Delicious_500","fontStyle":"","lineHeight":"","textDecoration":"","textAlign":"center","path":"","strokeStyle":"","backgroundColor":""}],"background":"#ffffff"}'; - canvas.loadFromJSON(json, function() { - - canvas.renderAll(); - - equal('text', canvas.item(0).type); - equal(150, canvas.item(0).left); - equal(200, canvas.item(0).top) - equal('NAME HERE', canvas.item(0).text); - - start(); - }); - }); - })();