diff --git a/src/brushes/pencil_brush.class.js b/src/brushes/pencil_brush.class.js index 83539c4c5c3..40bcf9c5d76 100644 --- a/src/brushes/pencil_brush.class.js +++ b/src/brushes/pencil_brush.class.js @@ -244,7 +244,7 @@ this.shadow.affectStroke = true; path.shadow = new fabric.Shadow(this.shadow); } - + path.pathOffset.scalarAddEquals(path.strokeWidth / 2); return path; }, diff --git a/src/shapes/path.class.js b/src/shapes/path.class.js index f4b5e29ad6c..f26f9278dbb 100644 --- a/src/shapes/path.class.js +++ b/src/shapes/path.class.js @@ -64,7 +64,7 @@ Array.isArray(path) ? path : fabric.util.parsePath(path) ); - fabric.Polyline.prototype._setPositionDimensions.call(this, options || {}); + this._setPositionDimensions(options); }, /** @@ -237,6 +237,37 @@ return this.path.length; }, + /** + * @private + */ + _projectStrokeOnSegmentBBox: function (point, pointBefore, pointAfter, bboxPoints) { + var v1 = pointBefore.subtract(point); + var v2 = pointAfter.subtract(point); + var angle = fabric.util.calcAngleBetweenVectors(v1, v2); + if (Math.abs(angle) < Math.PI / 2) { + var projectionVector = fabric.util.calcStrokeProjection( + pointBefore, + point, + pointAfter, + this + ); + var projectedPoints = [ + point.add(projectionVector), + point.subtract(projectionVector), + ]; + Array.isArray(bboxPoints) && bboxPoints.forEach(function (point) { + projectedPoints.push( + point.add(projectionVector), + point.subtract(projectionVector) + ); + }); + return projectedPoints; + } + else { + return []; + } + }, + /** * @private */ @@ -245,34 +276,48 @@ var aX = [], aY = [], current, // current instruction - subpathStartX = 0, - subpathStartY = 0, - x = 0, // current x - y = 0, // current y - bounds; + subpathStart = new fabric.Point(0, 0), + subpathSecond = new fabric.Point(0, 0), + point = new fabric.Point(0, 0), + prev = new fabric.Point(0, 0), + beforePrev = new fabric.Point(0, 0), + opening = false, + second = false, + closing = false, + projectedPoints = [], + prevBounds, + bounds = []; for (var i = 0, len = this.path.length; i < len; ++i) { current = this.path[i]; + if (opening) { + second = true; + } + opening = closing = false; + beforePrev.setFromPoint(prev); + prev.setFromPoint(point); + prevBounds = bounds; + bounds = []; switch (current[0]) { // first letter case 'L': // lineto, absolute - x = current[1]; - y = current[2]; + point.setXY(current[1], current[2]); bounds = []; break; case 'M': // moveTo, absolute - x = current[1]; - y = current[2]; - subpathStartX = x; - subpathStartY = y; + point.setXY(current[1], current[2]); + subpathStart.setFromPoint(point); + prev.setFromPoint(point); + beforePrev.setFromPoint(point); + opening = true; bounds = []; break; case 'C': // bezierCurveTo, absolute - bounds = fabric.util.getBoundsOfCurve(x, y, + bounds = fabric.util.getBoundsOfCurve(point.x, point.y, current[1], current[2], current[3], @@ -280,12 +325,11 @@ current[5], current[6] ); - x = current[5]; - y = current[6]; + point.setXY(current[5], current[6]); break; case 'Q': // quadraticCurveTo, absolute - bounds = fabric.util.getBoundsOfCurve(x, y, + bounds = fabric.util.getBoundsOfCurve(point.x, point.y, current[1], current[2], current[1], @@ -293,38 +337,102 @@ current[3], current[4] ); - x = current[3]; - y = current[4]; + point.setXY(current[3], current[4]); break; case 'z': case 'Z': - x = subpathStartX; - y = subpathStartY; + point.setFromPoint(subpathStart); + closing = true; break; } + + if (this.strokeLineJoin === 'miter') { + if (!opening && !second) { + projectedPoints.push.apply( + projectedPoints, + this._projectStrokeOnSegmentBBox(prev, beforePrev, point, prevBounds) + ); + } + if (closing) { + // project stroke on sub path start + projectedPoints.push.apply( + projectedPoints, + this._projectStrokeOnSegmentBBox(subpathStart, prev, subpathSecond, bounds) + ); + } + } + + aX.push(point.x); + aY.push(point.y); + bounds.forEach(function (point) { aX.push(point.x); aY.push(point.y); }); - aX.push(x); - aY.push(y); + + if (second) { + subpathSecond.setFromPoint(point); + second = false; + } + } - var minX = min(aX) || 0, - minY = min(aY) || 0, - maxX = max(aX) || 0, - maxY = max(aY) || 0, - deltaX = maxX - minX, - deltaY = maxY - minY; + projectedPoints.forEach(function (point) { + aX.push(point.x); + aY.push(point.y); + }); + + var minPoint = new fabric.Point(min(aX) || 0, min(aY) || 0), + maxPoint = new fabric.Point(max(aX) || 0, max(aY) || 0), + delta = maxPoint.subtract(minPoint); return { - left: minX, - top: minY, - width: deltaX, - height: deltaY + left: minPoint.x, + top: minPoint.y, + width: delta.x, + height: delta.y }; - } + }, + + _setPositionDimensions: function (options) { + options || (options = {}); + var calcDim = this._calcDimensions(options), origin; + this.width = calcDim.width; + this.height = calcDim.height; + if (!options.fromSVG) { + origin = this.translateToGivenOrigin( + // this looks bad, but is one way to keep it optional for now. + new fabric.Point( + calcDim.left, + calcDim.top + ), + 'left', + 'top', + this.originX, + this.originY + ); + } + if (typeof options.left === 'undefined') { + this.left = options.fromSVG ? calcDim.left : origin.x; + } + if (typeof options.top === 'undefined') { + this.top = options.fromSVG ? calcDim.top : origin.y; + } + this.pathOffset = new fabric.Point( + calcDim.left + this.width / 2, + calcDim.top + this.height / 2 + ); + }, + + /** + * @override stroke is already accounted for in size + * @returns {fabric.Point} dimensions + */ + _getNonTransformedDimensions: function () { + return new fabric.Point(this.width, this.height); + }, + }); /** diff --git a/src/util/misc.js b/src/util/misc.js index 721ed082963..373f4a62077 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -180,17 +180,18 @@ }, /** + * Let A, B, C be vertexes of a triangle, calculate the bisector of A * @static * @memberOf fabric.util - * @param {Point} A - * @param {Point} B - * @param {Point} C + * @param {Point} A the vertex of the bisector + * @param {Point} B a vertex that defines a side of the angle + * @param {Point} C a second vertex that defines the other side of the angle * @returns {{ vector: Point, angle: number }} vector representing the bisector of A and A's angle */ getBisector: function (A, B, C) { var AB = fabric.util.createVector(A, B), AC = fabric.util.createVector(A, C); var alpha = fabric.util.calcAngleBetweenVectors(AB, AC); - // check if alpha is relative to AB->BC + // check if alpha is relative to AB->AC var ro = fabric.util.calcAngleBetweenVectors(fabric.util.rotateVector(AB, alpha), AC); var phi = alpha * (ro === 0 ? 1 : -1) / 2; return { @@ -199,6 +200,44 @@ }; }, + /** + * Calculates the stroke projection vector to apply to `point` + * @static + * @memberOf fabric.util + * @param {Point} point the point to project + * @param {Point} pointBefore a vertex that defines one side of the angle + * @param {Point} pointAfter a vertex that defines a second side of the angle + * @param {Object} options + * @param {number} options.strokeWidth + * @param {'miter'|'bevel'|'round'} options.strokeLineJoin + * @param {number} options.strokeMiterLimit https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit + * @param {boolean} options.strokeUniform + * @param {number} options.scaleX + * @param {number} options.scaleY + * @returns {fabric.Point} a vector representing the stroke projection (direction sign can't be determined) + */ + calcStrokeProjection: function (point, pointBefore, pointAfter, options) { + var s = options.strokeWidth / 2, + strokeUniformScalar = options.strokeUniform ? + new fabric.Point(1 / options.scaleX, 1 / options.scaleY) : + new fabric.Point(1, 1), + bisector = fabric.util.getBisector(point, pointBefore, pointAfter), + bisectorVector = bisector.vector.multiply(strokeUniformScalar), + alpha = bisector.angle, + scalar = -s / Math.sin(alpha / 2), + miterVector = bisectorVector.scalarMultiply(scalar); + if (options.strokeLineJoin === 'miter' && + Math.hypot(miterVector.x, miterVector.y) / s <= options.strokeMiterLimit) { + return miterVector; + } + else { + // calculate bevel, round projections + // incorrect approximation + scalar = -s * Math.SQRT2; + return bisectorVector.scalarMultiply(scalar); + } + }, + /** * Project stroke width on points returning 2 projections for each point as follows: * - `miter`: 2 points corresponding to the outer boundary and the inner boundary of stroke. @@ -241,28 +280,7 @@ B = points[index - 1]; C = points[index + 1]; } - var bisector = fabric.util.getBisector(A, B, C), - bisectorVector = bisector.vector, - alpha = bisector.angle, - scalar, - miterVector; - if (options.strokeLineJoin === 'miter') { - scalar = -s / Math.sin(alpha / 2); - miterVector = new fabric.Point( - bisectorVector.x * scalar * strokeUniformScalar.x, - bisectorVector.y * scalar * strokeUniformScalar.y - ); - if (Math.hypot(miterVector.x, miterVector.y) / s <= options.strokeMiterLimit) { - coords.push(A.add(miterVector)); - coords.push(A.subtract(miterVector)); - return; - } - } - scalar = -s * Math.SQRT2; - miterVector = new fabric.Point( - bisectorVector.x * scalar * strokeUniformScalar.x, - bisectorVector.y * scalar * strokeUniformScalar.y - ); + var miterVector = fabric.util.calcStrokeProjection(A, B, C, options); coords.push(A.add(miterVector)); coords.push(A.subtract(miterVector)); }); diff --git a/src/util/path.js b/src/util/path.js index 2bd9680aaa0..5bb445667b0 100644 --- a/src/util/path.js +++ b/src/util/path.js @@ -187,14 +187,8 @@ bounds[0][jlen + 1] = x3; bounds[1][jlen + 1] = y3; var result = [ - { - x: min.apply(null, bounds[0]), - y: min.apply(null, bounds[1]) - }, - { - x: max.apply(null, bounds[0]), - y: max.apply(null, bounds[1]) - } + new fabric.Point(min.apply(null, bounds[0]), min.apply(null, bounds[1])), + new fabric.Point(max.apply(null, bounds[0]), max.apply(null, bounds[1])) ]; if (fabric.cachesBoundsOfCurve) { fabric.boundsOfCurveCache[argsString] = result; diff --git a/src/util/stroke_projection.svg b/src/util/stroke_projection.svg new file mode 100644 index 00000000000..0d7fae78f2c --- /dev/null +++ b/src/util/stroke_projection.svg @@ -0,0 +1,102 @@ + +Created with Fabric.js 5.2.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + strokeWidth / 2 + + + + + + bisector + + + miter + + + C + + + A + + + B + + + B' + + + C' + + + A' + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/visual/generic_rendering.js b/test/visual/generic_rendering.js index 9d17b981fde..991c20d4ba6 100644 --- a/test/visual/generic_rendering.js +++ b/test/visual/generic_rendering.js @@ -502,5 +502,65 @@ height: 100, }); + var pathWithStrokeBBoxProjectionStr = 'M0,150c0-0.2,0-0.3,0-0.6c3.6-2.7,6.8-5.7,9.7-8.9c4.5-5,8-10.6,9.8-17.1c2-6.6,1.7-13.3,0.3-19.9c-1.5-7.2-3.4-14.3-5-21.5 c-1.5-6.8-2.8-13.6-2.4-20.7c0.3-6.8,2.2-13.1,5.4-19.1c4.4-8.2,10.4-15,18-20.5c7.7-5.4,16.4-8,25.8-8.3c6.7-0.2,13.3,0.7,19.8,2 c5.7,1,11.2,2.4,16.8,3.6c5.8,1.2,11.7,2.2,17.5,3.1c5.4,0.9,11,0.9,16.4,0.1c5.6-0.8,10.6-2.8,15.4-5.8c4.3-2.7,8.1-6.1,11.2-10.2 c1.6-2,2.9-4.2,4.3-6.2c0.1,0,0.2,0,0.2,0c2.4,4.2,5.3,8,8.8,11.3c8.4,8.2,18.5,12.1,30.3,12.3c6.2,0,12.5-1,18.6-2.1 c8.6-1.5,17-3.6,25.6-5.2c6-1.2,12-1.9,18.3-1.6c6,0.2,11.8,1.4,17.3,3.6c4.2,1.6,8,3.9,11.4,6.7c4.3,3.4,7.9,7.3,11.1,11.7 c3.5,4.7,6.2,9.9,7.9,15.6c1.7,6,2.4,12.3,1.5,18.5c-0.8,5.7-1.7,11.2-3,16.8c-1.6,7.3-3.6,14.6-4.4,22.1 c-0.8,7.5,0.1,14.6,3.5,21.4c2.3,4.7,5.3,8.9,9,12.6c2.5,2.4,5.2,4.7,7.9,7.3c-0.2,0.2-0.6,0.6-0.9,0.8c-4.7,3.8-9.3,7.9-12.8,12.8 c-2.9,4-5.2,8.3-6.5,13.1c-1.7,6.6-1.6,13.4-0.2,20.1c1.6,7.9,3.6,15.7,5.2,23.6c1.2,5.2,1.9,10.5,1.7,16 c-0.2,6.6-1.6,12.8-4.5,18.7c-2,4.2-4.4,8-7.3,11.4c-3.4,4-7.1,7.7-11.4,10.8c-7.1,4.9-14.8,7.6-23.2,8.3c-3.9,0.3-7.9,0.2-11.8-0.2 c-6.9-0.9-13.8-2.1-20.6-3.4c-8-1.5-16-3.2-23.9-4.7c-4.3-0.8-8.6-0.9-13-0.7c-6.2,0.3-11.9,2-17.3,5c-4.7,2.7-8.7,6.2-12.1,10.4 c-1.9,2.2-3.5,4.6-5.2,7.1c-0.3-0.5-0.7-0.9-0.9-1.3c-2.1-2.8-4-5.8-6.4-8.4c-3.2-3.6-6.9-6.5-11.2-8.9c-4.6-2.5-9.6-3.9-14.7-4.6 c-6.5-0.8-13-0.1-19.3,1.2c-9.6,1.9-19.1,3.9-28.7,5.7c-4.4,0.8-8.7,1.5-13.2,1.7c-3.5,0.2-7.1,0-10.5-0.2c-8-0.6-15.4-3.4-22-7.9 c-5.8-3.9-10.3-9-14.3-14.8c-8.1-11.6-10.9-24.3-8.1-38c1.4-7.1,3.1-14,4.6-21c1.5-6.8,3-13.6,2.3-20.7c-0.5-4.9-1.9-9.4-4-13.8 c-2.7-5.3-6.5-9.7-10.8-13.6C4.4,153.4,2.2,151.7,0,150z'; + + ['miter', 'bevel', 'round'].forEach(strokeLineJoin => { + function pathWithStrokeBBoxProjection(canvas, callback) { + var path = new fabric.Path(pathWithStrokeBBoxProjectionStr, { + fill: 'black', + stroke: 'red', + strokeWidth: 100, + paintFirst: 'stroke', + hasControls: false, + strokeLineJoin + }); + canvas.add( + path + ); + canvas.setActiveObject(path); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + function pathWithStrokeBBoxProjectionWithPosition(canvas, callback) { + var path = new fabric.Path(pathWithStrokeBBoxProjectionStr, { + fill: 'black', + stroke: 'red', + strokeWidth: 100, + paintFirst: 'stroke', + hasControls: false, + strokeLineJoin + }); + canvas.add( + path + ); + canvas.setActiveObject(path); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'path with stroke bbox projection ' + strokeLineJoin, + code: pathWithStrokeBBoxProjection, + golden: 'path_stroke_projection_' + strokeLineJoin+'.png', + newModule: 'bbox', + percentage: 0.004, + width: 700, + height: 600, + fabricClass: 'Canvas' + }); + + tests.push({ + test: 'path with stroke bbox projection ' + strokeLineJoin, + code: pathWithStrokeBBoxProjectionWithPosition, + golden: 'path_stroke_projection_' + strokeLineJoin + '.png', + newModule: 'bbox', + percentage: 0.004, + width: 700, + height: 600, + fabricClass: 'Canvas' + }); + }); + tests.forEach(visualTestLoop(QUnit)); })(); diff --git a/test/visual/golden/path_stroke_projection_bevel.png b/test/visual/golden/path_stroke_projection_bevel.png new file mode 100644 index 00000000000..543c32bbff8 Binary files /dev/null and b/test/visual/golden/path_stroke_projection_bevel.png differ diff --git a/test/visual/golden/path_stroke_projection_miter.png b/test/visual/golden/path_stroke_projection_miter.png new file mode 100644 index 00000000000..fa8df615da2 Binary files /dev/null and b/test/visual/golden/path_stroke_projection_miter.png differ diff --git a/test/visual/golden/path_stroke_projection_round.png b/test/visual/golden/path_stroke_projection_round.png new file mode 100644 index 00000000000..4cc96033263 Binary files /dev/null and b/test/visual/golden/path_stroke_projection_round.png differ diff --git a/test/visual/golden/textpath4.png b/test/visual/golden/textpath4.png index 5d9198a8300..e1461256219 100644 Binary files a/test/visual/golden/textpath4.png and b/test/visual/golden/textpath4.png differ diff --git a/test/visual/golden/textpath5.png b/test/visual/golden/textpath5.png index 1ebaaaf5779..6b3220e6fae 100644 Binary files a/test/visual/golden/textpath5.png and b/test/visual/golden/textpath5.png differ diff --git a/test/visual/z_svg_export.js b/test/visual/z_svg_export.js index cd9bc3eddc9..c335ecdf51d 100644 --- a/test/visual/z_svg_export.js +++ b/test/visual/z_svg_export.js @@ -369,7 +369,7 @@ }); function clipping13(canvas, callback) { - var jsonData = '{"version":"2.4.6","objects":[{"type":"path","version":"2.4.6","left":84.63385601266276,"top":385.11623376730074,"width":3.14,"height":10.44,"fill":{"type":"linear","coords":{"x1":481.066,"y1":785.465,"x2":480.953,"y2":793.102},"colorStops":[{"offset":1,"color":"rgb(160,137,44)","opacity":1},{"offset":0.9,"color":"rgb(85,68,0)","opacity":1},{"offset":0.78,"color":"rgb(80,68,22)","opacity":1},{"offset":0.607,"color":"rgb(160,137,44)","opacity":1},{"offset":0.467,"color":"rgb(255,255,255)","opacity":1},{"offset":0.299,"color":"rgb(200,171,55)","opacity":1},{"offset":0.24,"color":"rgb(160,137,44)","opacity":1},{"offset":0.096,"color":"rgb(120,103,33)","opacity":1},{"offset":0,"color":"rgb(211,188,95)","opacity":1}],"offsetX":-439.1113425994523,"offsetY":-783.951,"gradientTransform":[-1,0,0,1,921.58,0]},"scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",442.142,794.394],["l",-2.583,-1.46],["c",-0.644,-2.39,-0.512,-5.004,-0.113,-7.693],["l",2.808,-1.29]]},{"type":"path","version":"2.4.6","left":78.08737427855635,"top":315.5127636903678,"width":2.42,"height":11.51,"fill":{"type":"linear","coords":{"x1":473.934,"y1":792.821,"x2":473.822,"y2":784.005},"colorStops":[{"offset":1,"color":"rgb(211,188,95)","opacity":1},{"offset":0.904,"color":"rgb(120,103,33)","opacity":1},{"offset":0.76,"color":"rgb(160,137,44)","opacity":1},{"offset":0.701,"color":"rgb(200,171,55)","opacity":1},{"offset":0.533,"color":"rgb(255,255,255)","opacity":1},{"offset":0.393,"color":"rgb(160,137,44)","opacity":1},{"offset":0.22,"color":"rgb(80,68,22)","opacity":1},{"offset":0.1,"color":"rgb(85,68,0)","opacity":1},{"offset":0,"color":"rgb(160,137,44)","opacity":1}],"offsetX":-446.578,"offsetY":-783.332,"gradientTransform":[-1,0,0,1,921.58,0]},"scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",448.32,783.332],["l",0.673,2.02],["c",-0.885,1.242,-0.83,2.952,-0.075,7.92],["l",-1.16,1.57],["l",-1.18,-4.49],["l",0.17,-5.673],["z"]]},{"type":"path","version":"2.4.6","left":92.34368668174653,"top":366.5275069557187,"width":6.57,"height":5.67,"fill":{"type":"linear","coords":{"x1":475.081,"y1":785.381,"x2":479.3,"y2":788.975},"colorStops":[{"offset":1,"color":"rgb(120,103,33)","opacity":1},{"offset":0,"color":"rgb(200,171,55)","opacity":1}],"offsetX":-441.1054336190674,"offsetY":-784.68,"gradientTransform":[-1,0,0,1,921.58,0]},"scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",447.365,784.68],["h",-5.616],["c",-0.592,1.87,-0.812,3.743,-0.506,5.615],["l",5.952,0.056],["c",0.658,-1.976,0.554,-3.843,0.17,-5.67],["z"]]},{"type":"path","version":"2.4.6","left":151.23029459047214,"top":365.75783100618594,"width":6.58,"height":4.48,"fill":{"type":"linear","coords":{"x1":476.181,"y1":791.235,"x2":477.099,"y2":794.257},"colorStops":[{"offset":1,"color":"rgb(120,103,33)","opacity":1},{"offset":0,"color":"rgb(200,171,55)","opacity":1}],"offsetX":-441.18800000000005,"offsetY":-790.248,"gradientTransform":[-1,0,0,1,921.58,0]},"scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",447.196,790.248],["c",0.397,1.492,0.613,3.14,0.562,4.483],["l",-5.503,-0.056],["c",-0.974,-1.853,-0.957,-3.128,-1.067,-4.406],["z"]]},{"type":"path","version":"2.4.6","left":78.67962464545468,"top":360.50959855742764,"width":6.4,"height":1.35,"fill":{"type":"linear","coords":{"x1":473.32,"y1":784.06,"x2":479.896,"y2":784.06},"colorStops":[{"offset":1,"color":"rgb(200,171,55)","opacity":1},{"offset":0,"color":"rgb(120,103,33)","opacity":1}],"offsetX":-441.751,"offsetY":-783.3879999999999,"gradientTransform":[-1,0,0,1,921.58,0]},"scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",447.196,784.68],["c",0.783,-0.43,0.715,-0.862,0.955,-1.292],["l",-5.67,0.224],["c",-0.308,0.343,-0.704,0.64,-0.73,1.123],["z"]]},{"type":"path","version":"2.4.6","left":195.70884317712697,"top":264.26532049868615,"width":2.99,"height":8.78,"fill":{"type":"linear","coords":{"x1":481.066,"y1":785.465,"x2":480.953,"y2":793.102},"colorStops":[{"offset":1,"color":"rgb(160,137,44)","opacity":1},{"offset":0.9,"color":"rgb(85,68,0)","opacity":1},{"offset":0.78,"color":"rgb(80,68,22)","opacity":1},{"offset":0.607,"color":"rgb(160,137,44)","opacity":1},{"offset":0.467,"color":"rgb(255,255,255)","opacity":1},{"offset":0.299,"color":"rgb(200,171,55)","opacity":1},{"offset":0.24,"color":"rgb(160,137,44)","opacity":1},{"offset":0.096,"color":"rgb(120,103,33)","opacity":1},{"offset":0,"color":"rgb(211,188,95)","opacity":1}],"offsetX":-228.298,"offsetY":-835.244,"gradientTransform":[0.93343,0,0,0.85628,-219.064,163.965]},"scaleX":9.32,"scaleY":10.58,"angle":90,"flipY":true,"path":[["M",228.298,844.02],["l",2.57,-1.083],["c",0.6,-2.048,0.477,-4.285,0.104,-6.587],["l",-2.62,-1.106]]},{"type":"circle","version":"2.4.6","left":-28.49,"top":-28.49,"width":4.64,"height":4.64,"fill":{"type":"radial","coords":{"x1":193.676,"y1":141.252,"x2":193.676,"y2":141.252,"r1":0,"r2":4.082},"colorStops":[{"offset":1,"color":"rgb(0,0,0)","opacity":1},{"offset":0.969,"color":"rgb(0,0,0)","opacity":1},{"offset":0.904,"color":"rgb(236,236,236)","opacity":1},{"offset":0.874,"color":"rgb(77,77,77)","opacity":1},{"offset":0.837,"color":"rgb(237,237,237)","opacity":1},{"offset":0.817,"color":"rgb(0,0,0)","opacity":1},{"offset":0,"color":"rgb(0,0,0)","opacity":1}],"offsetX":-116.293,"offsetY":-166.167,"gradientTransform":[0.3487,0.40483,-0.40345,0.34752,108.054,40.97]},"scaleX":59.8,"scaleY":59.8,"radius":2.321,"startAngle":0,"endAngle":360},{"type":"circle","version":"2.4.6","left":32.24,"top":30.43,"width":3.58,"height":3.58,"fill":{"type":"linear","coords":{"x1":195.171,"y1":143.461,"x2":191.574,"y2":138.568},"colorStops":[{"offset":1,"color":"rgb(204,204,204)","opacity":1},{"offset":0.687,"color":"rgb(255,255,255)","opacity":1},{"offset":0,"color":"rgb(255,255,255)","opacity":1}],"offsetX":-116.817,"offsetY":-166.661,"gradientTransform":[0.52872,0,0,0.52872,16.3,93.714]},"stroke":"#b3b3b3","strokeWidth":0.02,"strokeLineCap":"round","scaleX":59.8,"scaleY":59.8,"radius":1.789,"startAngle":0,"endAngle":360},{"type":"circle","version":"2.4.6","left":125.91,"top":124.11,"width":0.46,"height":0.46,"fill":{"type":"linear","coords":{"x1":656.429,"y1":320.934,"x2":506.429,"y2":131.648},"colorStops":[{"offset":1,"color":"rgb(242,242,242)","opacity":1},{"offset":0,"color":"rgb(102,102,102)","opacity":1}],"offsetX":-118.37599999999999,"offsetY":-168.22,"gradientTransform":[0.0017,0,0,0.0017,117.64,168.082]},"stroke":"#999","strokeWidth":0,"strokeLineCap":"round","scaleX":59.8,"scaleY":59.8,"radius":0.23,"startAngle":0,"endAngle":360}]}'; + var jsonData = '{"version":"5.1.0","objects":[{"type":"group","version":"5.1.0","left":-28.49,"top":-28.49,"width":337.3916,"height":413.6066,"objects":[{"type":"path","version":"5.1.0","left":-55.572,"top":206.8033,"width":3.1427,"height":10.443,"fill":{"type":"linear","coords":{"x1":481.066,"y1":785.465,"x2":480.953,"y2":793.102},"colorStops":[{"offset":1,"color":"rgb(160,137,44)","opacity":1},{"offset":0.9,"color":"rgb(85,68,0)","opacity":1},{"offset":0.78,"color":"rgb(80,68,22)","opacity":1},{"offset":0.607,"color":"rgb(160,137,44)","opacity":1},{"offset":0.467,"color":"rgb(255,255,255)","opacity":1},{"offset":0.299,"color":"rgb(200,171,55)","opacity":1},{"offset":0.24,"color":"rgb(160,137,44)","opacity":1},{"offset":0.096,"color":"rgb(120,103,33)","opacity":1},{"offset":0,"color":"rgb(211,188,95)","opacity":1}],"offsetX":-439.1113425994523,"offsetY":-783.951,"gradientUnits":"pixels","gradientTransform":[-1,0,0,1,921.58,0]},"stroke":"","scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",442.142,794.394],["L",439.55899999999997,792.934],["C",438.91499999999996,790.544,439.04699999999997,787.93,439.44599999999997,785.241],["L",442.25399999999996,783.951]]},{"type":"path","version":"5.1.0","left":-64.4041,"top":137.1995,"width":2.7626,"height":12.3327,"fill":{"type":"linear","coords":{"x1":473.934,"y1":792.821,"x2":473.822,"y2":784.005},"colorStops":[{"offset":1,"color":"rgb(211,188,95)","opacity":1},{"offset":0.904,"color":"rgb(120,103,33)","opacity":1},{"offset":0.76,"color":"rgb(160,137,44)","opacity":1},{"offset":0.701,"color":"rgb(200,171,55)","opacity":1},{"offset":0.533,"color":"rgb(255,255,255)","opacity":1},{"offset":0.393,"color":"rgb(160,137,44)","opacity":1},{"offset":0.22,"color":"rgb(80,68,22)","opacity":1},{"offset":0.1,"color":"rgb(85,68,0)","opacity":1},{"offset":0,"color":"rgb(160,137,44)","opacity":1}],"offsetX":-446.578,"offsetY":-783.115958749821,"gradientUnits":"pixels","gradientTransform":[-1,0,0,1,921.58,0]},"stroke":"","scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",448.32,783.332],["L",448.993,785.352],["C",448.108,786.5939999999999,448.163,788.304,448.918,793.2719999999999],["L",447.758,794.842],["L",446.578,790.352],["L",446.748,784.679],["z"]]},{"type":"path","version":"5.1.0","left":-59.9006,"top":191.9376,"width":6.9652,"height":7.8907,"fill":{"type":"linear","coords":{"x1":475.081,"y1":785.381,"x2":479.3,"y2":788.975},"colorStops":[{"offset":1,"color":"rgb(120,103,33)","opacity":1},{"offset":0,"color":"rgb(200,171,55)","opacity":1}],"offsetX":-440.70594607384373,"offsetY":-783.5421461913462,"gradientUnits":"pixels","gradientTransform":[-1,0,0,1,921.58,0]},"stroke":"","scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",447.365,784.68],["L",441.749,784.68],["C",441.15700000000004,786.55,440.937,788.423,441.24300000000005,790.295],["L",447.19500000000005,790.351],["C",447.85300000000007,788.375,447.749,786.508,447.36500000000007,784.681],["z"]]},{"type":"path","version":"5.1.0","left":1.4031,"top":190.1394,"width":7.4814,"height":6.2775,"fill":{"type":"linear","coords":{"x1":476.181,"y1":791.235,"x2":477.099,"y2":794.257},"colorStops":[{"offset":1,"color":"rgb(120,103,33)","opacity":1},{"offset":0,"color":"rgb(200,171,55)","opacity":1}],"offsetX":-440.8988611577768,"offsetY":-789.3386038208589,"gradientUnits":"pixels","gradientTransform":[-1,0,0,1,921.58,0]},"stroke":"","scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",447.196,790.248],["C",447.593,791.74,447.809,793.388,447.75800000000004,794.731],["L",442.25500000000005,794.675],["C",441.28100000000006,792.822,441.29800000000006,791.5469999999999,441.18800000000005,790.269],["z"]]},{"type":"path","version":"5.1.0","left":-67.4486,"top":184.3337,"width":6.8373,"height":2.4714,"fill":{"type":"linear","coords":{"x1":473.32,"y1":784.06,"x2":479.896,"y2":784.06},"colorStops":[{"offset":1,"color":"rgb(200,171,55)","opacity":1},{"offset":0,"color":"rgb(120,103,33)","opacity":1}],"offsetX":-441.5216681728531,"offsetY":-782.828220356729,"gradientUnits":"pixels","gradientTransform":[-1,0,0,1,921.58,0]},"stroke":"","scaleX":9.32,"scaleY":10.58,"angle":-90,"path":[["M",447.196,784.68],["C",447.97900000000004,784.25,447.911,783.818,448.151,783.3879999999999],["L",442.481,783.612],["C",442.173,783.9549999999999,441.777,784.252,441.751,784.735],["z"]]},{"type":"path","version":"5.1.0","left":55.503,"top":85.9516,"width":2.9869,"height":8.776,"fill":{"type":"linear","coords":{"x1":481.066,"y1":785.465,"x2":480.953,"y2":793.102},"colorStops":[{"offset":1,"color":"rgb(160,137,44)","opacity":1},{"offset":0.9,"color":"rgb(85,68,0)","opacity":1},{"offset":0.78,"color":"rgb(80,68,22)","opacity":1},{"offset":0.607,"color":"rgb(160,137,44)","opacity":1},{"offset":0.467,"color":"rgb(255,255,255)","opacity":1},{"offset":0.299,"color":"rgb(200,171,55)","opacity":1},{"offset":0.24,"color":"rgb(160,137,44)","opacity":1},{"offset":0.096,"color":"rgb(120,103,33)","opacity":1},{"offset":0,"color":"rgb(211,188,95)","opacity":1}],"offsetX":-228.298,"offsetY":-835.244,"gradientUnits":"pixels","gradientTransform":[0.9334,0,0,0.8563,-219.064,163.965]},"stroke":"","scaleX":9.32,"scaleY":10.58,"angle":90,"flipY":true,"path":[["M",228.298,844.02],["L",230.868,842.937],["C",231.468,840.889,231.345,838.652,230.972,836.35],["L",228.352,835.244]]},{"type":"circle","version":"5.1.0","left":-168.6958,"top":-206.8033,"width":4.642,"height":4.642,"fill":{"type":"radial","coords":{"x1":193.676,"y1":141.252,"x2":193.676,"y2":141.252,"r1":0,"r2":4.082},"colorStops":[{"offset":1,"color":"rgb(0,0,0)","opacity":1},{"offset":0.9689999999999999,"color":"rgb(0,0,0)","opacity":1},{"offset":0.904,"color":"rgb(236,236,236)","opacity":1},{"offset":0.8740000000000001,"color":"rgb(77,77,77)","opacity":1},{"offset":0.8370000000000001,"color":"rgb(237,237,237)","opacity":1},{"offset":0.8169999999999998,"color":"rgb(0,0,0)","opacity":1},{"offset":0,"color":"rgb(0,0,0)","opacity":1}],"offsetX":2.321,"offsetY":2.321,"gradientUnits":"pixels","gradientTransform":[0.3487,0.4048,-0.4034,0.3475,-10.56,-127.518]},"stroke":"","scaleX":59.8,"scaleY":59.8,"radius":2.321},{"type":"circle","version":"5.1.0","left":-107.9658,"top":-147.8833,"width":3.578,"height":3.578,"fill":{"type":"linear","coords":{"x1":195.171,"y1":143.461,"x2":191.574,"y2":138.568},"colorStops":[{"offset":1,"color":"rgb(204,204,204)","opacity":1},{"offset":0.687,"color":"rgb(255,255,255)","opacity":1},{"offset":0,"color":"rgb(255,255,255)","opacity":1}],"offsetX":1.789,"offsetY":1.789,"gradientUnits":"pixels","gradientTransform":[0.5287,0,0,0.5287,-102.306,-74.736]},"stroke":"rgb(179,179,179)","strokeWidth":0.02,"strokeLineCap":"round","scaleX":59.8,"scaleY":59.8,"radius":1.789},{"type":"circle","version":"5.1.0","left":-14.2958,"top":-54.2033,"width":0.46,"height":0.46,"fill":{"type":"linear","coords":{"x1":656.429,"y1":320.934,"x2":506.429,"y2":131.648},"colorStops":[{"offset":1,"color":"rgb(242,242,242)","opacity":1},{"offset":0,"color":"rgb(102,102,102)","opacity":1}],"offsetX":0.23,"offsetY":0.23,"gradientUnits":"pixels","gradientTransform":[0.0017,0,0,0.0017,-0.966,-0.368]},"stroke":"rgb(153,153,153)","strokeWidth":0,"strokeLineCap":"round","scaleX":59.8,"scaleY":59.8,"radius":0.23}]}]}'; canvas.loadFromJSON(jsonData).then(function() { toSVGCanvas(canvas, callback); }); @@ -379,7 +379,7 @@ test: 'Export complex gradients', code: clipping13, golden: 'clipping13.png', - percentage: 0.06, + percentage: 0.02, width: 290, height: 400, });