diff --git a/README.md b/README.md index c8e82d2..b26448c 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,21 @@ dom.wrapper; // a div with the svg as a child dom.svg; // the inline svg element ``` +Working with paths directly: + +```javascript +import { Context, Path2D } from "svgcanvas"; + +const ctx = new Context(500, 500); + +// Create a path: +const path = new Path2D(ctx, "M 230 80 L 275 80 Z"); // or ctx.createPath("M 230 80 L 275 80 Z"); +ctx.stroke(path); + +// serialize your SVG +const mySerializedSVG = ctx.getSerializedSvg(); +``` + ## Tests https://zenozeng.github.io/p5.js-svg/test/ diff --git a/context.js b/context.js index 8b22aca..e512322 100644 --- a/context.js +++ b/context.js @@ -14,22 +14,15 @@ */ import * as utils from './utils'; +import { format } from './utils'; import imageUtils from './image'; +import Path2D from './path2d'; export default (function () { "use strict"; var STYLES, Context, CanvasGradient, CanvasPattern, namedEntities; - //helper function to format a string - function format(str, args) { - var keys = Object.keys(args), i; - for (i=0; i -1) { - this.__addPathCommand(format("L {x} {y}", { - x: this.__matrixTransform(x, y).x, - y: this.__matrixTransform(x, y).y - })); - } else { - this.__addPathCommand(format("M {x} {y}", { - x: this.__matrixTransform(x, y).x, - y: this.__matrixTransform(x, y).y - })); - } - }; - - /** - * Add a bezier command - */ - Context.prototype.bezierCurveTo = function (cp1x, cp1y, cp2x, cp2y, x, y) { - this.__currentPosition = {x: x, y: y}; - this.__addPathCommand(format("C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}", - { - cp1x: this.__matrixTransform(cp1x, cp1y).x, - cp1y: this.__matrixTransform(cp1x, cp1y).y, - cp2x: this.__matrixTransform(cp2x, cp2y).x, - cp2y: this.__matrixTransform(cp2x, cp2y).y, - x: this.__matrixTransform(x, y).x, - y: this.__matrixTransform(x, y).y - })); - }; - - /** - * Adds a quadratic curve to command - */ - Context.prototype.quadraticCurveTo = function (cpx, cpy, x, y) { - this.__currentPosition = {x: x, y: y}; - this.__addPathCommand(format("Q {cpx} {cpy} {x} {y}", { - cpx: this.__matrixTransform(cpx, cpy).x, - cpy: this.__matrixTransform(cpx, cpy).y, - x: this.__matrixTransform(x, y).x, - y: this.__matrixTransform(x, y).y - })); - }; - - - /** - * Return a new normalized vector of given vector - */ - var normalize = function (vector) { - var len = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]); - return [vector[0] / len, vector[1] / len]; - }; - - /** - * Adds the arcTo to the current path - * - * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto - */ - Context.prototype.arcTo = function (x1, y1, x2, y2, radius) { - // Let the point (x0, y0) be the last point in the subpath. - var x0 = this.__currentPosition && this.__currentPosition.x; - var y0 = this.__currentPosition && this.__currentPosition.y; - - // First ensure there is a subpath for (x1, y1). - if (typeof x0 == "undefined" || typeof y0 == "undefined") { - return; - } - - // Negative values for radius must cause the implementation to throw an IndexSizeError exception. - if (radius < 0) { - throw new Error("IndexSizeError: The radius provided (" + radius + ") is negative."); - } - - // If the point (x0, y0) is equal to the point (x1, y1), - // or if the point (x1, y1) is equal to the point (x2, y2), - // or if the radius radius is zero, - // then the method must add the point (x1, y1) to the subpath, - // and connect that point to the previous point (x0, y0) by a straight line. - if (((x0 === x1) && (y0 === y1)) - || ((x1 === x2) && (y1 === y2)) - || (radius === 0)) { - this.lineTo(x1, y1); - return; - } - - // Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line, - // then the method must add the point (x1, y1) to the subpath, - // and connect that point to the previous point (x0, y0) by a straight line. - var unit_vec_p1_p0 = normalize([x0 - x1, y0 - y1]); - var unit_vec_p1_p2 = normalize([x2 - x1, y2 - y1]); - if (unit_vec_p1_p0[0] * unit_vec_p1_p2[1] === unit_vec_p1_p0[1] * unit_vec_p1_p2[0]) { - this.lineTo(x1, y1); - return; - } - - // Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius, - // and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1), - // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1), and crosses the point (x2, y2). - // The points at which this circle touches these two lines are called the start and end tangent points respectively. - - // note that both vectors are unit vectors, so the length is 1 - var cos = (unit_vec_p1_p0[0] * unit_vec_p1_p2[0] + unit_vec_p1_p0[1] * unit_vec_p1_p2[1]); - var theta = Math.acos(Math.abs(cos)); - - // Calculate origin - var unit_vec_p1_origin = normalize([ - unit_vec_p1_p0[0] + unit_vec_p1_p2[0], - unit_vec_p1_p0[1] + unit_vec_p1_p2[1] - ]); - var len_p1_origin = radius / Math.sin(theta / 2); - var x = x1 + len_p1_origin * unit_vec_p1_origin[0]; - var y = y1 + len_p1_origin * unit_vec_p1_origin[1]; - - // Calculate start angle and end angle - // rotate 90deg clockwise (note that y axis points to its down) - var unit_vec_origin_start_tangent = [ - -unit_vec_p1_p0[1], - unit_vec_p1_p0[0] - ]; - // rotate 90deg counter clockwise (note that y axis points to its down) - var unit_vec_origin_end_tangent = [ - unit_vec_p1_p2[1], - -unit_vec_p1_p2[0] - ]; - var getAngle = function (vector) { - // get angle (clockwise) between vector and (1, 0) - var x = vector[0]; - var y = vector[1]; - if (y >= 0) { // note that y axis points to its down - return Math.acos(x); - } else { - return -Math.acos(x); - } - }; - var startAngle = getAngle(unit_vec_origin_start_tangent); - var endAngle = getAngle(unit_vec_origin_end_tangent); - - // Connect the point (x0, y0) to the start tangent point by a straight line - this.lineTo(x + unit_vec_origin_start_tangent[0] * radius, - y + unit_vec_origin_start_tangent[1] * radius); - - // Connect the start tangent point to the end tangent point by arc - // and adding the end tangent point to the subpath. - this.arc(x, y, radius, startAngle, endAngle); - }; - /** * Sets the stroke property on the current element */ - Context.prototype.stroke = function () { - if (this.__currentElement.nodeName === "path") { - this.__currentElement.setAttribute("paint-order", "fill stroke markers"); + Context.prototype.stroke = function (path2d) { + if (path2d) { + var path = this.__createPathElement(); + this.__applyStyleToElement(path, "stroke"); + path.setAttribute("paint-order", "fill stroke markers"); + path.setAttribute("d", path2d.__pathString); + } else { + if (this.__currentElement.nodeName === "path") { + this.__currentElement.setAttribute("paint-order", "fill stroke markers"); + } + this.__applyCurrentDefaultPath(); + this.__applyStyleToElement(this.__currentElement, "stroke"); } - this.__applyCurrentDefaultPath(); - this.__applyStyleToCurrentElement("stroke"); }; /** * Sets fill properties on the current element */ - Context.prototype.fill = function () { - if (this.__currentElement.nodeName === "path") { - this.__currentElement.setAttribute("paint-order", "stroke fill markers"); + Context.prototype.fill = function (path2d) { + if (path2d) { + var path = this.__createPathElement(); + this.__applyStyleToElement(path, "fill"); + path.setAttribute("paint-order", "fill stroke markers"); + path.setAttribute("d", path2d.__pathString); + } else { + if (this.__currentElement.nodeName === "path") { + this.__currentElement.setAttribute("paint-order", "stroke fill markers"); + } + this.__applyCurrentDefaultPath(); + this.__applyStyleToElement(this.__currentElement, "fill"); } - this.__applyCurrentDefaultPath(); - this.__applyStyleToCurrentElement("fill"); }; - /** - * Adds a rectangle to the path. - */ - Context.prototype.rect = function (x, y, width, height) { - if (this.__currentElement.nodeName !== "path") { - this.beginPath(); - } - this.moveTo(x, y); - this.lineTo(x+width, y); - this.lineTo(x+width, y+height); - this.lineTo(x, y+height); - this.lineTo(x, y); - this.closePath(); - }; - - /** * adds a rectangle element */ @@ -811,7 +687,7 @@ export default (function () { parent.appendChild(rect); this.__currentElement = rect; this.__applyTransformation(rect); - this.__applyStyleToCurrentElement("fill"); + this.__applyStyleToElement(this.__currentElement, "fill"); }; /** @@ -833,7 +709,7 @@ export default (function () { parent.appendChild(rect); this.__currentElement = rect; this.__applyTransformation(rect); - this.__applyStyleToCurrentElement("stroke"); + this.__applyStyleToElement(this.__currentElement, "stroke"); }; @@ -942,7 +818,7 @@ export default (function () { textElement.appendChild(this.__document.createTextNode(text)); this.__currentElement = textElement; this.__applyTransformation(textElement); - this.__applyStyleToCurrentElement(action); + this.__applyStyleToElement(this.__currentElement, action); if (this.__fontHref) { var a = this.__createElement("a"); @@ -985,120 +861,6 @@ export default (function () { return this.__ctx.measureText(text); }; - /** - * Arc command! - */ - Context.prototype.arc = function (x, y, radius, startAngle, endAngle, counterClockwise) { - // in canvas no circle is drawn if no angle is provided. - if (startAngle === endAngle) { - return; - } - startAngle = startAngle % (2*Math.PI); - endAngle = endAngle % (2*Math.PI); - if (startAngle === endAngle) { - //circle time! subtract some of the angle so svg is happy (svg elliptical arc can't draw a full circle) - endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI); - } - var endX = x+radius*Math.cos(endAngle), - endY = y+radius*Math.sin(endAngle), - startX = x+radius*Math.cos(startAngle), - startY = y+radius*Math.sin(startAngle), - sweepFlag = counterClockwise ? 0 : 1, - largeArcFlag = 0, - diff = endAngle - startAngle; - - // https://github.com/gliffy/canvas2svg/issues/4 - if (diff < 0) { - diff += 2*Math.PI; - } - - if (counterClockwise) { - largeArcFlag = diff > Math.PI ? 0 : 1; - } else { - largeArcFlag = diff > Math.PI ? 1 : 0; - } - - var scaleX = Math.hypot(this.__transformMatrix.a, this.__transformMatrix.b); - var scaleY = Math.hypot(this.__transformMatrix.c, this.__transformMatrix.d); - - this.lineTo(startX, startY); - this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", - { - rx:radius * scaleX, - ry:radius * scaleY, - xAxisRotation:0, - largeArcFlag:largeArcFlag, - sweepFlag:sweepFlag, - endX: this.__matrixTransform(endX, endY).x, - endY: this.__matrixTransform(endX, endY).y - })); - - this.__currentPosition = {x: endX, y: endY}; - }; - - /** - * Ellipse command! - */ - Context.prototype.ellipse = function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterClockwise) { - if (startAngle === endAngle) { - return; - } - - var transformedCenter = this.__matrixTransform(x, y); - x = transformedCenter.x; - y = transformedCenter.y; - var scale = this.__getTransformScale(); - radiusX = radiusX * scale.x; - radiusY = radiusY * scale.y; - rotation = rotation + this.__getTransformRotation() - - startAngle = startAngle % (2*Math.PI); - endAngle = endAngle % (2*Math.PI); - if(startAngle === endAngle) { - endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI); - } - var endX = x + Math.cos(-rotation) * radiusX * Math.cos(endAngle) - + Math.sin(-rotation) * radiusY * Math.sin(endAngle), - endY = y - Math.sin(-rotation) * radiusX * Math.cos(endAngle) - + Math.cos(-rotation) * radiusY * Math.sin(endAngle), - startX = x + Math.cos(-rotation) * radiusX * Math.cos(startAngle) - + Math.sin(-rotation) * radiusY * Math.sin(startAngle), - startY = y - Math.sin(-rotation) * radiusX * Math.cos(startAngle) - + Math.cos(-rotation) * radiusY * Math.sin(startAngle), - sweepFlag = counterClockwise ? 0 : 1, - largeArcFlag = 0, - diff = endAngle - startAngle; - - if(diff < 0) { - diff += 2*Math.PI; - } - - if(counterClockwise) { - largeArcFlag = diff > Math.PI ? 0 : 1; - } else { - largeArcFlag = diff > Math.PI ? 1 : 0; - } - - // Transform is already applied, so temporarily remove since lineTo - // will apply it again. - var currentTransform = this.__transformMatrix; - this.resetTransform(); - this.lineTo(startX, startY); - this.__transformMatrix = currentTransform; - - this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", - { - rx:radiusX, - ry:radiusY, - xAxisRotation:rotation*(180/Math.PI), - largeArcFlag:largeArcFlag, - sweepFlag:sweepFlag, - endX:endX, - endY:endY - })); - - this.__currentPosition = {x: endX, y: endY}; - }; /** * Generates a ClipPath from the clip command. diff --git a/index.js b/index.js index b945055..81ded5c 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,7 @@ import Context from './context'; import Element from './element'; +import Path2D from './path2d'; export {Context}; -export {Element}; \ No newline at end of file +export {Element}; +export {Path2D}; \ No newline at end of file diff --git a/path2d.js b/path2d.js new file mode 100644 index 0000000..a59e018 --- /dev/null +++ b/path2d.js @@ -0,0 +1,341 @@ +import { format } from './utils'; + +export default (function () { + "use strict"; + + var Path2D; + + Path2D = function (ctx, arg) { + if (!ctx) { + console.error("Path2D must be passed the context"); + } + if (typeof arg === 'string') { + // Initialize from string path. + this.__pathString = arg; + } else if (typeof arg === 'object') { + // Initialize by copying another path. + this.__pathString = arg.__pathString; + } else { + // Initialize a new path. + this.__pathString = ""; + } + + this.ctx = ctx; + this.__currentPosition = {x: undefined, y: undefined}; + } + + Path2D.prototype.__matrixTransform = function(x, y) { + return this.ctx.__matrixTransform(x, y); + } + + Path2D.prototype.addPath = function(path, transform) { + if (transform) console.error("transform argument to addPath is not supported"); + + this.__pathString = this.__pathString + " " + path; + } + + /** + * Closes the current path + */ + Path2D.prototype.closePath = function () { + this.addPath("Z"); + }; + + /** + * Adds the move command to the current path element, + * if the currentPathElement is not empty create a new path element + */ + Path2D.prototype.moveTo = function (x,y) { + // creates a new subpath with the given point + this.__currentPosition = {x: x, y: y}; + this.addPath(format("M {x} {y}", { + x: this.__matrixTransform(x, y).x, + y: this.__matrixTransform(x, y).y + })); + }; + + + /** + * Adds a line to command + */ + Path2D.prototype.lineTo = function (x, y) { + this.__currentPosition = {x: x, y: y}; + if (this.__pathString.indexOf('M') > -1) { + this.addPath(format("L {x} {y}", { + x: this.__matrixTransform(x, y).x, + y: this.__matrixTransform(x, y).y + })); + } else { + this.addPath(format("M {x} {y}", { + x: this.__matrixTransform(x, y).x, + y: this.__matrixTransform(x, y).y + })); + } + }; + + /** + * Adds a rectangle to the path. + */ + Path2D.prototype.rect = function (x, y, width, height) { + if (this.__currentElement.nodeName !== "path") { + this.beginPath(); + } + this.moveTo(x, y); + this.lineTo(x+width, y); + this.lineTo(x+width, y+height); + this.lineTo(x, y+height); + this.lineTo(x, y); + this.closePath(); + }; + + /** + * Add a bezier command + */ + Path2D.prototype.bezierCurveTo = function (cp1x, cp1y, cp2x, cp2y, x, y) { + this.__currentPosition = {x: x, y: y}; + this.addPath(format("C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}", + { + cp1x: this.__matrixTransform(cp1x, cp1y).x, + cp1y: this.__matrixTransform(cp1x, cp1y).y, + cp2x: this.__matrixTransform(cp2x, cp2y).x, + cp2y: this.__matrixTransform(cp2x, cp2y).y, + x: this.__matrixTransform(x, y).x, + y: this.__matrixTransform(x, y).y + })); + }; + + /** + * Adds a quadratic curve to command + */ + Path2D.prototype.quadraticCurveTo = function (cpx, cpy, x, y) { + this.__currentPosition = {x: x, y: y}; + this.addPath(format("Q {cpx} {cpy} {x} {y}", { + cpx: this.__matrixTransform(cpx, cpy).x, + cpy: this.__matrixTransform(cpx, cpy).y, + x: this.__matrixTransform(x, y).x, + y: this.__matrixTransform(x, y).y + })); + }; + + + + /** + * Arc command! + */ + Path2D.prototype.arc = function (x, y, radius, startAngle, endAngle, counterClockwise) { + // in canvas no circle is drawn if no angle is provided. + if (startAngle === endAngle) { + return; + } + startAngle = startAngle % (2*Math.PI); + endAngle = endAngle % (2*Math.PI); + if (startAngle === endAngle) { + //circle time! subtract some of the angle so svg is happy (svg elliptical arc can't draw a full circle) + endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI); + } + var endX = x+radius*Math.cos(endAngle), + endY = y+radius*Math.sin(endAngle), + startX = x+radius*Math.cos(startAngle), + startY = y+radius*Math.sin(startAngle), + sweepFlag = counterClockwise ? 0 : 1, + largeArcFlag = 0, + diff = endAngle - startAngle; + + // https://github.com/gliffy/canvas2svg/issues/4 + if (diff < 0) { + diff += 2*Math.PI; + } + + if (counterClockwise) { + largeArcFlag = diff > Math.PI ? 0 : 1; + } else { + largeArcFlag = diff > Math.PI ? 1 : 0; + } + + var scaleX = Math.hypot(this.ctx.__transformMatrix.a, this.ctx.__transformMatrix.b); + var scaleY = Math.hypot(this.ctx.__transformMatrix.c, this.ctx.__transformMatrix.d); + + this.lineTo(startX, startY); + this.addPath(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", + { + rx:radius * scaleX, + ry:radius * scaleY, + xAxisRotation:0, + largeArcFlag:largeArcFlag, + sweepFlag:sweepFlag, + endX: this.__matrixTransform(endX, endY).x, + endY: this.__matrixTransform(endX, endY).y + })); + + this.__currentPosition = {x: endX, y: endY}; + }; + + + /** + * Return a new normalized vector of given vector + */ + var normalize = function (vector) { + var len = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]); + return [vector[0] / len, vector[1] / len]; + }; + + /** + * Adds the arcTo to the current path + * + * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto + */ + Path2D.prototype.arcTo = function (x1, y1, x2, y2, radius) { + // Let the point (x0, y0) be the last point in the subpath. + var x0 = this.__currentPosition && this.__currentPosition.x; + var y0 = this.__currentPosition && this.__currentPosition.y; + + // First ensure there is a subpath for (x1, y1). + if (typeof x0 == "undefined" || typeof y0 == "undefined") { + return; + } + + // Negative values for radius must cause the implementation to throw an IndexSizeError exception. + if (radius < 0) { + throw new Error("IndexSizeError: The radius provided (" + radius + ") is negative."); + } + + // If the point (x0, y0) is equal to the point (x1, y1), + // or if the point (x1, y1) is equal to the point (x2, y2), + // or if the radius radius is zero, + // then the method must add the point (x1, y1) to the subpath, + // and connect that point to the previous point (x0, y0) by a straight line. + if (((x0 === x1) && (y0 === y1)) + || ((x1 === x2) && (y1 === y2)) + || (radius === 0)) { + this.lineTo(x1, y1); + return; + } + + // Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line, + // then the method must add the point (x1, y1) to the subpath, + // and connect that point to the previous point (x0, y0) by a straight line. + var unit_vec_p1_p0 = normalize([x0 - x1, y0 - y1]); + var unit_vec_p1_p2 = normalize([x2 - x1, y2 - y1]); + if (unit_vec_p1_p0[0] * unit_vec_p1_p2[1] === unit_vec_p1_p0[1] * unit_vec_p1_p2[0]) { + this.lineTo(x1, y1); + return; + } + + // Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius, + // and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1), + // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1), and crosses the point (x2, y2). + // The points at which this circle touches these two lines are called the start and end tangent points respectively. + + // note that both vectors are unit vectors, so the length is 1 + var cos = (unit_vec_p1_p0[0] * unit_vec_p1_p2[0] + unit_vec_p1_p0[1] * unit_vec_p1_p2[1]); + var theta = Math.acos(Math.abs(cos)); + + // Calculate origin + var unit_vec_p1_origin = normalize([ + unit_vec_p1_p0[0] + unit_vec_p1_p2[0], + unit_vec_p1_p0[1] + unit_vec_p1_p2[1] + ]); + var len_p1_origin = radius / Math.sin(theta / 2); + var x = x1 + len_p1_origin * unit_vec_p1_origin[0]; + var y = y1 + len_p1_origin * unit_vec_p1_origin[1]; + + // Calculate start angle and end angle + // rotate 90deg clockwise (note that y axis points to its down) + var unit_vec_origin_start_tangent = [ + -unit_vec_p1_p0[1], + unit_vec_p1_p0[0] + ]; + // rotate 90deg counter clockwise (note that y axis points to its down) + var unit_vec_origin_end_tangent = [ + unit_vec_p1_p2[1], + -unit_vec_p1_p2[0] + ]; + var getAngle = function (vector) { + // get angle (clockwise) between vector and (1, 0) + var x = vector[0]; + var y = vector[1]; + if (y >= 0) { // note that y axis points to its down + return Math.acos(x); + } else { + return -Math.acos(x); + } + }; + var startAngle = getAngle(unit_vec_origin_start_tangent); + var endAngle = getAngle(unit_vec_origin_end_tangent); + + // Connect the point (x0, y0) to the start tangent point by a straight line + this.lineTo(x + unit_vec_origin_start_tangent[0] * radius, + y + unit_vec_origin_start_tangent[1] * radius); + + // Connect the start tangent point to the end tangent point by arc + // and adding the end tangent point to the subpath. + this.arc(x, y, radius, startAngle, endAngle); + }; + + + /** + * Ellipse command! + */ + Path2D.prototype.ellipse = function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterClockwise) { + if (startAngle === endAngle) { + return; + } + + var transformedCenter = this.__matrixTransform(x, y); + x = transformedCenter.x; + y = transformedCenter.y; + var scale = this.ctx.__getTransformScale(); + radiusX = radiusX * scale.x; + radiusY = radiusY * scale.y; + rotation = rotation + this.ctx.__getTransformRotation() + + startAngle = startAngle % (2*Math.PI); + endAngle = endAngle % (2*Math.PI); + if(startAngle === endAngle) { + endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI); + } + var endX = x + Math.cos(-rotation) * radiusX * Math.cos(endAngle) + + Math.sin(-rotation) * radiusY * Math.sin(endAngle), + endY = y - Math.sin(-rotation) * radiusX * Math.cos(endAngle) + + Math.cos(-rotation) * radiusY * Math.sin(endAngle), + startX = x + Math.cos(-rotation) * radiusX * Math.cos(startAngle) + + Math.sin(-rotation) * radiusY * Math.sin(startAngle), + startY = y - Math.sin(-rotation) * radiusX * Math.cos(startAngle) + + Math.cos(-rotation) * radiusY * Math.sin(startAngle), + sweepFlag = counterClockwise ? 0 : 1, + largeArcFlag = 0, + diff = endAngle - startAngle; + + if(diff < 0) { + diff += 2*Math.PI; + } + + if(counterClockwise) { + largeArcFlag = diff > Math.PI ? 0 : 1; + } else { + largeArcFlag = diff > Math.PI ? 1 : 0; + } + + // Transform is already applied, so temporarily remove since lineTo + // will apply it again. + var currentTransform = this.ctx.__transformMatrix; + this.ctx.resetTransform(); + this.lineTo(startX, startY); + this.ctx.__transformMatrix = currentTransform; + + this.addPath(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}", + { + rx:radiusX, + ry:radiusY, + xAxisRotation:rotation*(180/Math.PI), + largeArcFlag:largeArcFlag, + sweepFlag:sweepFlag, + endX:endX, + endY:endY + })); + + this.__currentPosition = {x: endX, y: endY}; + }; + + return Path2D; +}()); diff --git a/test/index.js b/test/index.js index a00bf38..db8695f 100644 --- a/test/index.js +++ b/test/index.js @@ -20,6 +20,7 @@ import text from './tests/text' import tiger from './tests/tiger' import transform from './tests/transform' import pattern from "./tests/pattern"; +import path2D from './tests/path2D'; const tests = [ tiger, @@ -42,7 +43,8 @@ const tests = [ setLineDash, text, transform, - pattern + pattern, + path2D ]; for (let fn of tests) { diff --git a/test/rendering.test.js b/test/rendering.test.js index 4d7e0ef..e1e83b3 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -21,6 +21,7 @@ import text from './tests/text' import tiger from './tests/tiger' import transform from './tests/transform' import pattern from "./tests/pattern"; +import path2D from './tests/path2D'; const tests = { tiger, @@ -43,7 +44,8 @@ const tests = { setLineDash, text, transform, - pattern + pattern, + path2D }; const config = { diff --git a/test/tests/path2D.js b/test/tests/path2D.js new file mode 100644 index 0000000..c93a1c5 --- /dev/null +++ b/test/tests/path2D.js @@ -0,0 +1,18 @@ +function makePath(ctx, arg) { + if (ctx.createPath) { + return ctx.createPath(arg); + } else { + return new Path2D(arg); + } +} + +export default function path2D(ctx) { + const path1 = makePath(ctx, `M 230 80 + A 45 45, 0, 1, 0, 275 125 + L 275 80 Z`); + + ctx.strokeStyle = 'red'; + ctx.stroke(path1); + ctx.fillStyle = 'grey'; + ctx.fill(path1); +}; \ No newline at end of file diff --git a/utils.js b/utils.js index 4384a5c..6363b09 100644 --- a/utils.js +++ b/utils.js @@ -16,4 +16,14 @@ function debug(...data) { } } -export {toString, debug}; \ No newline at end of file + +//helper function to format a string +function format(str, args) { + var keys = Object.keys(args), i; + for (i=0; i