[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.rarUpdate setGradient and addColorStop interface

Update setGradient and addColorStop interface
- setGradient('fill', {x1: 50, y2: 10, colorStops: {'0.7', 'rgba(0,153,153,0.5)'}});
- addColorStop({'0.4', 'blue'});
This commit is contained in:
Kienz 2013-03-09 21:00:54 +01:00
commit e35e1a0191
11 changed files with 443 additions and 106 deletions

View file

@ -59,12 +59,25 @@
* @return {String} svg representation of an instance
*/
toSVG: function() {
return ('<circle ' +
'cx="0" cy="0" ' +
'r="' + this.radius + '" ' +
'style="' + this.getSvgStyles() + '" ' +
'transform="' + this.getSvgTransform() + '" ' +
'/>');
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(
'<circle ',
'cx="0" cy="0" ',
'r="', this.radius,
'" style="', this.getSvgStyles(),
'" transform="', this.getSvgTransform(),
'"/>'
);
return markup.join('');
},
/**

View file

@ -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

View file

@ -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(
'<ellipse ',
'rx="', this.get('rx'), '" ',
'ry="', this.get('ry'), '" ',
'style="', this.getSvgStyles(), '" ',
'transform="', this.getSvgTransform(), '" ',
'/>'
].join('');
'rx="', this.get('rx'),
'" ry="', this.get('ry'),
'" style="', this.getSvgStyles(),
'" transform="', this.getSvgTransform(),
'"/>'
);
return markup.join('');
},
/**

View file

@ -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,46 @@
/**
* Constructor
* @method initialize
* @param [options] Options object with x1, y1, x2, y2 and colorStops
* @param {Object} [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 and color
* @return {fabric.Gradient} thisArg
*/
addColorStop: function(colorStop) {
for (var position in colorStop) {
var color = new fabric.Color(colorStop[position]);
this.colorStops.push({offset: position, color: color.toRgb(), opacity: color.getAlpha()});
}
return this;
},
/**
@ -55,10 +106,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 +120,98 @@
* @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),
markup;
// 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') {
markup = [
'<linearGradient ',
'id="SVGID_', this.id,
'" gradientUnits="', this.gradientUnits,
'" x1="', coords.x1,
'" y1="', coords.y1,
'" x2="', coords.x2,
'" y2="', coords.y2,
'">'
];
}
else if (this.type === 'radial') {
markup = [
'<radialGradient ',
'id="SVGID_', this.id,
'" gradientUnits="', this.gradientUnits,
'" cx="', coords.x2,
'" cy="', coords.y2,
'" r="', coords.r2,
'" fx="', coords.x1,
'" fy="', coords.y1,
'">'
];
}
for (var i = 0; i < this.colorStops.length; i++) {
markup.push(
'<stop ',
'offset="', (this.colorStops[i].offset * 100) + '%',
'" style="stop-color:', this.colorStops[i].color,
(this.colorStops[i].opacity ? ';stop-opacity: ' + this.colorStops[i].opacity : ';'),
'"/>'
);
}
markup.push((this.type === 'linear' ? '</linearGradient>' : '</radialGradient>'));
return markup.join('');
}
});
@ -90,52 +223,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:
*
* <linearGradient id="grad1">
* <linearGradient id="linearGrad1">
* <stop offset="0%" stop-color="white"/>
* <stop offset="100%" stop-color="black"/>
* </linearGradient>
*
* OR
*
* <linearGradient id="grad1">
* <stop offset="0%" style="stop-color:rgb(255,255,255)"/>
* <stop offset="100%" style="stop-color:rgb(0,0,0)"/>
* <linearGradient id="linearGrad2">
* <stop offset="0" style="stop-color:rgb(255,255,255)"/>
* <stop offset="1" style="stop-color:rgb(0,0,0)"/>
* </linearGradient>
*
* OR
*
* <radialGradient id="radialGrad1">
* <stop offset="0%" stop-color="white" stop-opacity="1" />
* <stop offset="50%" stop-color="black" stop-opacity="0.5" />
* <stop offset="100%" stop-color="white" stop-opacity="1" />
* </radialGradient>
*
* OR
*
* <radialGradient id="radialGrad2">
* <stop offset="0" stop-color="rgb(255,255,255)" />
* <stop offset="0.5" stop-color="rgb(0,0,0)" />
* <stop offset="1" stop-color="rgb(255,255,255)" />
* </radialGradient>
*
*/
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 +303,8 @@
* Returns {@link fabric.Gradient} instance from its object representation
* @method forObject
* @static
* @param obj
* @param [options]
* @param {Object} obj
* @param {Object} [options] Options object
* @memberof fabric.Gradient
*/
forObject: function(obj, options) {
@ -155,11 +314,15 @@
}
});
/**
* @private
* @method _convertPercentUnitsToValues
*/
function _convertPercentUnitsToValues(object, options) {
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 +339,29 @@
}
}
/**
* @private
* @method _convertValuesToPercentUnits
*/
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

