diff --git a/README.md b/README.md index d2f85ff2..8790fe4d 100644 --- a/README.md +++ b/README.md @@ -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 `` 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 `` element. + + [Contributions](https://github.com/kangax/fabric.js/wiki/Love-Fabric%3F-Help-us-by...) are very much welcome! ### Goals diff --git a/src/mixins/canvas_dataurl_exporter.mixin.js b/src/mixins/canvas_dataurl_exporter.mixin.js index f65775b8..566026a4 100644 --- a/src/mixins/canvas_dataurl_exporter.mixin.js +++ b/src/mixins/canvas_dataurl_exporter.mixin.js @@ -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); diff --git a/src/mixins/object.svg_export.js b/src/mixins/object.svg_export.js index 20e85462..4bcc008a 100644 --- a/src/mixins/object.svg_export.js +++ b/src/mixins/object.svg_export.js @@ -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, '; ', diff --git a/src/shadow.class.js b/src/shadow.class.js index db644b69..45ce75c9 100644 --- a/src/shadow.class.js +++ b/src/shadow.class.js @@ -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 ( '\n' + diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index b7c0ceb7..b5edd508 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -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 diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 51314cb0..7ecf74b5 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -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; diff --git a/src/shapes/rect.class.js b/src/shapes/rect.class.js index c753c99a..92b7a6f1 100644 --- a/src/shapes/rect.class.js +++ b/src/shapes/rect.class.js @@ -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; } diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index fb448ccd..fc1fe6e8 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -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\n', + '\t\n', textAndBg.textBgRects.join(''), '\t\t\n', + 'style="', this.getSvgStyles(noShadow), '" >\n', textAndBg.textSpans.join(''), '\t\t\n', '\t\n' diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js index f28a0695..8e2ada9f 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -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. diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 71743faf..4737a858 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -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; } diff --git a/test/unit/shadow.js b/test/unit/shadow.js index 4f7d4813..21421c4f 100644 --- a/test/unit/shadow.js +++ b/test/unit/shadow.js @@ -170,6 +170,17 @@ shadow.color = '#000000'; equal(shadow.toSVG(object), '\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\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), '\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'); + + }); test('toSVG with rotated object', function() { // reset uid @@ -180,5 +191,15 @@ equal(shadow.toSVG(object), '\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\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), '\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'); + }); })();