From 4c4f845bfe87c05857246cd95ef5176142782339 Mon Sep 17 00:00:00 2001 From: Kienz Date: Sat, 23 Feb 2013 17:02:52 +0100 Subject: [PATCH] [BACK_INCOMPAT] Implement fabric.Gradient#toSVG() and radialGradient - Implement radial gradient and expand linear gradient (stop-opacity should now take into account) - Gradients should now be included in the SVG output for the following fabric objects: circle, ellipse, line, path, polygon, polyline, rect and triangle (text is not yet implemented) - Gradients (linear / radial) can be applied to stroke or fill property => change setGradientFill(options) to setGradient(type, options) - Change toObject() that linear and radial gradients can be serialized - Expand fabric.Color by 16 basic colors fabric.Color.colorNameMap => gradients with e.g. stop-color="blue" and stop-opacity="0.5 can be converted to RGBA color - RGBA colors in svg has no affect (convert to RGB color), only stop-opacity has affect to color opacity - Attached some test svg files http://kienzle.geschaeft.s3.amazonaws.com/projects/fabricjs/gradients/gradients.rar --- src/circle.class.js | 25 +++- src/color.class.js | 33 +++++- src/ellipse.class.js | 25 ++-- src/gradient.class.js | 262 +++++++++++++++++++++++++++++++++++------- src/line.class.js | 24 ++-- src/object.class.js | 20 +++- src/path.class.js | 25 +++- src/polygon.class.js | 25 ++-- src/polyline.class.js | 25 ++-- src/rect.class.js | 27 +++-- src/triangle.class.js | 25 ++-- 11 files changed, 410 insertions(+), 106 deletions(-) diff --git a/src/circle.class.js b/src/circle.class.js index 6de5fcc2..23cd8c8c 100644 --- a/src/circle.class.js +++ b/src/circle.class.js @@ -59,12 +59,25 @@ * @return {String} svg representation of an instance */ toSVG: function() { - return (''); + var markup = []; + + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, false)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, false)); + } + + markup.push( + '' + ); + + return markup.join(''); }, /** diff --git a/src/color.class.js b/src/color.class.js index a1ed5b4a..4ec4bb7f 100644 --- a/src/color.class.js +++ b/src/color.class.js @@ -37,7 +37,14 @@ * @method _tryParsingColor */ _tryParsingColor: function(color) { - var source = Color.sourceFromHex(color); + var source; + + if (color in Color.colorNameMap) { + color = Color.colorNameMap[color]; + } + + source = Color.sourceFromHex(color); + if (!source) { source = Color.sourceFromRgb(color); } @@ -197,6 +204,30 @@ */ fabric.Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i; + /** + * Map of the 16 basic color names with HEX code + * @static + * @field + */ + fabric.Color.colorNameMap = { + 'aqua': '#00FFFF', + 'black': '#000000', + 'blue': '#0000FF', + 'fuchsia': '#FF00FF', + 'gray': '#808080', + 'green': '#008000', + 'lime': '#00FF00', + 'maroon': '#800000', + 'navy': '#000080', + 'olive': '#808000', + 'purple': '#800080', + 'red': '#FF0000', + 'silver': '#C0C0C0', + 'teal': '#008080', + 'white': '#FFFFFF', + 'yellow': '#FFFF00', + }; + /** * Returns new color object, when given a color in RGB format * @method fromRgb diff --git a/src/ellipse.class.js b/src/ellipse.class.js index 8d1eda6f..ad1e3229 100644 --- a/src/ellipse.class.js +++ b/src/ellipse.class.js @@ -62,14 +62,25 @@ * @return {String} svg representation of an instance */ toSVG: function() { - return [ + var markup = []; + + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, false)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, false)); + } + + markup.push( '' - ].join(''); + 'rx="', this.get('rx'), + '" ry="', this.get('ry'), + '" style="', this.getSvgStyles(), + '" transform="', this.getSvgTransform(), + '"/>' + ); + + return markup.join(''); }, /** diff --git a/src/gradient.class.js b/src/gradient.class.js index 9a4bf77f..489c3743 100644 --- a/src/gradient.class.js +++ b/src/gradient.class.js @@ -1,7 +1,12 @@ (function() { - function getColorStopFromStyle(el) { - var style = el.getAttribute('style'); + function getColorStop(el) { + var style = el.getAttribute('style'), + offset = el.getAttribute('offset'), + color, opacity; + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); if (style) { var keyValuePairs = style.split(/\s*;\s*/); @@ -17,10 +22,29 @@ value = split[1].trim(); if (key === 'stop-color') { - return value; + color = value; + } + else if (key === 'stop-opacity') { + opacity = value; } } } + + if (!color) { + color = el.getAttribute('stop-color'); + } + if (!opacity) { + opacity = el.getAttribute('stop-opacity'); + } + + // convert rgba color to rgb color - alpha value has no affect in svg + color = new fabric.Color(color).toRgb(); + + return { + offset: offset, + color: color, + opacity: opacity + } } /** @@ -33,19 +57,43 @@ /** * Constructor * @method initialize - * @param [options] Options object with x1, y1, x2, y2 and colorStops + * @param [options] Options object with type, coords, gradientUnits and colorStops * @return {fabric.Gradient} thisArg */ initialize: function(options) { - options || (options = { }); - this.x1 = options.x1 || 0; - this.y1 = options.y1 || 0; - this.x2 = options.x2 || 0; - this.y2 = options.y2 || 0; + var coords = { }; - this.colorStops = options.colorStops; + this.id = fabric.Object.__uid++; + this.type = options.type || 'linear'; + + coords = { + x1: options.coords.x1 || 0, + y1: options.coords.y1 || 0, + x2: options.coords.x2 || 0, + y2: options.coords.y2 || 0 + } + + if (this.type === 'radial') { + coords.r1 = options.coords.r1 || 0; + coords.r2 = options.coords.r2 || 0; + } + + this.coords = coords; + this.gradientUnits = options.gradientUnits || 'objectBoundingBox'; + this.colorStops = options.colorStops.slice(); + }, + + /** + * Adds another colorStop + * @method add + * @param {Object} colorStop Object with offset, color and opacity + * @return {fabric.Gradient} thisArg + */ + addColorStop: function(colorStop) { + this.colorStops.push(colorStop); + return this; }, /** @@ -55,10 +103,9 @@ */ toObject: function() { return { - x1: this.x1, - x2: this.x2, - y1: this.y1, - y2: this.y2, + type: this.type, + coords: this.coords, + gradientUnits: this.gradientUnits, colorStops: this.colorStops }; }, @@ -70,15 +117,97 @@ * @return {CanvasGradient} */ toLive: function(ctx) { - var gradient = ctx.createLinearGradient( - this.x1, this.y1, this.x2 || ctx.canvas.width, this.y2); + var gradient; - for (var position in this.colorStops) { - var colorValue = this.colorStops[position]; - gradient.addColorStop(parseFloat(position), colorValue); + if (!this.type) return; + + if (this.type === 'linear') { + gradient = ctx.createLinearGradient( + this.coords.x1, this.coords.y1, this.coords.x2 || ctx.canvas.width, this.coords.y2); + } + else if (this.type === 'radial') { + gradient = ctx.createRadialGradient( + this.coords.x1, this.coords.y1, this.coords.r1, this.coords.x2, this.coords.y2, this.coords.r2); + } + + for (var i = 0; i < this.colorStops.length; i++) { + var color = this.colorStops[i].color, + opacity = this.colorStops[i].opacity, + offset = this.colorStops[i].offset; + + if (opacity) { + color = new fabric.Color(color).setAlpha(opacity).toRgba(); + } + gradient.addColorStop(parseFloat(offset), color); } return gradient; + }, + + /** + * Returns SVG representation of an gradient + * @method toSVG + * @param {Object} object Object to create a gradient for + * @param {Boolean} normalize Whether coords should be normalized + * @return {String} SVG representation of an gradient (linear/radial) + */ + toSVG: function(object, normalize) { + var coords = fabric.util.object.clone(this.coords); + + // colorStops must be sorted ascending + this.colorStops.sort(function(a, b) { + return a.offset - b.offset; + }); + + if (normalize && this.gradientUnits === 'userSpaceOnUse') { + coords.x1 += object.width / 2; + coords.y1 += object.height / 2; + coords.x2 += object.width / 2; + coords.y2 += object.height / 2; + } + else if (this.gradientUnits === 'objectBoundingBox') { + _convertValuesToPercentUnits(object, coords); + } + + if (this.type === 'linear') { + var markup = [ + '' + ]; + } + else if (this.type === 'radial') { + var markup = [ + '' + ]; + } + + for (var i = 0; i < this.colorStops.length; i++) { + markup.push( + '' + ); + } + + markup.push((this.type === 'linear' ? '' : '')); + + return markup.join(''); } }); @@ -90,52 +219,78 @@ * @static * @memberof fabric.Gradient * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement */ fromElement: function(el, instance) { /** * @example: * - * + * * * * * * OR * - * - * - * + * + * + * * * + * OR + * + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * */ var colorStopEls = el.getElementsByTagName('stop'), - offset, - colorStops = { }, - coords = { - x1: el.getAttribute('x1') || 0, - y1: el.getAttribute('y1') || 0, - x2: el.getAttribute('x2') || '100%', - y2: el.getAttribute('y2') || 0 - }; + type = (el.nodeName === 'linearGradient' ? 'linear' : 'radial'), + gradientUnits = el.getAttribute('gradientUnits') || 'objectBoundingBox', + colorStops = [], + coords = { }; + + if (type === 'linear') { + coords = { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + } + else if (type === 'radial') { + coords = { + x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', + y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', + r1: 0, + x2: el.getAttribute('cx') || '50%', + y2: el.getAttribute('cy') || '50%', + r2: el.getAttribute('r') || '50%', + }; + } for (var i = colorStopEls.length; i--; ) { - el = colorStopEls[i]; - offset = el.getAttribute('offset'); - - // convert percents to absolute values - offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); - colorStops[offset] = getColorStopFromStyle(el) || el.getAttribute('stop-color'); + colorStops.push(getColorStop(colorStopEls[i])); } _convertPercentUnitsToValues(instance, coords); return new fabric.Gradient({ - x1: coords.x1, - y1: coords.y1, - x2: coords.x2, - y2: coords.y2, + type: type, + coords: coords, + gradientUnits: gradientUnits, colorStops: colorStops }); }, @@ -144,8 +299,8 @@ * Returns {@link fabric.Gradient} instance from its object representation * @method forObject * @static - * @param obj - * @param [options] + * @param {Object} obj + * @param {Object} [options] * @memberof fabric.Gradient */ forObject: function(obj, options) { @@ -159,7 +314,7 @@ for (var prop in options) { if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { var percents = parseFloat(options[prop], 10); - if (prop === 'x1' || prop === 'x2') { + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { options[prop] = fabric.util.toFixed(object.width * percents / 100, 2); } else if (prop === 'y1' || prop === 'y2') { @@ -176,6 +331,25 @@ } } + function _convertValuesToPercentUnits(object, options) { + for (var prop in options) { + // normalize rendering point (should be from center rather than top/left corner of the shape) + if (prop === 'x1' || prop === 'x2') { + options[prop] += fabric.util.toFixed(object.width / 2, 2); + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] += fabric.util.toFixed(object.height / 2, 2); + } + // convert to percent units + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { + options[prop] = fabric.util.toFixed(options[prop] / object.width * 100, 2) + '%'; + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] = fabric.util.toFixed(options[prop] / object.height * 100, 2) + '%'; + } + } + } + /** * Parses an SVG document, returning all of the gradient declarations found in it * @static diff --git a/src/line.class.js b/src/line.class.js index c0062716..cd2aea3d 100644 --- a/src/line.class.js +++ b/src/line.class.js @@ -135,15 +135,23 @@ * @return {String} svg representation of an instance */ toSVG: function() { - return [ + var markup = []; + + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, true)); + } + + markup.push( '' - ].join(''); + 'x1="', this.get('x1'), + '" y1="', this.get('y1'), + '" x2="', this.get('x2'), + '" y2="', this.get('y2'), + '" style="', this.getSvgStyles(), + '"/>' + ); + + return markup.join(''); } }); diff --git a/src/object.class.js b/src/object.class.js index 3d0394d6..8b17c953 100644 --- a/src/object.class.js +++ b/src/object.class.js @@ -434,10 +434,10 @@ */ getSvgStyles: function() { return [ - "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", + "stroke: ", (this.stroke ? (this.stroke && this.stroke.toLive ? 'url(#SVGID_' + this.stroke.id + ')' : this.stroke) : 'none'), "; ", "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), - "fill: ", (this.fill ? this.fill : 'none'), "; ", + "fill: ", (this.fill ? (this.fill && this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) : 'none'), "; ", "opacity: ", (this.opacity ? this.opacity : '1'), ";" ].join(""); }, @@ -855,11 +855,13 @@ }, /** - * Sets gradient fill of an object - * @method setGradientFill + * Sets gradient (fill or stroke) of an object + * @method setGradient + * @param {String} property Property name 'stroke' or 'fill' + * @param {Object} [options] Options object */ - setGradientFill: function(options) { - this.set('fill', fabric.Gradient.forObject(this, options)); + setGradient: function(property, options) { + this.set(property, fabric.Gradient.forObject(this, options)); }, /** @@ -1065,4 +1067,10 @@ */ fabric.Object.NUM_FRACTION_DIGITS = 2; + /** + * @static + * @type Number + */ + fabric.Object.__uid = 0; + })(typeof exports !== 'undefined' ? exports : this); diff --git a/src/path.class.js b/src/path.class.js index 149f395d..67857b47 100644 --- a/src/path.class.js +++ b/src/path.class.js @@ -626,20 +626,33 @@ * @return {String} svg representation of an instance */ toSVG: function() { - var chunks = []; + var chunks = [], + markup = []; + for (var i = 0, len = this.path.length; i < len; i++) { chunks.push(this.path[i].join(' ')); } var path = chunks.join(' '); - return [ + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, true)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, true)); + } + + markup.push( '', '', + 'd="', path, + '" style="', this.getSvgStyles(), + '" transform="translate(', (-this.width / 2), ' ', (-this.height/2), ')', + '" stroke-linecap="round" ', + '/>', '' - ].join(''); + ); + + return markup.join(''); }, /** diff --git a/src/polygon.class.js b/src/polygon.class.js index a55ee784..1f136f13 100644 --- a/src/polygon.class.js +++ b/src/polygon.class.js @@ -90,18 +90,29 @@ * @return {String} svg representation of an instance */ toSVG: function() { - var points = []; + var points = [], + markup = []; + for (var i = 0, len = this.points.length; i < len; i++) { points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); } - return [ + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, false)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, false)); + } + + markup.push( '' - ].join(''); + 'points="', points.join(''), + '" style="', this.getSvgStyles(), + '" transform="', this.getSvgTransform(), + '"/>' + ); + + return markup.join(''); }, /** diff --git a/src/polyline.class.js b/src/polyline.class.js index 9f04c541..dadfc27d 100644 --- a/src/polyline.class.js +++ b/src/polyline.class.js @@ -63,18 +63,29 @@ * @return {String} svg representation of an instance */ toSVG: function() { - var points = []; + var points = [], + markup = []; + for (var i = 0, len = this.points.length; i < len; i++) { points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); } - return [ + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, false)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, false)); + } + + markup.push( '' - ].join(''); + 'points="', points.join(''), + '" style="', this.getSvgStyles(), + '" transform="', this.getSvgTransform(), + '"/>' + ); + + return markup.join(''); }, /** diff --git a/src/rect.class.js b/src/rect.class.js index 0a7d6bde..4219dd12 100644 --- a/src/rect.class.js +++ b/src/rect.class.js @@ -237,13 +237,26 @@ * @return {String} svg representation of an instance */ toSVG: function() { - return ''; + var markup = []; + + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, false)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, false)); + } + + markup.push( + '' + ); + + return markup.join(''); } }); diff --git a/src/triangle.class.js b/src/triangle.class.js index d9545346..6757adf4 100644 --- a/src/triangle.class.js +++ b/src/triangle.class.js @@ -76,8 +76,8 @@ * @return {String} svg representation of an instance */ toSVG: function() { - - var widthBy2 = this.width / 2, + var markup = [], + widthBy2 = this.width / 2, heightBy2 = this.height / 2; var points = [ @@ -86,11 +86,22 @@ widthBy2 + " " + heightBy2 ].join(","); - return ''; + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, true)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, true)); + } + + markup.push( + '' + ); + + return markup.join(''); } });