diff --git a/src/path.js b/src/path.js index 28f1f66..b0225a1 100644 --- a/src/path.js +++ b/src/path.js @@ -81,17 +81,19 @@ Path.prototype = path.prototype = { this._ += "A" + r + "," + r + ",0,0," + (+(y01 * x20 > x01 * y20)) + "," + (this._x1 = x1 + t21 * x21) + "," + (this._y1 = y1 + t21 * y21); } }, - arc: function(x, y, r, a0, a1, ccw) { - x = +x, y = +y, r = +r, ccw = !!ccw; - var dx = r * Math.cos(a0), - dy = r * Math.sin(a0), + ellipse: function(x, y, rx, ry, rotation, a0, a1, ccw) { + x = +x, y = +y, rx = +rx, ry = +ry, ccw = !!ccw; + var dx = rx * Math.cos(a0) * Math.cos(rotation) - ry * Math.sin(a0) * Math.sin(rotation), + dy = ry * Math.sin(a0) * Math.cos(rotation) + rx * Math.cos(a0) * Math.sin(rotation), x0 = x + dx, y0 = y + dy, cw = 1 ^ ccw, - da = ccw ? a0 - a1 : a1 - a0; + da = ccw ? a0 - a1 : a1 - a0, + rotationDegrees = rotation / tau * 360; - // Is the radius negative? Error. - if (r < 0) throw new Error("negative radius: " + r); + // Is a radius negative? Error. + if (rx < 0) throw new Error("negative x radius: " + rx); + if (ry < 0) throw new Error("negative y radius: " + ry); // Is this path empty? Move to (x0,y0). if (this._x1 === null) { @@ -104,21 +106,29 @@ Path.prototype = path.prototype = { } // Is this arc empty? We’re done. - if (!r) return; + if (!rx || !ry) return; // Does the angle go the wrong way? Flip the direction. if (da < 0) da = da % tau + tau; - // Is this a complete circle? Draw two arcs to complete the circle. + // Is this a complete ellipse? Draw two arcs to complete the ellipse. if (da > tauEpsilon) { - this._ += "A" + r + "," + r + ",0,1," + cw + "," + (x - dx) + "," + (y - dy) + "A" + r + "," + r + ",0,1," + cw + "," + (this._x1 = x0) + "," + (this._y1 = y0); + this._ += "A" + rx + "," + ry + "," + rotationDegrees + ",1," + cw + "," + (x - dx) + "," + (y - dy) + "A" + rx + "," + ry + "," + rotationDegrees + ",1," + cw + "," + (this._x1 = x0) + "," + (this._y1 = y0); } // Is this arc non-empty? Draw an arc! else if (da > epsilon) { - this._ += "A" + r + "," + r + ",0," + (+(da >= pi)) + "," + cw + "," + (this._x1 = x + r * Math.cos(a1)) + "," + (this._y1 = y + r * Math.sin(a1)); + this._ += "A" + rx + "," + ry + "," + rotationDegrees + "," + (+(da >= pi)) + "," + cw + "," + (this._x1 = x + rx * Math.cos(a1) * Math.cos(rotation) - ry * Math.sin(a1) * Math.sin(rotation)) + "," + (this._y1 = y + ry * Math.sin(a1) * Math.cos(rotation) + rx * Math.cos(a1) * Math.sin(rotation)); } }, + arc: function(x, y, r, a0, a1, ccw) { + // Is the radius negative? Error. + if (r < 0) throw new Error("negative radius: " + r); + + // arc() is equivalent to ellipse() except that both radii are equal and rotation is 0. + // see: https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-arc + this.ellipse(x, y, r, r, 0, a0, a1, ccw); + }, rect: function(x, y, w, h) { this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y) + "h" + (+w) + "v" + (+h) + "h" + (-w) + "Z"; }, diff --git a/test/path-test.js b/test/path-test.js index 404b83d..ad24be7 100644 --- a/test/path-test.js +++ b/test/path-test.js @@ -347,3 +347,54 @@ tape("path.rect(x, y, w, h) appends M, h, v, h, and Z commands", function(test) test.pathEqual(p, "M150,100M100,200h50v25h-50Z"); test.end(); }); + +tape("path.ellipse(x, y, rx, ry, rotation, startAngle, endAngle) throws an error if rx is negative, counterclockwise", function(test) { + var p = path.path(); p.moveTo(150, 100); + test.throws(function() { p.ellipse(100, 100, -50, 50, 0, 0, Math.PI / 2); }, /negative x radius/); + test.end(); +}); + +tape("path.ellipse(x, y, rx, ry, rotation, startAngle, endAngle) throws an error if ry is negative, counterclockwise", function(test) { + var p = path.path(); p.moveTo(150, 100); + test.throws(function() { p.ellipse(100, 100, 50, -50, 0, 0, Math.PI / 2); }, /negative y radius/); + test.end(); +}); + +tape("path.ellipse(x, y, rx, ry, π/2, 0, π/2, falsey) draws the bottom half of an ellipse, rotated by 90 degrees", function(test) { + var p = path.path(); p.moveTo(150, 100); + p.ellipse(100, 100, 50, 75, Math.PI/2, 0, Math.PI, false); + test.pathEqual(p, "M150,100L100,150A50,75,90,1,1,100,50"); + test.end(); +}); + +tape("path.ellipse(x, y, rx, ry, π/2, 0, 3π/2, falsey) draws a large arc of an ellipse rotated by 90 degrees", function(test) { + var p = path.path(); p.moveTo(150, 100); + p.ellipse(100, 100, 50, 75, Math.PI/2, 0, 3*Math.PI/2, false); + test.pathEqual(p, "M150,100L100,150A50,75,90,1,1,175,100"); + test.end(); +}); + +tape("path.ellipse(x, y, rx, ry, π/2, 0, π/2, truey) draws the bottom half of a ccw ellipse, rotated by 90 degrees", function(test) { + var p = path.path(); p.moveTo(150, 100); + p.ellipse(100, 100, 50, 75, Math.PI/2, 0, Math.PI, true); + test.pathEqual(p, "M150,100L100,150A50,75,90,1,0,100,50"); + test.end(); +}); + +tape("path.ellipse(x, y, rx, ry, π/2, 0, 3π/2, truey) draws a large arc of a ccw ellipse rotated by 90 degrees", function(test) { + var p = path.path(); p.moveTo(150, 100); + p.ellipse(100, 100, 50, 75, Math.PI/2, 0, 3*Math.PI/2, true); + test.pathEqual(p, "M150,100L100,150A50,75,90,0,0,175,100"); + test.end(); +}); + +tape("draws a sequence of straight lines and elliptical arcs, forming an S shape", function(test) { + var p = path.path(); + p.moveTo(10, 25); + p.lineTo(50, 25); + p.ellipse(150, 100, 75, 50, Math.PI/2, Math.PI, Math.PI/2, true); + p.ellipse(50, 100, 75, 50, Math.PI/2, -Math.PI/2, 0, false); + p.lineTo(190, 175); + test.pathEqual(p, "M10,25L50,25L150,25A75,50,90,0,0,100,100A75,50,90,0,1,50,175L190,175"); + test.end(); +}); \ No newline at end of file