diff --git a/README.md b/README.md index b26448c..f73456c 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,19 @@ const mySerializedSVG = ctx.getSerializedSvg(); https://zenozeng.github.io/p5.js-svg/test/ +To run the testsuite: + +``` +npm run test +``` + +To debug tests in a browser: + +``` +open test/index.html +npm run watch +``` + ## License This library is licensed under the MIT license. diff --git a/package-lock.json b/package-lock.json index 4a43335..23ede9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aha-app/svgcanvas", - "version": "2.5.0-a11", + "version": "2.5.0-a12", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7d44f05..a71d22b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aha-app/svgcanvas", - "version": "2.5.0-a12", + "version": "2.5.0-a13", "description": "svgcanvas", "main": "dist/svgcanvas.js", "scripts": { diff --git a/path2d.js b/path2d.js index 7885202..f09bb19 100644 --- a/path2d.js +++ b/path2d.js @@ -201,7 +201,8 @@ export default (function () { }; /** - * Adds the arcTo to the current path + * Adds the arcTo to the current path. Based on Webkit implementation from + * https://github.com/WebKit/webkit/blob/main/Source/WebCore/platform/graphics/cairo/PathCairo.cpp * * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto */ @@ -232,67 +233,66 @@ export default (function () { 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] - ) { + const p1p0 = [x0 - x1, y0 - y1]; + const p1p2 = [x2 - x1, y2 - y1]; + const p1p0_length = Math.hypot(p1p0[0], p1p0[1]); + const p1p2_length = Math.hypot(p1p2[0], p1p2[1]); + const cos_phi = (p1p0[0] * p1p2[0] + p1p0[1] * p1p2[1]) / (p1p0_length * p1p2_length); + // all points on a line logic + if (cos_phi == -1) { this.lineTo(x1, y1); return; } + if (cos_phi == 1) { + // add infinite far away point + const max_length = 65535; + const factor_max = max_length / p1p0_length; + const ep = [xp0 + factor_max * p1p0[0], y0 + factor_max * p1p0[1]]; + this.lineTo(ep[0], ep[1]); + 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. + const tangent = radius / Math.tan(Math.acos(cos_phi) / 2); + const factor_p1p0 = tangent / p1p0_length; + const t_p1p0 = [x1 + factor_p1p0 * p1p0[0], y1 + factor_p1p0 * p1p0[1]]; - // 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)); + let orth_p1p0 = [p1p0[1], -p1p0[0]]; + const orth_p1p0_length = Math.hypot(orth_p1p0[0], orth_p1p0[1]); + const factor_ra = radius / orth_p1p0_length; - // 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]; + // angle between orth_p1p0 and p1p2 to get the right vector orthographic to p1p0 + const cos_alpha = (orth_p1p0[0] * p1p2[0] + orth_p1p0[1] * p1p2[1]) / (orth_p1p0_length * p1p2_length); + if (cos_alpha < 0) { + orth_p1p0 = [-orth_p1p0[0], -orth_p1p0[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); + const p = [t_p1p0[0] + factor_ra * orth_p1p0[0], t_p1p0[1] + factor_ra * orth_p1p0[1]]; - // 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 - ); + // calculate angles for addArc + orth_p1p0 = [-orth_p1p0[0], -orth_p1p0[1]]; + let sa = Math.acos(orth_p1p0[0] / orth_p1p0_length); + if (orth_p1p0[1] < 0) { + sa = 2 * Math.PI - sa; + } - // 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); + // anticlockwise logic + let anticlockwise = false; + + const factor_p1p2 = tangent / p1p2_length; + const t_p1p2 = [x1 + factor_p1p2 * p1p2[0], y1 + factor_p1p2 * p1p2[1]]; + const orth_p1p2 = [t_p1p2[0] - p[0], t_p1p2[1] - p[1]]; + const orth_p1p2_length = Math.hypot(orth_p1p2[0], orth_p1p2[1]); + let ea = Math.acos(orth_p1p2[0] / orth_p1p2_length); + if (orth_p1p2[1] < 0) { + ea = 2 * Math.PI - ea; + } + if (sa > ea && sa - ea < Math.PI) + anticlockwise = true; + if (sa < ea && ea - sa > Math.PI) + anticlockwise = true; + + this.lineTo(t_p1p0[0], t_p1p0[1]) + this.arc(p[0], p[1], radius, sa, ea, anticlockwise) }; /** diff --git a/test/tests/arcTo.js b/test/tests/arcTo.js index 7f06e61..6ad0b8b 100644 --- a/test/tests/arcTo.js +++ b/test/tests/arcTo.js @@ -1,16 +1,30 @@ export default function arcTo(ctx) { ctx.beginPath(); ctx.moveTo(150, 20); - ctx.arcTo(150, 100, 50, 20, 30); + ctx.arcTo(150, 100, 250, 20, 20); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(450, 100, 20, 180/180*Math.PI, 45/180*Math.PI, true); ctx.stroke(); ctx.fillStyle = 'blue'; // base point - ctx.fillRect(150, 20, 10, 10); + ctx.fillRect(150, 20, 2, 2); ctx.fillStyle = 'red'; // control point one - ctx.fillRect(150, 100, 10, 10); + ctx.fillRect(150, 100, 2, 2); // control point two - ctx.fillRect(50, 20, 10, 10); + ctx.fillRect(250, 20, 2, 2); + + ctx.beginPath(); + ctx.moveTo(150, 200); + ctx.arcTo(250, 200, 250, 250, 20); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(150, 400); + ctx.arcTo(50, 400, 20, 450, 20); + ctx.stroke(); }; \ No newline at end of file diff --git a/test/tests/arcTo2.js b/test/tests/arcTo2.js index 28765eb..7fb9321 100644 --- a/test/tests/arcTo2.js +++ b/test/tests/arcTo2.js @@ -4,4 +4,17 @@ export default function arcTo(ctx) { ctx.arcTo(300, 25, 500, 225, 75); // P1, P2 and the radius ctx.lineTo(500, 225); // P2 ctx.stroke(); + + + const path = [[50, 50], [50, 150], [100, 150], [100, 150], [200, 150], [200, 50], [300, 50], [300, 150]]; + ctx.beginPath(); + let fromPoint = path[0]; + ctx.moveTo(fromPoint[0], fromPoint[1]); + for (let i = 1; i < path.length; i++) { + const point = path[i]; + ctx.arcTo(fromPoint[0], fromPoint[1], point[0], point[1], 20); // P1, P2 and the radius + fromPoint = point; + } + ctx.lineTo(300, 100) + ctx.stroke(); }; \ No newline at end of file