From 7c5c19544f4e2befaa1111c4c09f608f788eb87b Mon Sep 17 00:00:00 2001 From: nitaku Date: Thu, 6 May 2021 14:38:46 +0200 Subject: [PATCH 1/3] ellipse() method, with arc() as a special case --- src/path.js | 29 +++++++++++++++++++---------- test/path-test.js | 12 ++++++++++++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/path.js b/src/path.js index 28f1f66..d38105e 100644 --- a/src/path.js +++ b/src/path.js @@ -81,17 +81,18 @@ 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), + dy = ry * Math.sin(a0), x0 = x + dx, y0 = y + dy, cw = 1 ^ ccw, da = ccw ? a0 - a1 : a1 - a0; - // 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 +105,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 + ",0,1," + cw + "," + (x - dx) + "," + (y - dy) + "A" + rx + "," + ry + ",0,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 + ",0," + (+(da >= pi)) + "," + cw + "," + (this._x1 = x + rx * Math.cos(a1)) + "," + (this._y1 = y + ry * Math.sin(a1)); } }, + 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..a0364fc 100644 --- a/test/path-test.js +++ b/test/path-test.js @@ -347,3 +347,15 @@ 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", 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", 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(); +}); \ No newline at end of file From 2735101df5e17f144ff056ee7576bf1146bea8d1 Mon Sep 17 00:00:00 2001 From: nitaku Date: Thu, 6 May 2021 14:41:47 +0200 Subject: [PATCH 2/3] rotation support --- src/path.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/path.js b/src/path.js index d38105e..b0225a1 100644 --- a/src/path.js +++ b/src/path.js @@ -83,12 +83,13 @@ Path.prototype = path.prototype = { }, 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), - dy = ry * Math.sin(a0), + 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 a radius negative? Error. if (rx < 0) throw new Error("negative x radius: " + rx); @@ -112,12 +113,12 @@ Path.prototype = path.prototype = { // Is this a complete ellipse? Draw two arcs to complete the ellipse. if (da > tauEpsilon) { - this._ += "A" + rx + "," + ry + ",0,1," + cw + "," + (x - dx) + "," + (y - dy) + "A" + rx + "," + ry + ",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" + rx + "," + ry + ",0," + (+(da >= pi)) + "," + cw + "," + (this._x1 = x + rx * Math.cos(a1)) + "," + (this._y1 = y + ry * 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) { From b4495434fb02b7450544e501d0d4a9b4d916ac03 Mon Sep 17 00:00:00 2001 From: kleem Date: Tue, 11 May 2021 13:35:16 +0200 Subject: [PATCH 3/3] more tests added --- test/path-test.js | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/test/path-test.js b/test/path-test.js index a0364fc..ad24be7 100644 --- a/test/path-test.js +++ b/test/path-test.js @@ -348,14 +348,53 @@ tape("path.rect(x, y, w, h) appends M, h, v, h, and Z commands", function(test) test.end(); }); -tape("path.ellipse(x, y, rx, ry, rotation, startAngle, endAngle) throws an error if rx is negative", function(test) { +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", function(test) { +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