mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-05-21 11:51:51 +00:00
commit
5b1a89c946
11 changed files with 106 additions and 85 deletions
|
|
@ -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. It’s 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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, '; ',
|
||||
|
|
|
|||
|
|
@ -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' +
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in a new issue