Merge pull request #21 from kangax/master

Sync to master
This commit is contained in:
Andrea Bogazzi 2016-03-27 23:43:19 +02:00
commit 5b1a89c946
11 changed files with 106 additions and 85 deletions

View file

@ -33,6 +33,11 @@
Using Fabric.js, you can create and populate objects on canvas; objects like simple geometrical shapes — rectangles, circles, ellipses, polygons, or more complex shapes consisting of hundreds or thousands of simple paths. You can then scale, move, and rotate these objects with the mouse; modify their properties — color, transparency, z-index, etc. You can also manipulate these objects altogether — grouping them with a simple mouse selection.
### Non-Technical Introduction to Fabric
Fabric.js allows you to easily create simple shapes like rectangles, circles, triangles and other polygons or more complex shapes made up of many paths, onto the HTML `<canvas>` element on a webpage using JavaScript. Fabric.js will then allow you to manipulate the size, position and rotation of these objects with a mouse. Its also possible to change some of the attributes of these objects such as their color, transparency, depth position on the webpage or selecting groups of these objects using the Fabric.js library. Fabric.js will also allow you to convert an SVG image into JavaScript data that can be used for putting it onto the `<canvas>` element.
[Contributions](https://github.com/kangax/fabric.js/wiki/Love-Fabric%3F-Help-us-by...) are very much welcome!
### Goals

View file

@ -44,6 +44,10 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
height: options.height
};
if (this._isRetinaScaling()) {
multiplier *= fabric.devicePixelRatio;
}
if (multiplier !== 1) {
return this.__toDataURLWithMultiplier(format, quality, cropping, multiplier);
}
@ -115,13 +119,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
scaledHeight = origHeight * multiplier,
activeObject = this.getActiveObject(),
activeGroup = this.getActiveGroup(),
ctx = this.contextContainer;
if (multiplier > 1) {
this.setWidth(scaledWidth).setHeight(scaledHeight);
this.setDimensions({ width: scaledWidth, height: scaledHeight });
}
ctx.scale(multiplier, multiplier);
ctx.save();
ctx.scale(multiplier / fabric.devicePixelRatio, multiplier / fabric.devicePixelRatio);
if (cropping.left) {
cropping.left *= multiplier;
@ -156,9 +160,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
// background properly (while context is scaled)
this.width = origWidth;
this.height = origHeight;
ctx.scale(1 / multiplier, 1 / multiplier);
this.setWidth(origWidth).setHeight(origHeight);
this.setDimensions({ width: origWidth, height: origHeight });
if (activeGroup) {
this._restoreBordersControlsOnGroup(activeGroup);

View file

@ -3,9 +3,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
/**
* Returns styles-string for svg-export
* @param {Boolean} skipShadow a boolean to skip shadow filter output
* @return {String}
*/
getSvgStyles: function() {
getSvgStyles: function(skipShadow) {
var fill = this.fill
? (this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill)
@ -23,7 +24,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1',
visibility = this.visible ? '' : ' visibility: hidden;',
filter = this.getSvgFilter();
filter = skipShadow ? '' : this.getSvgFilter();
return [
'stroke: ', stroke, '; ',

View file

@ -123,7 +123,12 @@
fBoxX = toFixed((Math.abs(offset.x) + this.blur) / object.width, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX;
fBoxY = toFixed((Math.abs(offset.y) + this.blur) / object.height, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX;
}
if (object.flipX) {
offset.x *= -1;
}
if (object.flipY) {
offset.y *= -1;
}
return (
'<filter id="SVGID_' + this.id + '" y="-' + fBoxY + '%" height="' + (100 + 2 * fBoxY) + '%" ' +
'x="-' + fBoxX + '%" width="' + (100 + 2 * fBoxX) + '%" ' + '>\n' +

View file

@ -657,6 +657,9 @@
charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl || {});
textDecoration = textDecoration || this.textDecoration;
if (decl && decl.textBackgroundColor) {
this._removeShadow(ctx);
}
shouldFill && ctx.fillText(_char, left, top);
shouldStroke && ctx.strokeText(_char, left, top);
@ -743,60 +746,43 @@
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextLinesBackground: function(ctx) {
if (!this.textBackgroundColor && !this.styles) {
return;
}
this.callSuper('_renderTextLinesBackground', ctx);
ctx.save();
if (this.textBackgroundColor) {
ctx.fillStyle = this.textBackgroundColor;
}
var lineHeights = 0;
var lineTopOffset = 0, heightOfLine,
lineWidth, lineLeftOffset,
leftOffset = this._getLeftOffset(),
topOffset = this._getTopOffset(),
line, _char, style;
for (var i = 0, len = this._textLines.length; i < len; i++) {
heightOfLine = this._getHeightOfLine(ctx, i);
line = this._textLines[i];
var heightOfLine = this._getHeightOfLine(ctx, i);
if (this._textLines[i] === '') {
lineHeights += heightOfLine;
if (line === '' || !this.styles || !this._getLineStyle(i)) {
lineTopOffset += heightOfLine;
continue;
}
var lineWidth = this._getLineWidth(ctx, i),
lineLeftOffset = this._getLineLeftOffset(lineWidth);
lineWidth = this._getLineWidth(ctx, i);
lineLeftOffset = this._getLineLeftOffset(lineWidth);
if (this.textBackgroundColor) {
ctx.fillStyle = this.textBackgroundColor;
for (var j = 0, jlen = line.length; j < jlen; j++) {
style = this._getStyleDeclaration(i, j);
if (!style || !style.textBackgroundColor) {
continue;
}
_char = line[j];
ctx.fillStyle = style.textBackgroundColor;
ctx.fillRect(
this._getLeftOffset() + lineLeftOffset,
this._getTopOffset() + lineHeights,
lineWidth,
leftOffset + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j),
topOffset + lineTopOffset,
this._getWidthOfChar(ctx, _char, i, j) + 1,
heightOfLine / this.lineHeight
);
}
if (this._getLineStyle(i)) {
for (var j = 0, jlen = this._textLines[i].length; j < jlen; j++) {
var style = this._getStyleDeclaration(i, j);
if (style && style.textBackgroundColor) {
var _char = this._textLines[i][j];
ctx.fillStyle = style.textBackgroundColor;
ctx.fillRect(
this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j),
this._getTopOffset() + lineHeights,
this._getWidthOfChar(ctx, _char, i, j) + 1,
heightOfLine / this.lineHeight
);
}
}
}
lineHeights += heightOfLine;
}
ctx.restore();
},
/**
@ -1070,28 +1056,6 @@
return height;
},
/**
* This method is overwritten to account for different top offset
* @private
*/
_renderTextBoxBackground: function(ctx) {
if (!this.backgroundColor) {
return;
}
ctx.save();
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(
this._getLeftOffset(),
this._getTopOffset(),
this.width,
this.height
);
ctx.restore();
},
/**
* Returns object representation of an instance
* @method toObject

View file

@ -1075,7 +1075,8 @@
* @param {Boolean} [noTransform] When true, context is not transformed
*/
_renderControls: function(ctx, noTransform) {
if (!this.active || noTransform) {
if (!this.active || noTransform
|| (this.group && this.group !== this.canvas.getActiveGroup())) {
return;
}
@ -1109,7 +1110,10 @@
var multX = (this.canvas && this.canvas.viewportTransform[0]) || 1,
multY = (this.canvas && this.canvas.viewportTransform[3]) || 1;
if (this.canvas && this.canvas._isRetinaScaling()) {
multX *= fabric.devicePixelRatio;
multY *= fabric.devicePixelRatio;
}
ctx.shadowColor = this.shadow.color;
ctx.shadowBlur = this.shadow.blur * (multX + multY) * (this.scaleX + this.scaleY) / 4;
ctx.shadowOffsetX = this.shadow.offsetX * multX * this.scaleX;

View file

@ -90,7 +90,7 @@
// optimize 1x1 case (used in spray brush)
if (this.width === 1 && this.height === 1) {
ctx.fillRect(0, 0, 1, 1);
ctx.fillRect(-0.5, -0.5, 1, 1);
return;
}

View file

@ -624,7 +624,9 @@
this.width,
this.height
);
// if there is background color no other shadows
// should be casted
this._removeShadow(ctx);
},
/**
@ -635,11 +637,12 @@
if (!this.textBackgroundColor) {
return;
}
var lineTopOffset = 0, heightOfLine = this._getHeightOfLine(),
var lineTopOffset = 0, heightOfLine,
lineWidth, lineLeftOffset;
ctx.fillStyle = this.textBackgroundColor;
for (var i = 0, len = this._textLines.length; i < len; i++) {
heightOfLine = this._getHeightOfLine(ctx, i);
if (this._textLines[i] !== '') {
lineWidth = this.textAlign === 'justify' ? this.width : this._getLineWidth(ctx, i);
lineLeftOffset = this._getLineLeftOffset(lineWidth);
@ -647,11 +650,14 @@
this._getLeftOffset() + lineLeftOffset,
this._getTopOffset() + lineTopOffset,
lineWidth,
this.fontSize * this._fontSizeMult
heightOfLine / this.lineHeight
);
}
lineTopOffset += heightOfLine;
}
// if there is text background color no
// other shadows should be casted
this._removeShadow(ctx);
},
/**
@ -881,8 +887,12 @@
* @private
*/
_wrapSVGTextAndBg: function(markup, textAndBg) {
var noShadow = true, filter = this.getSvgFilter(),
style = filter === '' ? '' : ' style="' + filter + '"';
markup.push(
'\t<g transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '">\n',
'\t<g transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '"',
style, '>\n',
textAndBg.textBgRects.join(''),
'\t\t<text ',
(this.fontFamily ? 'font-family="' + this.fontFamily.replace(/"/g, '\'') + '" ': ''),
@ -890,7 +900,7 @@
(this.fontStyle ? 'font-style="' + this.fontStyle + '" ': ''),
(this.fontWeight ? 'font-weight="' + this.fontWeight + '" ': ''),
(this.textDecoration ? 'text-decoration="' + this.textDecoration + '" ': ''),
'style="', this.getSvgStyles(), '" >\n',
'style="', this.getSvgStyles(noShadow), '" >\n',
textAndBg.textSpans.join(''),
'\t\t</text>\n',
'\t</g>\n'

View file

@ -262,29 +262,32 @@
infix = ' ',
wordWidth = 0,
infixWidth = 0,
largestWordWidth = 0;
largestWordWidth = 0,
lineJustStarted = true;
for (var i = 0; i < words.length; i++) {
word = words[i];
wordWidth = this._measureText(ctx, word, lineIndex, offset);
offset += word.length;
lineWidth += infixWidth + wordWidth;
if (lineWidth >= this.width && line !== '') {
if (lineWidth >= this.width && !lineJustStarted) {
lines.push(line);
line = '';
lineWidth = wordWidth;
lineJustStarted = true;
}
if (line !== '' || i === 1) {
if (!lineJustStarted) {
line += infix;
}
line += word;
infixWidth = this._measureText(ctx, infix, lineIndex, offset);
offset++;
lineJustStarted = false;
// keep track of largest word
if (wordWidth > largestWordWidth) {
largestWordWidth = wordWidth;
@ -299,7 +302,6 @@
return lines;
},
/**
* Gets lines of text to render in the Textbox. This function calculates
* text wrapping on the fly everytime it is called.

View file

@ -191,11 +191,18 @@
this.calcOffset();
},
/**
* @private
*/
_isRetinaScaling: function() {
return (fabric.devicePixelRatio !== 1 && this.enableRetinaScaling);
},
/**
* @private
*/
_initRetinaScaling: function() {
if (fabric.devicePixelRatio === 1 || !this.enableRetinaScaling) {
if (!this._isRetinaScaling()) {
return;
}

View file

@ -170,6 +170,17 @@
shadow.color = '#000000';
equal(shadow.toSVG(object), '<filter id="SVGID_0" y="-40%" height="180%" x="-40%" width="180%" >\n\t<feGaussianBlur in="SourceAlpha" stdDeviation="1"></feGaussianBlur>\n\t<feOffset dx="10" dy="-10" result="oBlur" ></feOffset>\n\t<feFlood flood-color="#000000"/>\n\t<feComposite in2="oBlur" operator="in" />\n\t<feMerge>\n\t\t<feMergeNode></feMergeNode>\n\t\t<feMergeNode in="SourceGraphic"></feMergeNode>\n\t</feMerge>\n</filter>\n');
});
test('toSVG with flipped object', function() {
// reset uid
fabric.Object.__uid = 0;
var shadow = new fabric.Shadow({color: '#FF0000', offsetX: 10, offsetY: -10, blur: 2});
var object = new fabric.Object({fill: '#FF0000', flipX: true, flipY: true});
equal(shadow.toSVG(object), '<filter id="SVGID_0" y="-40%" height="180%" x="-40%" width="180%" >\n\t<feGaussianBlur in="SourceAlpha" stdDeviation="1"></feGaussianBlur>\n\t<feOffset dx="-10" dy="10" result="oBlur" ></feOffset>\n\t<feFlood flood-color="#FF0000"/>\n\t<feComposite in2="oBlur" operator="in" />\n\t<feMerge>\n\t\t<feMergeNode></feMergeNode>\n\t\t<feMergeNode in="SourceGraphic"></feMergeNode>\n\t</feMerge>\n</filter>\n');
});
test('toSVG with rotated object', function() {
// reset uid
@ -180,5 +191,15 @@
equal(shadow.toSVG(object), '<filter id="SVGID_0" y="-40%" height="180%" x="-40%" width="180%" >\n\t<feGaussianBlur in="SourceAlpha" stdDeviation="1"></feGaussianBlur>\n\t<feOffset dx="14.14" dy="0" result="oBlur" ></feOffset>\n\t<feFlood flood-color="#FF0000"/>\n\t<feComposite in2="oBlur" operator="in" />\n\t<feMerge>\n\t\t<feMergeNode></feMergeNode>\n\t\t<feMergeNode in="SourceGraphic"></feMergeNode>\n\t</feMerge>\n</filter>\n');
});
test('toSVG with rotated flipped object', function() {
// reset uid
fabric.Object.__uid = 0;
var shadow = new fabric.Shadow({color: '#FF0000', offsetX: 10, offsetY: 10, blur: 2});
var object = new fabric.Object({fill: '#FF0000', angle: 45, flipX: true});
equal(shadow.toSVG(object), '<filter id="SVGID_0" y="-40%" height="180%" x="-40%" width="180%" >\n\t<feGaussianBlur in="SourceAlpha" stdDeviation="1"></feGaussianBlur>\n\t<feOffset dx="-14.14" dy="0" result="oBlur" ></feOffset>\n\t<feFlood flood-color="#FF0000"/>\n\t<feComposite in2="oBlur" operator="in" />\n\t<feMerge>\n\t\t<feMergeNode></feMergeNode>\n\t\t<feMergeNode in="SourceGraphic"></feMergeNode>\n\t</feMerge>\n</filter>\n');
});
})();