View file

@ -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(
'<line ',
'x1="', this.get('x1'), '" ',
'y1="', this.get('y1'), '" ',
'x2="', this.get('x2'), '" ',
'y2="', this.get('y2'), '" ',
'style="', this.getSvgStyles(), '" ',
'/>'
].join('');
'x1="', this.get('x1'),
'" y1="', this.get('y1'),
'" x2="', this.get('x2'),
'" y2="', this.get('y2'),
'" style="', this.getSvgStyles(),
'"/>'
);
return markup.join('');
}
});

View file

@ -445,7 +445,7 @@
"stroke: ", (this.stroke ? 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'), ";",
(this.visible ? '' : " visibility: hidden;")
].join("");
@ -872,12 +872,35 @@
},
/**
* Sets gradient fill of an object
* @method setGradientFill
* @param {Object} options
* 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) {
options || (options = { });
var gradient = {colorStops: []};
gradient.type = options.type || (options.r1 || options.r2 ? 'radial' : 'linear');
gradient.coords = {
x1: options.x1,
y1: options.y1,
x2: options.x2,
y2: options.y2
};
if (options.r1 || options.r2) {
gradient.coords.r1 = options.r1;
gradient.coords.r2 = options.r2;
}
for (var position in options.colorStops) {
var color = new fabric.Color(options.colorStops[position]);
gradient.colorStops.push({offset: position, color: color.toRgb(), opacity: color.getAlpha()});
}
this.set(property, fabric.Gradient.forObject(this, gradient));
},
/**
@ -1085,4 +1108,10 @@
*/
fabric.Object.NUM_FRACTION_DIGITS = 2;
/**
* @static
* @type Number
*/
fabric.Object.__uid = 0;
})(typeof exports !== 'undefined' ? exports : this);

View file

@ -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(
'<g transform="', (this.group ? '' : this.getSvgTransform()), '">',
'<path ',
'd="', path, '" ',
'style="', this.getSvgStyles(), '" ',
'transform="translate(', (-this.width / 2), ' ', (-this.height/2), ')" />',
'd="', path,
'" style="', this.getSvgStyles(),
'" transform="translate(', (-this.width / 2), ' ', (-this.height/2), ')',
'" stroke-linecap="round" ',
'/>',
'</g>'
].join('');
);
return markup.join('');
},
/**

View file

@ -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(
'<polygon ',
'points="', points.join(''), '" ',
'style="', this.getSvgStyles(), '" ',
'transform="', this.getSvgTransform(), '" ',
'/>'
].join('');
'points="', points.join(''),
'" style="', this.getSvgStyles(),
'" transform="', this.getSvgTransform(),
'"/>'
);
return markup.join('');
},
/**

View file

@ -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(
'<polyline ',
'points="', points.join(''), '" ',
'style="', this.getSvgStyles(), '" ',
'transform="', this.getSvgTransform(), '" ',
'/>'
].join('');
'points="', points.join(''),
'" style="', this.getSvgStyles(),
'" transform="', this.getSvgTransform(),
'"/>'
);
return markup.join('');
},
/**

View file

@ -237,13 +237,26 @@
* @return {String} svg representation of an instance
*/
toSVG: function() {
return '<rect ' +
'x="' + (-1 * this.width / 2) + '" y="' + (-1 * this.height / 2) + '" ' +
'rx="' + this.get('rx') + '" ry="' + this.get('ry') + '" ' +
'width="' + this.width + '" height="' + this.height + '" ' +
'style="' + this.getSvgStyles() + '" ' +
'transform="' + this.getSvgTransform() + '" ' +
'/>';
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(
'<rect ',
'x="', (-1 * this.width / 2), '" y="', (-1 * this.height / 2),
'" rx="', this.get('rx'), '" ry="', this.get('ry'),
'" width="', this.width, '" height="', this.height,
'" style="', this.getSvgStyles(),
'" transform="', this.getSvgTransform(),
'"/>'
);
return markup.join('');
}
});

View file

@ -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 '<polygon ' +
'points="' + points + '" ' +
'style="' + this.getSvgStyles() + '" ' +
'transform="' + this.getSvgTransform() + '" ' +
'/>';
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(
'<polygon ',
'points="', points,
'" style="', this.getSvgStyles(),
'" transform="', this.getSvgTransform(),
'"/>'
);
return markup.join('');
}
});