Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions src/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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";
},
Expand Down
51 changes: 51 additions & 0 deletions test/path-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});