diff --git a/demo/tree-shake/index.L.ts b/demo/tree-shake/index.L.ts new file mode 100644 index 0000000000..39be43b526 --- /dev/null +++ b/demo/tree-shake/index.L.ts @@ -0,0 +1,6 @@ +import * as joint from '../../'; + +console.log('Point', new joint.g.Point()); +console.log('PolyLine', new joint.g.Polyline('10,10 20,20 30,30').bbox()); +console.log('Path', new joint.g.Path('M 0 -5 L -10 0 L 0 5 Z').bbox()); +console.log(new joint.dia.Graph()); diff --git a/demo/tree-shake/index.M.ts b/demo/tree-shake/index.M.ts new file mode 100644 index 0000000000..2df55155fc --- /dev/null +++ b/demo/tree-shake/index.M.ts @@ -0,0 +1,7 @@ +import * as joint from '../../'; + +console.log('Point', new joint.g.Point()); +console.log('PolyLine', new joint.g.Polyline('10,10 20,20 30,30').bbox()); +console.log('Path', new joint.g.Path('M 0 -5 L -10 0 L 0 5 Z').bbox()); + + diff --git a/demo/tree-shake/index.S.ts b/demo/tree-shake/index.S.ts new file mode 100644 index 0000000000..0d2a586ab8 --- /dev/null +++ b/demo/tree-shake/index.S.ts @@ -0,0 +1,5 @@ +import * as joint from '../../'; + +console.log('Point', new joint.g.Point()); + + diff --git a/demo/tree-shake/index.html b/demo/tree-shake/index.html new file mode 100644 index 0000000000..47be4a8615 --- /dev/null +++ b/demo/tree-shake/index.html @@ -0,0 +1,15 @@ + + + + + Rappid Visio Cross-Functional FlowChart Import + + + + +
+ + + + + diff --git a/demo/tree-shake/package.json b/demo/tree-shake/package.json new file mode 100644 index 0000000000..2577a709c0 --- /dev/null +++ b/demo/tree-shake/package.json @@ -0,0 +1,14 @@ +{ + "scripts": { + "start": "npm run analyze-S && npm run analyze-M && npm run analyze-L", + "analyze-S": "webpack --entry=./index.S.ts -o ./dist/S --profile --json > stats.S.json && webpack-bundle-analyzer ./stats.S.json ./dist/S --mode static --report stats.S.html --no-open --title=Small Bundle", + "analyze-M": "webpack --entry=./index.M.ts -o ./dist/M --profile --json > stats.M.json && webpack-bundle-analyzer ./stats.M.json ./dist/M --mode static --report stats.M.html --no-open --title=Medium Bundle", + "analyze-L": "webpack --entry=./index.L.ts -o ./dist/L --profile --json > stats.L.json && webpack-bundle-analyzer ./stats.L.json ./dist/L --mode static --report stats.L.html --no-open --title=Large Bundle" + }, + "devDependencies": { + "ts-loader": "^8.0.17", + "webpack": "^5.32.0", + "webpack-bundle-analyzer": "^4.4.1", + "webpack-cli": "^4.6.0" + } +} diff --git a/demo/tree-shake/tsconfig.json b/demo/tree-shake/tsconfig.json new file mode 100644 index 0000000000..81e4be5505 --- /dev/null +++ b/demo/tree-shake/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "ES5", + "moduleResolution": "node", + "lib": [ + "dom" + ], + "noImplicitAny": true, + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "strictNullChecks": false, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": true, + "skipLibCheck": true + }, + "compileOnSave": false +} diff --git a/demo/tree-shake/webpack.config.js b/demo/tree-shake/webpack.config.js new file mode 100644 index 0000000000..b75f048b9c --- /dev/null +++ b/demo/tree-shake/webpack.config.js @@ -0,0 +1,24 @@ +const path = process.cwd() + '/dist'; + +module.exports = { + entry: './index.S.ts', + mode: 'development', + output: { + path, + filename: 'bundle.js' + }, + resolve: { + extensions: ['.ts', '.js'], + alias: { + 'underscore': 'lodash' + } + }, + module: { + rules: [ + { + test: /\.ts$/, + use: [{ loader: 'ts-loader', options: { allowTsInNodeModules: true }}] + } + ] + } +}; diff --git a/package.json b/package.json index 595f00e0eb..d490f32fcf 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "scripts": { "test": "grunt test" }, + "sideEffects": false, "version": "3.3.1", "main": "dist/joint.min.js", "module": "joint.mjs", diff --git a/src/g/bezier.mjs b/src/g/bezier.mjs new file mode 100644 index 0000000000..fbe3ecfca9 --- /dev/null +++ b/src/g/bezier.mjs @@ -0,0 +1,175 @@ +import { Path } from './path.mjs'; +import { Curve } from './curve.mjs'; +import { Point } from './point.mjs'; + +export const bezier = { + + // Cubic Bezier curve path through points. + // @deprecated + // @param {array} points Array of points through which the smooth line will go. + // @return {array} SVG Path commands as an array + curveThroughPoints: function(points) { + + console.warn('deprecated'); + + return new Path(Curve.throughPoints(points)).serialize(); + }, + + // Get open-ended Bezier Spline Control Points. + // @deprecated + // @param knots Input Knot Bezier spline points (At least two points!). + // @param firstControlPoints Output First Control points. Array of knots.length - 1 length. + // @param secondControlPoints Output Second Control points. Array of knots.length - 1 length. + getCurveControlPoints: function(knots) { + + console.warn('deprecated'); + + var firstControlPoints = []; + var secondControlPoints = []; + var n = knots.length - 1; + var i; + + // Special case: Bezier curve should be a straight line. + if (n == 1) { + // 3P1 = 2P0 + P3 + firstControlPoints[0] = new Point( + (2 * knots[0].x + knots[1].x) / 3, + (2 * knots[0].y + knots[1].y) / 3 + ); + + // P2 = 2P1 – P0 + secondControlPoints[0] = new Point( + 2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y + ); + + return [firstControlPoints, secondControlPoints]; + } + + // Calculate first Bezier control points. + // Right hand side vector. + var rhs = []; + + // Set right hand side X values. + for (i = 1; i < n - 1; i++) { + rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x; + } + + rhs[0] = knots[0].x + 2 * knots[1].x; + rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; + + // Get first control points X-values. + var x = this.getFirstControlPoints(rhs); + + // Set right hand side Y values. + for (i = 1; i < n - 1; ++i) { + rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; + } + + rhs[0] = knots[0].y + 2 * knots[1].y; + rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; + + // Get first control points Y-values. + var y = this.getFirstControlPoints(rhs); + + // Fill output arrays. + for (i = 0; i < n; i++) { + // First control point. + firstControlPoints.push(new Point(x[i], y[i])); + + // Second control point. + if (i < n - 1) { + secondControlPoints.push(new Point( + 2 * knots [i + 1].x - x[i + 1], + 2 * knots[i + 1].y - y[i + 1] + )); + + } else { + secondControlPoints.push(new Point( + (knots[n].x + x[n - 1]) / 2, + (knots[n].y + y[n - 1]) / 2) + ); + } + } + + return [firstControlPoints, secondControlPoints]; + }, + + // Divide a Bezier curve into two at point defined by value 't' <0,1>. + // Using deCasteljau algorithm. http://math.stackexchange.com/a/317867 + // @deprecated + // @param control points (start, control start, control end, end) + // @return a function that accepts t and returns 2 curves. + getCurveDivider: function(p0, p1, p2, p3) { + + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + + return function divideCurve(t) { + + var divided = curve.divide(t); + + return [{ + p0: divided[0].start, + p1: divided[0].controlPoint1, + p2: divided[0].controlPoint2, + p3: divided[0].end + }, { + p0: divided[1].start, + p1: divided[1].controlPoint1, + p2: divided[1].controlPoint2, + p3: divided[1].end + }]; + }; + }, + + // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. + // @deprecated + // @param rhs Right hand side vector. + // @return Solution vector. + getFirstControlPoints: function(rhs) { + + console.warn('deprecated'); + + var n = rhs.length; + // `x` is a solution vector. + var x = []; + var tmp = []; + var b = 2.0; + + x[0] = rhs[0] / b; + + // Decomposition and forward substitution. + for (var i = 1; i < n; i++) { + tmp[i] = 1 / b; + b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]; + x[i] = (rhs[i] - x[i - 1]) / b; + } + + for (i = 1; i < n; i++) { + // Backsubstitution. + x[n - i - 1] -= tmp[n - i] * x[n - i]; + } + + return x; + }, + + // Solves an inversion problem -- Given the (x, y) coordinates of a point which lies on + // a parametric curve x = x(t)/w(t), y = y(t)/w(t), find the parameter value t + // which corresponds to that point. + // @deprecated + // @param control points (start, control start, control end, end) + // @return a function that accepts a point and returns t. + getInversionSolver: function(p0, p1, p2, p3) { + + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + + return function solveInversion(p) { + + return curve.closestPointT(p); + }; + } +}; diff --git a/src/g/curve.mjs b/src/g/curve.mjs new file mode 100644 index 0000000000..e7e54b5dc9 --- /dev/null +++ b/src/g/curve.mjs @@ -0,0 +1,927 @@ +import { Point } from './point.mjs'; +import { Rect } from './rect.mjs'; +import { Line } from './line.mjs'; +import { Polyline } from './polyline.mjs'; + +const math = Math; +const abs = math.abs; +const sqrt = math.sqrt; +const min = math.min; +const max = math.max; +const pow = math.pow; + +export const Curve = function(p1, p2, p3, p4) { + + if (!(this instanceof Curve)) { + return new Curve(p1, p2, p3, p4); + } + + if (p1 instanceof Curve) { + return new Curve(p1.start, p1.controlPoint1, p1.controlPoint2, p1.end); + } + + this.start = new Point(p1); + this.controlPoint1 = new Point(p2); + this.controlPoint2 = new Point(p3); + this.end = new Point(p4); +}; + +// Curve passing through points. +// Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx). +// @param {array} points Array of points through which the smooth line will go. +// @return {array} curves. +Curve.throughPoints = (function() { + + // Get open-ended Bezier Spline Control Points. + // @param knots Input Knot Bezier spline points (At least two points!). + // @param firstControlPoints Output First Control points. Array of knots.length - 1 length. + // @param secondControlPoints Output Second Control points. Array of knots.length - 1 length. + function getCurveControlPoints(knots) { + + var firstControlPoints = []; + var secondControlPoints = []; + var n = knots.length - 1; + var i; + + // Special case: Bezier curve should be a straight line. + if (n == 1) { + // 3P1 = 2P0 + P3 + firstControlPoints[0] = new Point( + (2 * knots[0].x + knots[1].x) / 3, + (2 * knots[0].y + knots[1].y) / 3 + ); + + // P2 = 2P1 – P0 + secondControlPoints[0] = new Point( + 2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y + ); + + return [firstControlPoints, secondControlPoints]; + } + + // Calculate first Bezier control points. + // Right hand side vector. + var rhs = []; + + // Set right hand side X values. + for (i = 1; i < n - 1; i++) { + rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x; + } + + rhs[0] = knots[0].x + 2 * knots[1].x; + rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; + + // Get first control points X-values. + var x = getFirstControlPoints(rhs); + + // Set right hand side Y values. + for (i = 1; i < n - 1; ++i) { + rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; + } + + rhs[0] = knots[0].y + 2 * knots[1].y; + rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; + + // Get first control points Y-values. + var y = getFirstControlPoints(rhs); + + // Fill output arrays. + for (i = 0; i < n; i++) { + // First control point. + firstControlPoints.push(new Point(x[i], y[i])); + + // Second control point. + if (i < n - 1) { + secondControlPoints.push(new Point( + 2 * knots [i + 1].x - x[i + 1], + 2 * knots[i + 1].y - y[i + 1] + )); + + } else { + secondControlPoints.push(new Point( + (knots[n].x + x[n - 1]) / 2, + (knots[n].y + y[n - 1]) / 2 + )); + } + } + + return [firstControlPoints, secondControlPoints]; + } + + // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. + // @param rhs Right hand side vector. + // @return Solution vector. + function getFirstControlPoints(rhs) { + + var n = rhs.length; + // `x` is a solution vector. + var x = []; + var tmp = []; + var b = 2.0; + + x[0] = rhs[0] / b; + + // Decomposition and forward substitution. + for (var i = 1; i < n; i++) { + tmp[i] = 1 / b; + b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]; + x[i] = (rhs[i] - x[i - 1]) / b; + } + + for (i = 1; i < n; i++) { + // Backsubstitution. + x[n - i - 1] -= tmp[n - i] * x[n - i]; + } + + return x; + } + + return function(points) { + + if (!points || (Array.isArray(points) && points.length < 2)) { + throw new Error('At least 2 points are required'); + } + + var controlPoints = getCurveControlPoints(points); + + var curves = []; + var n = controlPoints[0].length; + for (var i = 0; i < n; i++) { + + var controlPoint1 = new Point(controlPoints[0][i].x, controlPoints[0][i].y); + var controlPoint2 = new Point(controlPoints[1][i].x, controlPoints[1][i].y); + + curves.push(new Curve(points[i], controlPoint1, controlPoint2, points[i + 1])); + } + + return curves; + }; +})(); + +Curve.prototype = { + + // Returns a bbox that tightly envelops the curve. + bbox: function() { + + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; + + var x0 = start.x; + var y0 = start.y; + var x1 = controlPoint1.x; + var y1 = controlPoint1.y; + var x2 = controlPoint2.x; + var y2 = controlPoint2.y; + var x3 = end.x; + var y3 = end.y; + + var points = new Array(); // local extremes + var tvalues = new Array(); // t values of local extremes + var bounds = [new Array(), new Array()]; + + var a, b, c, t; + var t1, t2; + var b2ac, sqrtb2ac; + + for (var i = 0; i < 2; ++i) { + + if (i === 0) { + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; + + } else { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } + + if (abs(a) < 1e-12) { // Numerical robustness + if (abs(b) < 1e-12) { // Numerical robustness + continue; + } + + t = -c / b; + if ((0 < t) && (t < 1)) tvalues.push(t); + + continue; + } + + b2ac = b * b - 4 * c * a; + sqrtb2ac = sqrt(b2ac); + + if (b2ac < 0) continue; + + t1 = (-b + sqrtb2ac) / (2 * a); + if ((0 < t1) && (t1 < 1)) tvalues.push(t1); + + t2 = (-b - sqrtb2ac) / (2 * a); + if ((0 < t2) && (t2 < 1)) tvalues.push(t2); + } + + var j = tvalues.length; + var jlen = j; + var mt; + var x, y; + + while (j--) { + t = tvalues[j]; + mt = 1 - t; + + x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + bounds[0][j] = x; + + y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + bounds[1][j] = y; + + points[j] = { X: x, Y: y }; + } + + tvalues[jlen] = 0; + tvalues[jlen + 1] = 1; + + points[jlen] = { X: x0, Y: y0 }; + points[jlen + 1] = { X: x3, Y: y3 }; + + bounds[0][jlen] = x0; + bounds[1][jlen] = y0; + + bounds[0][jlen + 1] = x3; + bounds[1][jlen + 1] = y3; + + tvalues.length = jlen + 2; + bounds[0].length = jlen + 2; + bounds[1].length = jlen + 2; + points.length = jlen + 2; + + var left = min.apply(null, bounds[0]); + var top = min.apply(null, bounds[1]); + var right = max.apply(null, bounds[0]); + var bottom = max.apply(null, bounds[1]); + + return new Rect(left, top, (right - left), (bottom - top)); + }, + + clone: function() { + + return new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); + }, + + // Returns the point on the curve closest to point `p` + closestPoint: function(p, opt) { + + return this.pointAtT(this.closestPointT(p, opt)); + }, + + closestPointLength: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + return this.lengthAtT(this.closestPointT(p, localOpt), localOpt); + }, + + closestPointNormalizedLength: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + var cpLength = this.closestPointLength(p, localOpt); + if (!cpLength) return 0; + + var length = this.length(localOpt); + if (length === 0) return 0; + + return cpLength / length; + }, + + // Returns `t` of the point on the curve closest to point `p` + closestPointT: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // does not use localOpt + + // identify the subdivision that contains the point: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + var distFromStart; // distance of point from start of baseline + var distFromEnd; // distance of point from end of baseline + var chordLength; // distance between start and end of the subdivision + var minSumDist; // lowest observed sum of the two distances + var n = subdivisions.length; + var subdivisionSize = (n ? (1 / n) : 0); + for (var i = 0; i < n; i++) { + + var currentSubdivision = subdivisions[i]; + + var startDist = currentSubdivision.start.distance(p); + var endDist = currentSubdivision.end.distance(p); + var sumDist = startDist + endDist; + + // check that the point is closest to current subdivision and not any other + if (!minSumDist || (sumDist < minSumDist)) { + investigatedSubdivision = currentSubdivision; + + investigatedSubdivisionStartT = i * subdivisionSize; + investigatedSubdivisionEndT = (i + 1) * subdivisionSize; + + distFromStart = startDist; + distFromEnd = endDist; + + chordLength = currentSubdivision.start.distance(currentSubdivision.end); + + minSumDist = sumDist; + } + } + + var precisionRatio = pow(10, -precision); + + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { + + // check if we have reached at least one required observed precision + // - calculated as: the difference in distances from point to start and end divided by the distance + // - note that this function is not monotonic = it doesn't converge stably but has "teeth" + // - the function decreases while one of the endpoints is fixed but "jumps" whenever we switch + // - this criterion works well for points lying far away from the curve + var startPrecisionRatio = (distFromStart ? (abs(distFromStart - distFromEnd) / distFromStart) : 0); + var endPrecisionRatio = (distFromEnd ? (abs(distFromStart - distFromEnd) / distFromEnd) : 0); + var hasRequiredPrecision = ((startPrecisionRatio < precisionRatio) || (endPrecisionRatio < precisionRatio)); + + // check if we have reached at least one required minimal distance + // - calculated as: the subdivision chord length multiplied by precisionRatio + // - calculation is relative so it will work for arbitrarily large/small curves and their subdivisions + // - this is a backup criterion that works well for points lying "almost at" the curve + var hasMinimalStartDistance = (distFromStart ? (distFromStart < (chordLength * precisionRatio)) : true); + var hasMinimalEndDistance = (distFromEnd ? (distFromEnd < (chordLength * precisionRatio)) : true); + var hasMinimalDistance = (hasMinimalStartDistance || hasMinimalEndDistance); + + // do we stop now? + if (hasRequiredPrecision || hasMinimalDistance) { + return ((distFromStart <= distFromEnd) ? investigatedSubdivisionStartT : investigatedSubdivisionEndT); + } + + // otherwise, set up for next iteration + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; + + var startDist1 = divided[0].start.distance(p); + var endDist1 = divided[0].end.distance(p); + var sumDist1 = startDist1 + endDist1; + + var startDist2 = divided[1].start.distance(p); + var endDist2 = divided[1].end.distance(p); + var sumDist2 = startDist2 + endDist2; + + if (sumDist1 <= sumDist2) { + investigatedSubdivision = divided[0]; + + investigatedSubdivisionEndT -= subdivisionSize; // subdivisionSize was already halved + + distFromStart = startDist1; + distFromEnd = endDist1; + + } else { + investigatedSubdivision = divided[1]; + + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved + + distFromStart = startDist2; + distFromEnd = endDist2; + } + } + }, + + closestPointTangent: function(p, opt) { + + return this.tangentAtT(this.closestPointT(p, opt)); + }, + + // Returns `true` if the area surrounded by the curve contains the point `p`. + // Implements the even-odd algorithm (self-intersections are "outside"). + // Closes open curves (always imagines a closing segment). + // Precision may be adjusted by passing an `opt` object. + containsPoint: function(p, opt) { + + var polyline = this.toPolyline(opt); + return polyline.containsPoint(p); + }, + + // Divides the curve into two at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. + // For a function that uses `t`, use Curve.divideAtT(). + divideAt: function(ratio, opt) { + + if (ratio <= 0) return this.divideAtT(0); + if (ratio >= 1) return this.divideAtT(1); + + var t = this.tAt(ratio, opt); + + return this.divideAtT(t); + }, + + // Divides the curve into two at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + divideAtLength: function(length, opt) { + + var t = this.tAtLength(length, opt); + + return this.divideAtT(t); + }, + + // Divides the curve into two at point defined by `t` between 0 and 1. + // Using de Casteljau's algorithm (http://math.stackexchange.com/a/317867). + // Additional resource: https://pomax.github.io/bezierinfo/#decasteljau + divideAtT: function(t) { + + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; + + // shortcuts for `t` values that are out of range + if (t <= 0) { + return [ + new Curve(start, start, start, start), + new Curve(start, controlPoint1, controlPoint2, end) + ]; + } + + if (t >= 1) { + return [ + new Curve(start, controlPoint1, controlPoint2, end), + new Curve(end, end, end, end) + ]; + } + + var dividerPoints = this.getSkeletonPoints(t); + + var startControl1 = dividerPoints.startControlPoint1; + var startControl2 = dividerPoints.startControlPoint2; + var divider = dividerPoints.divider; + var dividerControl1 = dividerPoints.dividerControlPoint1; + var dividerControl2 = dividerPoints.dividerControlPoint2; + + // return array with two new curves + return [ + new Curve(start, startControl1, startControl2, divider), + new Curve(divider, dividerControl1, dividerControl2, end) + ]; + }, + + // Returns the distance between the curve's start and end points. + endpointDistance: function() { + + return this.start.distance(this.end); + }, + + // Checks whether two curves are exactly the same. + equals: function(c) { + + return !!c && + this.start.x === c.start.x && + this.start.y === c.start.y && + this.controlPoint1.x === c.controlPoint1.x && + this.controlPoint1.y === c.controlPoint1.y && + this.controlPoint2.x === c.controlPoint2.x && + this.controlPoint2.y === c.controlPoint2.y && + this.end.x === c.end.x && + this.end.y === c.end.y; + }, + + // Returns five helper points necessary for curve division. + getSkeletonPoints: function(t) { + + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + // shortcuts for `t` values that are out of range + if (t <= 0) { + return { + startControlPoint1: start.clone(), + startControlPoint2: start.clone(), + divider: start.clone(), + dividerControlPoint1: control1.clone(), + dividerControlPoint2: control2.clone() + }; + } + + if (t >= 1) { + return { + startControlPoint1: control1.clone(), + startControlPoint2: control2.clone(), + divider: end.clone(), + dividerControlPoint1: end.clone(), + dividerControlPoint2: end.clone() + }; + } + + var midpoint1 = (new Line(start, control1)).pointAt(t); + var midpoint2 = (new Line(control1, control2)).pointAt(t); + var midpoint3 = (new Line(control2, end)).pointAt(t); + + var subControl1 = (new Line(midpoint1, midpoint2)).pointAt(t); + var subControl2 = (new Line(midpoint2, midpoint3)).pointAt(t); + + var divider = (new Line(subControl1, subControl2)).pointAt(t); + + var output = { + startControlPoint1: midpoint1, + startControlPoint2: subControl1, + divider: divider, + dividerControlPoint1: subControl2, + dividerControlPoint2: midpoint3 + }; + + return output; + }, + + // Returns a list of curves whose flattened length is better than `opt.precision`. + // That is, observed difference in length between recursions is less than 10^(-3) = 0.001 = 0.1% + // (Observed difference is not real precision, but close enough as long as special cases are covered) + // (That is why skipping iteration 1 is important) + // As a rule of thumb, increasing `precision` by 1 requires two more division operations + // - Precision 0 (endpointDistance) - total of 2^0 - 1 = 0 operations (1 subdivision) + // - Precision 1 (<10% error) - total of 2^2 - 1 = 3 operations (4 subdivisions) + // - Precision 2 (<1% error) - total of 2^4 - 1 = 15 operations requires 4 division operations on all elements (15 operations total) (16 subdivisions) + // - Precision 3 (<0.1% error) - total of 2^6 - 1 = 63 operations - acceptable when drawing (64 subdivisions) + // - Precision 4 (<0.01% error) - total of 2^8 - 1 = 255 operations - high resolution, can be used to interpolate `t` (256 subdivisions) + // (Variation of 1 recursion worse or better is possible depending on the curve, doubling/halving the number of operations accordingly) + getSubdivisions: function(opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt + + var subdivisions = [new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end)]; + if (precision === 0) return subdivisions; + + var previousLength = this.endpointDistance(); + + var precisionRatio = pow(10, -precision); + + // recursively divide curve at `t = 0.5` + // until the difference between observed length at subsequent iterations is lower than precision + var iteration = 0; + while (true) { + iteration += 1; + + // divide all subdivisions + var newSubdivisions = []; + var numSubdivisions = subdivisions.length; + for (var i = 0; i < numSubdivisions; i++) { + + var currentSubdivision = subdivisions[i]; + var divided = currentSubdivision.divide(0.5); // dividing at t = 0.5 (not at middle length!) + newSubdivisions.push(divided[0], divided[1]); + } + + // measure new length + var length = 0; + var numNewSubdivisions = newSubdivisions.length; + for (var j = 0; j < numNewSubdivisions; j++) { + + var currentNewSubdivision = newSubdivisions[j]; + length += currentNewSubdivision.endpointDistance(); + } + + // check if we have reached required observed precision + // sine-like curves may have the same observed length in iteration 0 and 1 - skip iteration 1 + // not a problem for further iterations because cubic curves cannot have more than two local extrema + // (i.e. cubic curves cannot intersect the baseline more than once) + // therefore two subsequent iterations cannot produce sampling with equal length + var observedPrecisionRatio = ((length !== 0) ? ((length - previousLength) / length) : 0); + if (iteration > 1 && observedPrecisionRatio < precisionRatio) { + return newSubdivisions; + } + + // otherwise, set up for next iteration + subdivisions = newSubdivisions; + previousLength = length; + } + }, + + isDifferentiable: function() { + + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); + }, + + // Returns flattened length of the curve with precision better than `opt.precision`; or using `opt.subdivisions` provided. + length: function(opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt + + var length = 0; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { + + var currentSubdivision = subdivisions[i]; + length += currentSubdivision.endpointDistance(); + } + + return length; + }, + + // Returns distance along the curve up to `t` with precision better than requested `opt.precision`. (Not using `opt.subdivisions`.) + lengthAtT: function(t, opt) { + + if (t <= 0) return 0; + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt + + var subCurve = this.divide(t)[0]; + var subCurveLength = subCurve.length({ precision: precision }); + + return subCurveLength; + }, + + // Returns point at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. + // Mirrors Line.pointAt() function. + // For a function that tracks `t`, use Curve.pointAtT(). + pointAt: function(ratio, opt) { + + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); + + var t = this.tAt(ratio, opt); + + return this.pointAtT(t); + }, + + // Returns point at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + pointAtLength: function(length, opt) { + + var t = this.tAtLength(length, opt); + + return this.pointAtT(t); + }, + + // Returns the point at provided `t` between 0 and 1. + // `t` does not track distance along curve as it does in Line objects. + // Non-linear relationship, speeds up and slows down as curve warps! + // For linear length-based solution, use Curve.pointAt(). + pointAtT: function(t) { + + if (t <= 0) return this.start.clone(); + if (t >= 1) return this.end.clone(); + + return this.getSkeletonPoints(t).divider; + }, + + // Default precision + PRECISION: 3, + + round: function(precision) { + + this.start.round(precision); + this.controlPoint1.round(precision); + this.controlPoint2.round(precision); + this.end.round(precision); + return this; + }, + + scale: function(sx, sy, origin) { + + this.start.scale(sx, sy, origin); + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, + + // Returns a tangent line at requested `ratio` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAt: function(ratio, opt) { + + if (!this.isDifferentiable()) return null; + + if (ratio < 0) ratio = 0; + else if (ratio > 1) ratio = 1; + + var t = this.tAt(ratio, opt); + + return this.tangentAtT(t); + }, + + // Returns a tangent line at requested `length` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAtLength: function(length, opt) { + + if (!this.isDifferentiable()) return null; + + var t = this.tAtLength(length, opt); + + return this.tangentAtT(t); + }, + + // Returns a tangent line at requested `t`. + tangentAtT: function(t) { + + if (!this.isDifferentiable()) return null; + + if (t < 0) t = 0; + else if (t > 1) t = 1; + + var skeletonPoints = this.getSkeletonPoints(t); + + var p1 = skeletonPoints.startControlPoint2; + var p2 = skeletonPoints.dividerControlPoint1; + + var tangentStart = skeletonPoints.divider; + + var tangentLine = new Line(p1, p2); + tangentLine.translate(tangentStart.x - p1.x, tangentStart.y - p1.y); // move so that tangent line starts at the point requested + + return tangentLine; + }, + + // Returns `t` at requested `ratio` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + tAt: function(ratio, opt) { + + if (ratio <= 0) return 0; + if (ratio >= 1) return 1; + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + var curveLength = this.length(localOpt); + var length = curveLength * ratio; + + return this.tAtLength(length, localOpt); + }, + + // Returns `t` at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + // Uses `precision` to approximate length within `precision` (always underestimates) + // Then uses a binary search to find the `t` of a subdivision endpoint that is close (within `precision`) to the `length`, if the curve was as long as approximated + // As a rule of thumb, increasing `precision` by 1 causes the algorithm to go 2^(precision - 1) deeper + // - Precision 0 (chooses one of the two endpoints) - 0 levels + // - Precision 1 (chooses one of 5 points, <10% error) - 1 level + // - Precision 2 (<1% error) - 3 levels + // - Precision 3 (<0.1% error) - 7 levels + // - Precision 4 (<0.01% error) - 15 levels + tAtLength: function(length, opt) { + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + // identify the subdivision that contains the point at requested `length`: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + //var baseline; // straightened version of subdivision to investigate + //var baselinePoint; // point on the baseline that is the requested distance away from start + var baselinePointDistFromStart; // distance of baselinePoint from start of baseline + var baselinePointDistFromEnd; // distance of baselinePoint from end of baseline + var l = 0; // length so far + var n = subdivisions.length; + var subdivisionSize = 1 / n; + for (var i = 0; i < n; i++) { + var index = (fromStart ? i : (n - 1 - i)); + + var currentSubdivision = subdivisions[i]; + var d = currentSubdivision.endpointDistance(); // length of current subdivision + + if (length <= (l + d)) { + investigatedSubdivision = currentSubdivision; + + investigatedSubdivisionStartT = index * subdivisionSize; + investigatedSubdivisionEndT = (index + 1) * subdivisionSize; + + baselinePointDistFromStart = (fromStart ? (length - l) : ((d + l) - length)); + baselinePointDistFromEnd = (fromStart ? ((d + l) - length) : (length - l)); + + break; + } + + l += d; + } + + if (!investigatedSubdivision) return (fromStart ? 1 : 0); // length requested is out of range - return maximum t + // note that precision affects what length is recorded + // (imprecise measurements underestimate length by up to 10^(-precision) of the precise length) + // e.g. at precision 1, the length may be underestimated by up to 10% and cause this function to return 1 + + var curveLength = this.length(localOpt); + + var precisionRatio = pow(10, -precision); + + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { + + // check if we have reached required observed precision + var observedPrecisionRatio; + + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromStart / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionStartT; + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromEnd / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionEndT; + + // otherwise, set up for next iteration + var newBaselinePointDistFromStart; + var newBaselinePointDistFromEnd; + + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; + + var baseline1Length = divided[0].endpointDistance(); + var baseline2Length = divided[1].endpointDistance(); + + if (baselinePointDistFromStart <= baseline1Length) { // point at requested length is inside divided[0] + investigatedSubdivision = divided[0]; + + investigatedSubdivisionEndT -= subdivisionSize; // sudivisionSize was already halved + + newBaselinePointDistFromStart = baselinePointDistFromStart; + newBaselinePointDistFromEnd = baseline1Length - newBaselinePointDistFromStart; + + } else { // point at requested length is inside divided[1] + investigatedSubdivision = divided[1]; + + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved + + newBaselinePointDistFromStart = baselinePointDistFromStart - baseline1Length; + newBaselinePointDistFromEnd = baseline2Length - newBaselinePointDistFromStart; + } + + baselinePointDistFromStart = newBaselinePointDistFromStart; + baselinePointDistFromEnd = newBaselinePointDistFromEnd; + } + }, + + // Returns an array of points that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPoints: function(opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt + + var points = [subdivisions[0].start.clone()]; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { + + var currentSubdivision = subdivisions[i]; + points.push(currentSubdivision.end.clone()); + } + + return points; + }, + + // Returns a polyline that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPolyline: function(opt) { + + return new Polyline(this.toPoints(opt)); + }, + + toString: function() { + + return this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + }, + + translate: function(tx, ty) { + + this.start.translate(tx, ty); + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; + } +}; + +Curve.prototype.divide = Curve.prototype.divideAtT; diff --git a/src/g/ellipse.mjs b/src/g/ellipse.mjs new file mode 100644 index 0000000000..a8c967b5c6 --- /dev/null +++ b/src/g/ellipse.mjs @@ -0,0 +1,241 @@ +import { Rect } from './rect.mjs'; +import { Point } from './point.mjs'; + +const math = Math; +const sqrt = math.sqrt; +const round = math.round; +const pow = math.pow; + +export const Ellipse = function(c, a, b) { + + if (!(this instanceof Ellipse)) { + return new Ellipse(c, a, b); + } + + if (c instanceof Ellipse) { + return new Ellipse(new Point(c.x, c.y), c.a, c.b); + } + + c = new Point(c); + this.x = c.x; + this.y = c.y; + this.a = a; + this.b = b; +}; + +Ellipse.fromRect = function(rect) { + + rect = new Rect(rect); + return new Ellipse(rect.center(), rect.width / 2, rect.height / 2); +}; + +Ellipse.prototype = { + + bbox: function() { + + return new Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b); + }, + + /** + * @returns {g.Point} + */ + center: function() { + + return new Point(this.x, this.y); + }, + + clone: function() { + + return new Ellipse(this); + }, + + /** + * @param {g.Point} p + * @returns {boolean} + */ + containsPoint: function(p) { + + return this.normalizedDistance(p) <= 1; + }, + + equals: function(ellipse) { + + return !!ellipse && + ellipse.x === this.x && + ellipse.y === this.y && + ellipse.a === this.a && + ellipse.b === this.b; + }, + + // inflate by dx and dy + // @param dx {delta_x} representing additional size to x + // @param dy {delta_y} representing additional size to y - + // dy param is not required -> in that case y is sized by dx + inflate: function(dx, dy) { + if (dx === undefined) { + dx = 0; + } + + if (dy === undefined) { + dy = dx; + } + + this.a += 2 * dx; + this.b += 2 * dy; + + return this; + }, + + intersectionWithLine: function(line) { + + var intersections = []; + var a1 = line.start; + var a2 = line.end; + var rx = this.a; + var ry = this.b; + var dir = line.vector(); + var diff = a1.difference(new Point(this)); + var mDir = new Point(dir.x / (rx * rx), dir.y / (ry * ry)); + var mDiff = new Point(diff.x / (rx * rx), diff.y / (ry * ry)); + + var a = dir.dot(mDir); + var b = dir.dot(mDiff); + var c = diff.dot(mDiff) - 1.0; + var d = b * b - a * c; + + if (d < 0) { + return null; + } else if (d > 0) { + var root = sqrt(d); + var ta = (-b - root) / a; + var tb = (-b + root) / a; + + if ((ta < 0 || 1 < ta) && (tb < 0 || 1 < tb)) { + // if ((ta < 0 && tb < 0) || (ta > 1 && tb > 1)) outside else inside + return null; + } else { + if (0 <= ta && ta <= 1) intersections.push(a1.lerp(a2, ta)); + if (0 <= tb && tb <= 1) intersections.push(a1.lerp(a2, tb)); + } + } else { + var t = -b / a; + if (0 <= t && t <= 1) { + intersections.push(a1.lerp(a2, t)); + } else { + // outside + return null; + } + } + + return intersections; + }, + + // Find point on me where line from my center to + // point p intersects my boundary. + // @param {number} angle If angle is specified, intersection with rotated ellipse is computed. + intersectionWithLineFromCenterToPoint: function(p, angle) { + + p = new Point(p); + + if (angle) p.rotate(new Point(this.x, this.y), angle); + + var dx = p.x - this.x; + var dy = p.y - this.y; + var result; + + if (dx === 0) { + result = this.bbox().pointNearestToPoint(p); + if (angle) return result.rotate(new Point(this.x, this.y), -angle); + return result; + } + + var m = dy / dx; + var mSquared = m * m; + var aSquared = this.a * this.a; + var bSquared = this.b * this.b; + + var x = sqrt(1 / ((1 / aSquared) + (mSquared / bSquared))); + x = dx < 0 ? -x : x; + + var y = m * x; + result = new Point(this.x + x, this.y + y); + + if (angle) return result.rotate(new Point(this.x, this.y), -angle); + return result; + }, + + /** + * @param {g.Point} point + * @returns {number} result < 1 - inside ellipse, result == 1 - on ellipse boundary, result > 1 - outside + */ + normalizedDistance: function(point) { + + var x0 = point.x; + var y0 = point.y; + var a = this.a; + var b = this.b; + var x = this.x; + var y = this.y; + + return ((x0 - x) * (x0 - x)) / (a * a) + ((y0 - y) * (y0 - y)) / (b * b); + }, + + round: function(precision) { + + let f = 1; // case 0 + if (precision) { + switch (precision) { + case 1: f = 10; break; + case 2: f = 100; break; + case 3: f = 1000; break; + default: f = pow(10, precision); break; + } + } + + this.x = round(this.x * f) / f; + this.y = round(this.y * f) / f; + this.a = round(this.a * f) / f; + this.b = round(this.b * f) / f; + return this; + }, + + /** Compute angle between tangent and x axis + * @param {g.Point} p Point of tangency, it has to be on ellipse boundaries. + * @returns {number} angle between tangent and x axis + */ + tangentTheta: function(p) { + + var refPointDelta = 30; + var x0 = p.x; + var y0 = p.y; + var a = this.a; + var b = this.b; + var center = this.bbox().center(); + var m = center.x; + var n = center.y; + + var q1 = x0 > center.x + a / 2; + var q3 = x0 < center.x - a / 2; + + var y, x; + if (q1 || q3) { + y = x0 > center.x ? y0 - refPointDelta : y0 + refPointDelta; + x = (a * a / (x0 - m)) - (a * a * (y0 - n) * (y - n)) / (b * b * (x0 - m)) + m; + + } else { + x = y0 > center.y ? x0 + refPointDelta : x0 - refPointDelta; + y = (b * b / (y0 - n)) - (b * b * (x0 - m) * (x - m)) / (a * a * (y0 - n)) + n; + } + + return (new Point(x, y)).theta(p); + + }, + + toString: function() { + + return (new Point(this.x, this.y)).toString() + ' ' + this.a + ' ' + this.b; + } +}; + +// For backwards compatibility: +export const ellipse = Ellipse; diff --git a/src/g/geometry.helpers.mjs b/src/g/geometry.helpers.mjs new file mode 100644 index 0000000000..050fbd37cf --- /dev/null +++ b/src/g/geometry.helpers.mjs @@ -0,0 +1,57 @@ +// Declare shorthands to the most used math functions. +const math = Math; +const round = math.round; +const floor = math.floor; +const PI = math.PI; + +export const scale = { + + // Return the `value` from the `domain` interval scaled to the `range` interval. + linear: function(domain, range, value) { + + var domainSpan = domain[1] - domain[0]; + var rangeSpan = range[1] - range[0]; + return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0; + } +}; + +export const normalizeAngle = function(angle) { + + return (angle % 360) + (angle < 0 ? 360 : 0); +}; + +export const snapToGrid = function(value, gridSize) { + + return gridSize * round(value / gridSize); +}; + +export const toDeg = function(rad) { + + return (180 * rad / PI) % 360; +}; + +export const toRad = function(deg, over360) { + + over360 = over360 || false; + deg = over360 ? deg : (deg % 360); + return deg * PI / 180; +}; + +// Return a random integer from the interval [min,max], inclusive. +export const random = function(min, max) { + + if (max === undefined) { + // use first argument as max, min is 0 + max = (min === undefined) ? 1 : min; + min = 0; + + } else if (max < min) { + // switch max and min + const temp = min; + min = max; + max = temp; + } + + return floor((math.random() * (max - min + 1)) + min); +}; + diff --git a/src/g/index.mjs b/src/g/index.mjs index f2298277e7..89e7c0fa24 100644 --- a/src/g/index.mjs +++ b/src/g/index.mjs @@ -1,5675 +1,12 @@ // Geometry library. // ----------------- -// Declare shorthands to the most used math functions. -const math = Math; -const abs = math.abs; -const cos = math.cos; -const sin = math.sin; -const sqrt = math.sqrt; -const min = math.min; -const max = math.max; -const atan2 = math.atan2; -const round = math.round; -const floor = math.floor; -const PI = math.PI; -const pow = math.pow; - -export const bezier = { - - // Cubic Bezier curve path through points. - // @deprecated - // @param {array} points Array of points through which the smooth line will go. - // @return {array} SVG Path commands as an array - curveThroughPoints: function(points) { - - console.warn('deprecated'); - - return new Path(Curve.throughPoints(points)).serialize(); - }, - - // Get open-ended Bezier Spline Control Points. - // @deprecated - // @param knots Input Knot Bezier spline points (At least two points!). - // @param firstControlPoints Output First Control points. Array of knots.length - 1 length. - // @param secondControlPoints Output Second Control points. Array of knots.length - 1 length. - getCurveControlPoints: function(knots) { - - console.warn('deprecated'); - - var firstControlPoints = []; - var secondControlPoints = []; - var n = knots.length - 1; - var i; - - // Special case: Bezier curve should be a straight line. - if (n == 1) { - // 3P1 = 2P0 + P3 - firstControlPoints[0] = new Point( - (2 * knots[0].x + knots[1].x) / 3, - (2 * knots[0].y + knots[1].y) / 3 - ); - - // P2 = 2P1 – P0 - secondControlPoints[0] = new Point( - 2 * firstControlPoints[0].x - knots[0].x, - 2 * firstControlPoints[0].y - knots[0].y - ); - - return [firstControlPoints, secondControlPoints]; - } - - // Calculate first Bezier control points. - // Right hand side vector. - var rhs = []; - - // Set right hand side X values. - for (i = 1; i < n - 1; i++) { - rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x; - } - - rhs[0] = knots[0].x + 2 * knots[1].x; - rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; - - // Get first control points X-values. - var x = this.getFirstControlPoints(rhs); - - // Set right hand side Y values. - for (i = 1; i < n - 1; ++i) { - rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; - } - - rhs[0] = knots[0].y + 2 * knots[1].y; - rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; - - // Get first control points Y-values. - var y = this.getFirstControlPoints(rhs); - - // Fill output arrays. - for (i = 0; i < n; i++) { - // First control point. - firstControlPoints.push(new Point(x[i], y[i])); - - // Second control point. - if (i < n - 1) { - secondControlPoints.push(new Point( - 2 * knots [i + 1].x - x[i + 1], - 2 * knots[i + 1].y - y[i + 1] - )); - - } else { - secondControlPoints.push(new Point( - (knots[n].x + x[n - 1]) / 2, - (knots[n].y + y[n - 1]) / 2) - ); - } - } - - return [firstControlPoints, secondControlPoints]; - }, - - // Divide a Bezier curve into two at point defined by value 't' <0,1>. - // Using deCasteljau algorithm. http://math.stackexchange.com/a/317867 - // @deprecated - // @param control points (start, control start, control end, end) - // @return a function that accepts t and returns 2 curves. - getCurveDivider: function(p0, p1, p2, p3) { - - console.warn('deprecated'); - - var curve = new Curve(p0, p1, p2, p3); - - return function divideCurve(t) { - - var divided = curve.divide(t); - - return [{ - p0: divided[0].start, - p1: divided[0].controlPoint1, - p2: divided[0].controlPoint2, - p3: divided[0].end - }, { - p0: divided[1].start, - p1: divided[1].controlPoint1, - p2: divided[1].controlPoint2, - p3: divided[1].end - }]; - }; - }, - - // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. - // @deprecated - // @param rhs Right hand side vector. - // @return Solution vector. - getFirstControlPoints: function(rhs) { - - console.warn('deprecated'); - - var n = rhs.length; - // `x` is a solution vector. - var x = []; - var tmp = []; - var b = 2.0; - - x[0] = rhs[0] / b; - - // Decomposition and forward substitution. - for (var i = 1; i < n; i++) { - tmp[i] = 1 / b; - b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]; - x[i] = (rhs[i] - x[i - 1]) / b; - } - - for (i = 1; i < n; i++) { - // Backsubstitution. - x[n - i - 1] -= tmp[n - i] * x[n - i]; - } - - return x; - }, - - // Solves an inversion problem -- Given the (x, y) coordinates of a point which lies on - // a parametric curve x = x(t)/w(t), y = y(t)/w(t), find the parameter value t - // which corresponds to that point. - // @deprecated - // @param control points (start, control start, control end, end) - // @return a function that accepts a point and returns t. - getInversionSolver: function(p0, p1, p2, p3) { - - console.warn('deprecated'); - - var curve = new Curve(p0, p1, p2, p3); - - return function solveInversion(p) { - - return curve.closestPointT(p); - }; - } -}; - -export const Curve = function(p1, p2, p3, p4) { - - if (!(this instanceof Curve)) { - return new Curve(p1, p2, p3, p4); - } - - if (p1 instanceof Curve) { - return new Curve(p1.start, p1.controlPoint1, p1.controlPoint2, p1.end); - } - - this.start = new Point(p1); - this.controlPoint1 = new Point(p2); - this.controlPoint2 = new Point(p3); - this.end = new Point(p4); -}; - -// Curve passing through points. -// Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx). -// @param {array} points Array of points through which the smooth line will go. -// @return {array} curves. -Curve.throughPoints = (function() { - - // Get open-ended Bezier Spline Control Points. - // @param knots Input Knot Bezier spline points (At least two points!). - // @param firstControlPoints Output First Control points. Array of knots.length - 1 length. - // @param secondControlPoints Output Second Control points. Array of knots.length - 1 length. - function getCurveControlPoints(knots) { - - var firstControlPoints = []; - var secondControlPoints = []; - var n = knots.length - 1; - var i; - - // Special case: Bezier curve should be a straight line. - if (n == 1) { - // 3P1 = 2P0 + P3 - firstControlPoints[0] = new Point( - (2 * knots[0].x + knots[1].x) / 3, - (2 * knots[0].y + knots[1].y) / 3 - ); - - // P2 = 2P1 – P0 - secondControlPoints[0] = new Point( - 2 * firstControlPoints[0].x - knots[0].x, - 2 * firstControlPoints[0].y - knots[0].y - ); - - return [firstControlPoints, secondControlPoints]; - } - - // Calculate first Bezier control points. - // Right hand side vector. - var rhs = []; - - // Set right hand side X values. - for (i = 1; i < n - 1; i++) { - rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x; - } - - rhs[0] = knots[0].x + 2 * knots[1].x; - rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; - - // Get first control points X-values. - var x = getFirstControlPoints(rhs); - - // Set right hand side Y values. - for (i = 1; i < n - 1; ++i) { - rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; - } - - rhs[0] = knots[0].y + 2 * knots[1].y; - rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; - - // Get first control points Y-values. - var y = getFirstControlPoints(rhs); - - // Fill output arrays. - for (i = 0; i < n; i++) { - // First control point. - firstControlPoints.push(new Point(x[i], y[i])); - - // Second control point. - if (i < n - 1) { - secondControlPoints.push(new Point( - 2 * knots [i + 1].x - x[i + 1], - 2 * knots[i + 1].y - y[i + 1] - )); - - } else { - secondControlPoints.push(new Point( - (knots[n].x + x[n - 1]) / 2, - (knots[n].y + y[n - 1]) / 2 - )); - } - } - - return [firstControlPoints, secondControlPoints]; - } - - // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. - // @param rhs Right hand side vector. - // @return Solution vector. - function getFirstControlPoints(rhs) { - - var n = rhs.length; - // `x` is a solution vector. - var x = []; - var tmp = []; - var b = 2.0; - - x[0] = rhs[0] / b; - - // Decomposition and forward substitution. - for (var i = 1; i < n; i++) { - tmp[i] = 1 / b; - b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]; - x[i] = (rhs[i] - x[i - 1]) / b; - } - - for (i = 1; i < n; i++) { - // Backsubstitution. - x[n - i - 1] -= tmp[n - i] * x[n - i]; - } - - return x; - } - - return function(points) { - - if (!points || (Array.isArray(points) && points.length < 2)) { - throw new Error('At least 2 points are required'); - } - - var controlPoints = getCurveControlPoints(points); - - var curves = []; - var n = controlPoints[0].length; - for (var i = 0; i < n; i++) { - - var controlPoint1 = new Point(controlPoints[0][i].x, controlPoints[0][i].y); - var controlPoint2 = new Point(controlPoints[1][i].x, controlPoints[1][i].y); - - curves.push(new Curve(points[i], controlPoint1, controlPoint2, points[i + 1])); - } - - return curves; - }; -})(); - -Curve.prototype = { - - // Returns a bbox that tightly envelops the curve. - bbox: function() { - - var start = this.start; - var controlPoint1 = this.controlPoint1; - var controlPoint2 = this.controlPoint2; - var end = this.end; - - var x0 = start.x; - var y0 = start.y; - var x1 = controlPoint1.x; - var y1 = controlPoint1.y; - var x2 = controlPoint2.x; - var y2 = controlPoint2.y; - var x3 = end.x; - var y3 = end.y; - - var points = new Array(); // local extremes - var tvalues = new Array(); // t values of local extremes - var bounds = [new Array(), new Array()]; - - var a, b, c, t; - var t1, t2; - var b2ac, sqrtb2ac; - - for (var i = 0; i < 2; ++i) { - - if (i === 0) { - b = 6 * x0 - 12 * x1 + 6 * x2; - a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; - c = 3 * x1 - 3 * x0; - - } else { - b = 6 * y0 - 12 * y1 + 6 * y2; - a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; - c = 3 * y1 - 3 * y0; - } - - if (abs(a) < 1e-12) { // Numerical robustness - if (abs(b) < 1e-12) { // Numerical robustness - continue; - } - - t = -c / b; - if ((0 < t) && (t < 1)) tvalues.push(t); - - continue; - } - - b2ac = b * b - 4 * c * a; - sqrtb2ac = sqrt(b2ac); - - if (b2ac < 0) continue; - - t1 = (-b + sqrtb2ac) / (2 * a); - if ((0 < t1) && (t1 < 1)) tvalues.push(t1); - - t2 = (-b - sqrtb2ac) / (2 * a); - if ((0 < t2) && (t2 < 1)) tvalues.push(t2); - } - - var j = tvalues.length; - var jlen = j; - var mt; - var x, y; - - while (j--) { - t = tvalues[j]; - mt = 1 - t; - - x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); - bounds[0][j] = x; - - y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); - bounds[1][j] = y; - - points[j] = { X: x, Y: y }; - } - - tvalues[jlen] = 0; - tvalues[jlen + 1] = 1; - - points[jlen] = { X: x0, Y: y0 }; - points[jlen + 1] = { X: x3, Y: y3 }; - - bounds[0][jlen] = x0; - bounds[1][jlen] = y0; - - bounds[0][jlen + 1] = x3; - bounds[1][jlen + 1] = y3; - - tvalues.length = jlen + 2; - bounds[0].length = jlen + 2; - bounds[1].length = jlen + 2; - points.length = jlen + 2; - - var left = min.apply(null, bounds[0]); - var top = min.apply(null, bounds[1]); - var right = max.apply(null, bounds[0]); - var bottom = max.apply(null, bounds[1]); - - return new Rect(left, top, (right - left), (bottom - top)); - }, - - clone: function() { - - return new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); - }, - - // Returns the point on the curve closest to point `p` - closestPoint: function(p, opt) { - - return this.pointAtT(this.closestPointT(p, opt)); - }, - - closestPointLength: function(p, opt) { - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; - var localOpt = { precision: precision, subdivisions: subdivisions }; - - return this.lengthAtT(this.closestPointT(p, localOpt), localOpt); - }, - - closestPointNormalizedLength: function(p, opt) { - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; - var localOpt = { precision: precision, subdivisions: subdivisions }; - - var cpLength = this.closestPointLength(p, localOpt); - if (!cpLength) return 0; - - var length = this.length(localOpt); - if (length === 0) return 0; - - return cpLength / length; - }, - - // Returns `t` of the point on the curve closest to point `p` - closestPointT: function(p, opt) { - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; - // does not use localOpt - - // identify the subdivision that contains the point: - var investigatedSubdivision; - var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced - var investigatedSubdivisionEndT; - var distFromStart; // distance of point from start of baseline - var distFromEnd; // distance of point from end of baseline - var chordLength; // distance between start and end of the subdivision - var minSumDist; // lowest observed sum of the two distances - var n = subdivisions.length; - var subdivisionSize = (n ? (1 / n) : 0); - for (var i = 0; i < n; i++) { - - var currentSubdivision = subdivisions[i]; - - var startDist = currentSubdivision.start.distance(p); - var endDist = currentSubdivision.end.distance(p); - var sumDist = startDist + endDist; - - // check that the point is closest to current subdivision and not any other - if (!minSumDist || (sumDist < minSumDist)) { - investigatedSubdivision = currentSubdivision; - - investigatedSubdivisionStartT = i * subdivisionSize; - investigatedSubdivisionEndT = (i + 1) * subdivisionSize; - - distFromStart = startDist; - distFromEnd = endDist; - - chordLength = currentSubdivision.start.distance(currentSubdivision.end); - - minSumDist = sumDist; - } - } - - var precisionRatio = pow(10, -precision); - - // recursively divide investigated subdivision: - // until distance between baselinePoint and closest path endpoint is within 10^(-precision) - // then return the closest endpoint of that final subdivision - while (true) { - - // check if we have reached at least one required observed precision - // - calculated as: the difference in distances from point to start and end divided by the distance - // - note that this function is not monotonic = it doesn't converge stably but has "teeth" - // - the function decreases while one of the endpoints is fixed but "jumps" whenever we switch - // - this criterion works well for points lying far away from the curve - var startPrecisionRatio = (distFromStart ? (abs(distFromStart - distFromEnd) / distFromStart) : 0); - var endPrecisionRatio = (distFromEnd ? (abs(distFromStart - distFromEnd) / distFromEnd) : 0); - var hasRequiredPrecision = ((startPrecisionRatio < precisionRatio) || (endPrecisionRatio < precisionRatio)); - - // check if we have reached at least one required minimal distance - // - calculated as: the subdivision chord length multiplied by precisionRatio - // - calculation is relative so it will work for arbitrarily large/small curves and their subdivisions - // - this is a backup criterion that works well for points lying "almost at" the curve - var hasMinimalStartDistance = (distFromStart ? (distFromStart < (chordLength * precisionRatio)) : true); - var hasMinimalEndDistance = (distFromEnd ? (distFromEnd < (chordLength * precisionRatio)) : true); - var hasMinimalDistance = (hasMinimalStartDistance || hasMinimalEndDistance); - - // do we stop now? - if (hasRequiredPrecision || hasMinimalDistance) { - return ((distFromStart <= distFromEnd) ? investigatedSubdivisionStartT : investigatedSubdivisionEndT); - } - - // otherwise, set up for next iteration - var divided = investigatedSubdivision.divide(0.5); - subdivisionSize /= 2; - - var startDist1 = divided[0].start.distance(p); - var endDist1 = divided[0].end.distance(p); - var sumDist1 = startDist1 + endDist1; - - var startDist2 = divided[1].start.distance(p); - var endDist2 = divided[1].end.distance(p); - var sumDist2 = startDist2 + endDist2; - - if (sumDist1 <= sumDist2) { - investigatedSubdivision = divided[0]; - - investigatedSubdivisionEndT -= subdivisionSize; // subdivisionSize was already halved - - distFromStart = startDist1; - distFromEnd = endDist1; - - } else { - investigatedSubdivision = divided[1]; - - investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved - - distFromStart = startDist2; - distFromEnd = endDist2; - } - } - }, - - closestPointTangent: function(p, opt) { - - return this.tangentAtT(this.closestPointT(p, opt)); - }, - - // Returns `true` if the area surrounded by the curve contains the point `p`. - // Implements the even-odd algorithm (self-intersections are "outside"). - // Closes open curves (always imagines a closing segment). - // Precision may be adjusted by passing an `opt` object. - containsPoint: function(p, opt) { - - var polyline = this.toPolyline(opt); - return polyline.containsPoint(p); - }, - - // Divides the curve into two at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. - // For a function that uses `t`, use Curve.divideAtT(). - divideAt: function(ratio, opt) { - - if (ratio <= 0) return this.divideAtT(0); - if (ratio >= 1) return this.divideAtT(1); - - var t = this.tAt(ratio, opt); - - return this.divideAtT(t); - }, - - // Divides the curve into two at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. - divideAtLength: function(length, opt) { - - var t = this.tAtLength(length, opt); - - return this.divideAtT(t); - }, - - // Divides the curve into two at point defined by `t` between 0 and 1. - // Using de Casteljau's algorithm (http://math.stackexchange.com/a/317867). - // Additional resource: https://pomax.github.io/bezierinfo/#decasteljau - divideAtT: function(t) { - - var start = this.start; - var controlPoint1 = this.controlPoint1; - var controlPoint2 = this.controlPoint2; - var end = this.end; - - // shortcuts for `t` values that are out of range - if (t <= 0) { - return [ - new Curve(start, start, start, start), - new Curve(start, controlPoint1, controlPoint2, end) - ]; - } - - if (t >= 1) { - return [ - new Curve(start, controlPoint1, controlPoint2, end), - new Curve(end, end, end, end) - ]; - } - - var dividerPoints = this.getSkeletonPoints(t); - - var startControl1 = dividerPoints.startControlPoint1; - var startControl2 = dividerPoints.startControlPoint2; - var divider = dividerPoints.divider; - var dividerControl1 = dividerPoints.dividerControlPoint1; - var dividerControl2 = dividerPoints.dividerControlPoint2; - - // return array with two new curves - return [ - new Curve(start, startControl1, startControl2, divider), - new Curve(divider, dividerControl1, dividerControl2, end) - ]; - }, - - // Returns the distance between the curve's start and end points. - endpointDistance: function() { - - return this.start.distance(this.end); - }, - - // Checks whether two curves are exactly the same. - equals: function(c) { - - return !!c && - this.start.x === c.start.x && - this.start.y === c.start.y && - this.controlPoint1.x === c.controlPoint1.x && - this.controlPoint1.y === c.controlPoint1.y && - this.controlPoint2.x === c.controlPoint2.x && - this.controlPoint2.y === c.controlPoint2.y && - this.end.x === c.end.x && - this.end.y === c.end.y; - }, - - // Returns five helper points necessary for curve division. - getSkeletonPoints: function(t) { - - var start = this.start; - var control1 = this.controlPoint1; - var control2 = this.controlPoint2; - var end = this.end; - - // shortcuts for `t` values that are out of range - if (t <= 0) { - return { - startControlPoint1: start.clone(), - startControlPoint2: start.clone(), - divider: start.clone(), - dividerControlPoint1: control1.clone(), - dividerControlPoint2: control2.clone() - }; - } - - if (t >= 1) { - return { - startControlPoint1: control1.clone(), - startControlPoint2: control2.clone(), - divider: end.clone(), - dividerControlPoint1: end.clone(), - dividerControlPoint2: end.clone() - }; - } - - var midpoint1 = (new Line(start, control1)).pointAt(t); - var midpoint2 = (new Line(control1, control2)).pointAt(t); - var midpoint3 = (new Line(control2, end)).pointAt(t); - - var subControl1 = (new Line(midpoint1, midpoint2)).pointAt(t); - var subControl2 = (new Line(midpoint2, midpoint3)).pointAt(t); - - var divider = (new Line(subControl1, subControl2)).pointAt(t); - - var output = { - startControlPoint1: midpoint1, - startControlPoint2: subControl1, - divider: divider, - dividerControlPoint1: subControl2, - dividerControlPoint2: midpoint3 - }; - - return output; - }, - - // Returns a list of curves whose flattened length is better than `opt.precision`. - // That is, observed difference in length between recursions is less than 10^(-3) = 0.001 = 0.1% - // (Observed difference is not real precision, but close enough as long as special cases are covered) - // (That is why skipping iteration 1 is important) - // As a rule of thumb, increasing `precision` by 1 requires two more division operations - // - Precision 0 (endpointDistance) - total of 2^0 - 1 = 0 operations (1 subdivision) - // - Precision 1 (<10% error) - total of 2^2 - 1 = 3 operations (4 subdivisions) - // - Precision 2 (<1% error) - total of 2^4 - 1 = 15 operations requires 4 division operations on all elements (15 operations total) (16 subdivisions) - // - Precision 3 (<0.1% error) - total of 2^6 - 1 = 63 operations - acceptable when drawing (64 subdivisions) - // - Precision 4 (<0.01% error) - total of 2^8 - 1 = 255 operations - high resolution, can be used to interpolate `t` (256 subdivisions) - // (Variation of 1 recursion worse or better is possible depending on the curve, doubling/halving the number of operations accordingly) - getSubdivisions: function(opt) { - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - // not using opt.subdivisions - // not using localOpt - - var subdivisions = [new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end)]; - if (precision === 0) return subdivisions; - - var previousLength = this.endpointDistance(); - - var precisionRatio = pow(10, -precision); - - // recursively divide curve at `t = 0.5` - // until the difference between observed length at subsequent iterations is lower than precision - var iteration = 0; - while (true) { - iteration += 1; - - // divide all subdivisions - var newSubdivisions = []; - var numSubdivisions = subdivisions.length; - for (var i = 0; i < numSubdivisions; i++) { - - var currentSubdivision = subdivisions[i]; - var divided = currentSubdivision.divide(0.5); // dividing at t = 0.5 (not at middle length!) - newSubdivisions.push(divided[0], divided[1]); - } - - // measure new length - var length = 0; - var numNewSubdivisions = newSubdivisions.length; - for (var j = 0; j < numNewSubdivisions; j++) { - - var currentNewSubdivision = newSubdivisions[j]; - length += currentNewSubdivision.endpointDistance(); - } - - // check if we have reached required observed precision - // sine-like curves may have the same observed length in iteration 0 and 1 - skip iteration 1 - // not a problem for further iterations because cubic curves cannot have more than two local extrema - // (i.e. cubic curves cannot intersect the baseline more than once) - // therefore two subsequent iterations cannot produce sampling with equal length - var observedPrecisionRatio = ((length !== 0) ? ((length - previousLength) / length) : 0); - if (iteration > 1 && observedPrecisionRatio < precisionRatio) { - return newSubdivisions; - } - - // otherwise, set up for next iteration - subdivisions = newSubdivisions; - previousLength = length; - } - }, - - isDifferentiable: function() { - - var start = this.start; - var control1 = this.controlPoint1; - var control2 = this.controlPoint2; - var end = this.end; - - return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); - }, - - // Returns flattened length of the curve with precision better than `opt.precision`; or using `opt.subdivisions` provided. - length: function(opt) { - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call - var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; - // not using localOpt - - var length = 0; - var n = subdivisions.length; - for (var i = 0; i < n; i++) { - - var currentSubdivision = subdivisions[i]; - length += currentSubdivision.endpointDistance(); - } - - return length; - }, - - // Returns distance along the curve up to `t` with precision better than requested `opt.precision`. (Not using `opt.subdivisions`.) - lengthAtT: function(t, opt) { - - if (t <= 0) return 0; - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - // not using opt.subdivisions - // not using localOpt - - var subCurve = this.divide(t)[0]; - var subCurveLength = subCurve.length({ precision: precision }); - - return subCurveLength; - }, - - // Returns point at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. - // Mirrors Line.pointAt() function. - // For a function that tracks `t`, use Curve.pointAtT(). - pointAt: function(ratio, opt) { - - if (ratio <= 0) return this.start.clone(); - if (ratio >= 1) return this.end.clone(); - - var t = this.tAt(ratio, opt); - - return this.pointAtT(t); - }, - - // Returns point at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. - pointAtLength: function(length, opt) { - - var t = this.tAtLength(length, opt); - - return this.pointAtT(t); - }, - - // Returns the point at provided `t` between 0 and 1. - // `t` does not track distance along curve as it does in Line objects. - // Non-linear relationship, speeds up and slows down as curve warps! - // For linear length-based solution, use Curve.pointAt(). - pointAtT: function(t) { - - if (t <= 0) return this.start.clone(); - if (t >= 1) return this.end.clone(); - - return this.getSkeletonPoints(t).divider; - }, - - // Default precision - PRECISION: 3, - - round: function(precision) { - - this.start.round(precision); - this.controlPoint1.round(precision); - this.controlPoint2.round(precision); - this.end.round(precision); - return this; - }, - - scale: function(sx, sy, origin) { - - this.start.scale(sx, sy, origin); - this.controlPoint1.scale(sx, sy, origin); - this.controlPoint2.scale(sx, sy, origin); - this.end.scale(sx, sy, origin); - return this; - }, - - // Returns a tangent line at requested `ratio` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. - tangentAt: function(ratio, opt) { - - if (!this.isDifferentiable()) return null; - - if (ratio < 0) ratio = 0; - else if (ratio > 1) ratio = 1; - - var t = this.tAt(ratio, opt); - - return this.tangentAtT(t); - }, - - // Returns a tangent line at requested `length` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. - tangentAtLength: function(length, opt) { - - if (!this.isDifferentiable()) return null; - - var t = this.tAtLength(length, opt); - - return this.tangentAtT(t); - }, - - // Returns a tangent line at requested `t`. - tangentAtT: function(t) { - - if (!this.isDifferentiable()) return null; - - if (t < 0) t = 0; - else if (t > 1) t = 1; - - var skeletonPoints = this.getSkeletonPoints(t); - - var p1 = skeletonPoints.startControlPoint2; - var p2 = skeletonPoints.dividerControlPoint1; - - var tangentStart = skeletonPoints.divider; - - var tangentLine = new Line(p1, p2); - tangentLine.translate(tangentStart.x - p1.x, tangentStart.y - p1.y); // move so that tangent line starts at the point requested - - return tangentLine; - }, - - // Returns `t` at requested `ratio` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. - tAt: function(ratio, opt) { - - if (ratio <= 0) return 0; - if (ratio >= 1) return 1; - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; - var localOpt = { precision: precision, subdivisions: subdivisions }; - - var curveLength = this.length(localOpt); - var length = curveLength * ratio; - - return this.tAtLength(length, localOpt); - }, - - // Returns `t` at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. - // Uses `precision` to approximate length within `precision` (always underestimates) - // Then uses a binary search to find the `t` of a subdivision endpoint that is close (within `precision`) to the `length`, if the curve was as long as approximated - // As a rule of thumb, increasing `precision` by 1 causes the algorithm to go 2^(precision - 1) deeper - // - Precision 0 (chooses one of the two endpoints) - 0 levels - // - Precision 1 (chooses one of 5 points, <10% error) - 1 level - // - Precision 2 (<1% error) - 3 levels - // - Precision 3 (<0.1% error) - 7 levels - // - Precision 4 (<0.01% error) - 15 levels - tAtLength: function(length, opt) { - - var fromStart = true; - if (length < 0) { - fromStart = false; // negative lengths mean start calculation from end point - length = -length; // absolute value - } - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; - var localOpt = { precision: precision, subdivisions: subdivisions }; - - // identify the subdivision that contains the point at requested `length`: - var investigatedSubdivision; - var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced - var investigatedSubdivisionEndT; - //var baseline; // straightened version of subdivision to investigate - //var baselinePoint; // point on the baseline that is the requested distance away from start - var baselinePointDistFromStart; // distance of baselinePoint from start of baseline - var baselinePointDistFromEnd; // distance of baselinePoint from end of baseline - var l = 0; // length so far - var n = subdivisions.length; - var subdivisionSize = 1 / n; - for (var i = 0; i < n; i++) { - var index = (fromStart ? i : (n - 1 - i)); - - var currentSubdivision = subdivisions[i]; - var d = currentSubdivision.endpointDistance(); // length of current subdivision - - if (length <= (l + d)) { - investigatedSubdivision = currentSubdivision; - - investigatedSubdivisionStartT = index * subdivisionSize; - investigatedSubdivisionEndT = (index + 1) * subdivisionSize; - - baselinePointDistFromStart = (fromStart ? (length - l) : ((d + l) - length)); - baselinePointDistFromEnd = (fromStart ? ((d + l) - length) : (length - l)); - - break; - } - - l += d; - } - - if (!investigatedSubdivision) return (fromStart ? 1 : 0); // length requested is out of range - return maximum t - // note that precision affects what length is recorded - // (imprecise measurements underestimate length by up to 10^(-precision) of the precise length) - // e.g. at precision 1, the length may be underestimated by up to 10% and cause this function to return 1 - - var curveLength = this.length(localOpt); - - var precisionRatio = pow(10, -precision); - - // recursively divide investigated subdivision: - // until distance between baselinePoint and closest path endpoint is within 10^(-precision) - // then return the closest endpoint of that final subdivision - while (true) { - - // check if we have reached required observed precision - var observedPrecisionRatio; - - observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromStart / curveLength) : 0); - if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionStartT; - observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromEnd / curveLength) : 0); - if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionEndT; - - // otherwise, set up for next iteration - var newBaselinePointDistFromStart; - var newBaselinePointDistFromEnd; - - var divided = investigatedSubdivision.divide(0.5); - subdivisionSize /= 2; - - var baseline1Length = divided[0].endpointDistance(); - var baseline2Length = divided[1].endpointDistance(); - - if (baselinePointDistFromStart <= baseline1Length) { // point at requested length is inside divided[0] - investigatedSubdivision = divided[0]; - - investigatedSubdivisionEndT -= subdivisionSize; // sudivisionSize was already halved - - newBaselinePointDistFromStart = baselinePointDistFromStart; - newBaselinePointDistFromEnd = baseline1Length - newBaselinePointDistFromStart; - - } else { // point at requested length is inside divided[1] - investigatedSubdivision = divided[1]; - - investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved - - newBaselinePointDistFromStart = baselinePointDistFromStart - baseline1Length; - newBaselinePointDistFromEnd = baseline2Length - newBaselinePointDistFromStart; - } - - baselinePointDistFromStart = newBaselinePointDistFromStart; - baselinePointDistFromEnd = newBaselinePointDistFromEnd; - } - }, - - // Returns an array of points that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. - // Flattened length is no more than 10^(-precision) away from real curve length. - toPoints: function(opt) { - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call - var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; - // not using localOpt - - var points = [subdivisions[0].start.clone()]; - var n = subdivisions.length; - for (var i = 0; i < n; i++) { - - var currentSubdivision = subdivisions[i]; - points.push(currentSubdivision.end.clone()); - } - - return points; - }, - - // Returns a polyline that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. - // Flattened length is no more than 10^(-precision) away from real curve length. - toPolyline: function(opt) { - - return new Polyline(this.toPoints(opt)); - }, - - toString: function() { - - return this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; - }, - - translate: function(tx, ty) { - - this.start.translate(tx, ty); - this.controlPoint1.translate(tx, ty); - this.controlPoint2.translate(tx, ty); - this.end.translate(tx, ty); - return this; - } -}; - -Curve.prototype.divide = Curve.prototype.divideAtT; - -export const Ellipse = function(c, a, b) { - - if (!(this instanceof Ellipse)) { - return new Ellipse(c, a, b); - } - - if (c instanceof Ellipse) { - return new Ellipse(new Point(c.x, c.y), c.a, c.b); - } - - c = new Point(c); - this.x = c.x; - this.y = c.y; - this.a = a; - this.b = b; -}; - -Ellipse.fromRect = function(rect) { - - rect = new Rect(rect); - return new Ellipse(rect.center(), rect.width / 2, rect.height / 2); -}; - -Ellipse.prototype = { - - bbox: function() { - - return new Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b); - }, - - /** - * @returns {g.Point} - */ - center: function() { - - return new Point(this.x, this.y); - }, - - clone: function() { - - return new Ellipse(this); - }, - - /** - * @param {g.Point} p - * @returns {boolean} - */ - containsPoint: function(p) { - - return this.normalizedDistance(p) <= 1; - }, - - equals: function(ellipse) { - - return !!ellipse && - ellipse.x === this.x && - ellipse.y === this.y && - ellipse.a === this.a && - ellipse.b === this.b; - }, - - // inflate by dx and dy - // @param dx {delta_x} representing additional size to x - // @param dy {delta_y} representing additional size to y - - // dy param is not required -> in that case y is sized by dx - inflate: function(dx, dy) { - if (dx === undefined) { - dx = 0; - } - - if (dy === undefined) { - dy = dx; - } - - this.a += 2 * dx; - this.b += 2 * dy; - - return this; - }, - - intersectionWithLine: function(line) { - - var intersections = []; - var a1 = line.start; - var a2 = line.end; - var rx = this.a; - var ry = this.b; - var dir = line.vector(); - var diff = a1.difference(new Point(this)); - var mDir = new Point(dir.x / (rx * rx), dir.y / (ry * ry)); - var mDiff = new Point(diff.x / (rx * rx), diff.y / (ry * ry)); - - var a = dir.dot(mDir); - var b = dir.dot(mDiff); - var c = diff.dot(mDiff) - 1.0; - var d = b * b - a * c; - - if (d < 0) { - return null; - } else if (d > 0) { - var root = sqrt(d); - var ta = (-b - root) / a; - var tb = (-b + root) / a; - - if ((ta < 0 || 1 < ta) && (tb < 0 || 1 < tb)) { - // if ((ta < 0 && tb < 0) || (ta > 1 && tb > 1)) outside else inside - return null; - } else { - if (0 <= ta && ta <= 1) intersections.push(a1.lerp(a2, ta)); - if (0 <= tb && tb <= 1) intersections.push(a1.lerp(a2, tb)); - } - } else { - var t = -b / a; - if (0 <= t && t <= 1) { - intersections.push(a1.lerp(a2, t)); - } else { - // outside - return null; - } - } - - return intersections; - }, - - // Find point on me where line from my center to - // point p intersects my boundary. - // @param {number} angle If angle is specified, intersection with rotated ellipse is computed. - intersectionWithLineFromCenterToPoint: function(p, angle) { - - p = new Point(p); - - if (angle) p.rotate(new Point(this.x, this.y), angle); - - var dx = p.x - this.x; - var dy = p.y - this.y; - var result; - - if (dx === 0) { - result = this.bbox().pointNearestToPoint(p); - if (angle) return result.rotate(new Point(this.x, this.y), -angle); - return result; - } - - var m = dy / dx; - var mSquared = m * m; - var aSquared = this.a * this.a; - var bSquared = this.b * this.b; - - var x = sqrt(1 / ((1 / aSquared) + (mSquared / bSquared))); - x = dx < 0 ? -x : x; - - var y = m * x; - result = new Point(this.x + x, this.y + y); - - if (angle) return result.rotate(new Point(this.x, this.y), -angle); - return result; - }, - - /** - * @param {g.Point} point - * @returns {number} result < 1 - inside ellipse, result == 1 - on ellipse boundary, result > 1 - outside - */ - normalizedDistance: function(point) { - - var x0 = point.x; - var y0 = point.y; - var a = this.a; - var b = this.b; - var x = this.x; - var y = this.y; - - return ((x0 - x) * (x0 - x)) / (a * a) + ((y0 - y) * (y0 - y)) / (b * b); - }, - - round: function(precision) { - - let f = 1; // case 0 - if (precision) { - switch (precision) { - case 1: f = 10; break; - case 2: f = 100; break; - case 3: f = 1000; break; - default: f = pow(10, precision); break; - } - } - - this.x = round(this.x * f) / f; - this.y = round(this.y * f) / f; - this.a = round(this.a * f) / f; - this.b = round(this.b * f) / f; - return this; - }, - - /** Compute angle between tangent and x axis - * @param {g.Point} p Point of tangency, it has to be on ellipse boundaries. - * @returns {number} angle between tangent and x axis - */ - tangentTheta: function(p) { - - var refPointDelta = 30; - var x0 = p.x; - var y0 = p.y; - var a = this.a; - var b = this.b; - var center = this.bbox().center(); - var m = center.x; - var n = center.y; - - var q1 = x0 > center.x + a / 2; - var q3 = x0 < center.x - a / 2; - - var y, x; - if (q1 || q3) { - y = x0 > center.x ? y0 - refPointDelta : y0 + refPointDelta; - x = (a * a / (x0 - m)) - (a * a * (y0 - n) * (y - n)) / (b * b * (x0 - m)) + m; - - } else { - x = y0 > center.y ? x0 + refPointDelta : x0 - refPointDelta; - y = (b * b / (y0 - n)) - (b * b * (x0 - m) * (x - m)) / (a * a * (y0 - n)) + n; - } - - return (new Point(x, y)).theta(p); - - }, - - toString: function() { - - return (new Point(this.x, this.y)).toString() + ' ' + this.a + ' ' + this.b; - } -}; - -export const Line = function(p1, p2) { - - if (!(this instanceof Line)) { - return new Line(p1, p2); - } - - if (p1 instanceof Line) { - return new Line(p1.start, p1.end); - } - - this.start = new Point(p1); - this.end = new Point(p2); -}; - -Line.prototype = { - - // @returns the angle of incline of the line. - angle: function() { - - var horizontalPoint = new Point(this.start.x + 1, this.start.y); - return this.start.angleBetween(this.end, horizontalPoint); - }, - - bbox: function() { - - var left = min(this.start.x, this.end.x); - var top = min(this.start.y, this.end.y); - var right = max(this.start.x, this.end.x); - var bottom = max(this.start.y, this.end.y); - - return new Rect(left, top, (right - left), (bottom - top)); - }, - - // @return the bearing (cardinal direction) of the line. For example N, W, or SE. - // @returns {String} One of the following bearings : NE, E, SE, S, SW, W, NW, N. - bearing: function() { - - var lat1 = toRad(this.start.y); - var lat2 = toRad(this.end.y); - var lon1 = this.start.x; - var lon2 = this.end.x; - var dLon = toRad(lon2 - lon1); - var y = sin(dLon) * cos(lat2); - var x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon); - var brng = toDeg(atan2(y, x)); - - var bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N']; - - var index = brng - 22.5; - if (index < 0) - index += 360; - index = parseInt(index / 45); - - return bearings[index]; - }, - - clone: function() { - - return new Line(this.start, this.end); - }, - - // @return {point} the closest point on the line to point `p` - closestPoint: function(p) { - - return this.pointAt(this.closestPointNormalizedLength(p)); - }, - - closestPointLength: function(p) { - - return this.closestPointNormalizedLength(p) * this.length(); - }, - - // @return {number} the normalized length of the closest point on the line to point `p` - closestPointNormalizedLength: function(p) { - - var product = this.vector().dot((new Line(this.start, p)).vector()); - var cpNormalizedLength = min(1, max(0, product / this.squaredLength())); - - // cpNormalizedLength returns `NaN` if this line has zero length - // we can work with that - if `NaN`, return 0 - if (cpNormalizedLength !== cpNormalizedLength) return 0; // condition evaluates to `true` if and only if cpNormalizedLength is `NaN` - // (`NaN` is the only value that is not equal to itself) - - return cpNormalizedLength; - }, - - closestPointTangent: function(p) { - - return this.tangentAt(this.closestPointNormalizedLength(p)); - }, - - // Returns `true` if the point lies on the line. - containsPoint: function(p) { - - var start = this.start; - var end = this.end; - - if (start.cross(p, end) !== 0) return false; - // else: cross product of 0 indicates that this line and the vector to `p` are collinear - - var length = this.length(); - if ((new Line(start, p)).length() > length) return false; - if ((new Line(p, end)).length() > length) return false; - // else: `p` lies between start and end of the line - - return true; - }, - - // Divides the line into two at requested `ratio` between 0 and 1. - divideAt: function(ratio) { - - var dividerPoint = this.pointAt(ratio); - - // return array with two lines - return [ - new Line(this.start, dividerPoint), - new Line(dividerPoint, this.end) - ]; - }, - - // Divides the line into two at requested `length`. - divideAtLength: function(length) { - - var dividerPoint = this.pointAtLength(length); - - // return array with two new lines - return [ - new Line(this.start, dividerPoint), - new Line(dividerPoint, this.end) - ]; - }, - - equals: function(l) { - - return !!l && - this.start.x === l.start.x && - this.start.y === l.start.y && - this.end.x === l.end.x && - this.end.y === l.end.y; - }, - - // @return {point} Point where I'm intersecting a line. - // @return [point] Points where I'm intersecting a rectangle. - // @see Squeak Smalltalk, LineSegment>>intersectionWith: - intersect: function(shape, opt) { - - if (shape instanceof Line || - shape instanceof Rect || - shape instanceof Polyline || - shape instanceof Ellipse || - shape instanceof Path - ) { - var intersection = shape.intersectionWithLine(this, opt); - - // Backwards compatibility - if (intersection && (shape instanceof Line)) { - intersection = intersection[0]; - } - - return intersection; - } - - return null; - }, - - intersectionWithLine: function(line) { - - var pt1Dir = new Point(this.end.x - this.start.x, this.end.y - this.start.y); - var pt2Dir = new Point(line.end.x - line.start.x, line.end.y - line.start.y); - var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x); - var deltaPt = new Point(line.start.x - this.start.x, line.start.y - this.start.y); - var alpha = (deltaPt.x * pt2Dir.y) - (deltaPt.y * pt2Dir.x); - var beta = (deltaPt.x * pt1Dir.y) - (deltaPt.y * pt1Dir.x); - - if (det === 0 || alpha * det < 0 || beta * det < 0) { - // No intersection found. - return null; - } - - if (det > 0) { - if (alpha > det || beta > det) { - return null; - } - - } else { - if (alpha < det || beta < det) { - return null; - } - } - - return [new Point( - this.start.x + (alpha * pt1Dir.x / det), - this.start.y + (alpha * pt1Dir.y / det) - )]; - }, - - isDifferentiable: function() { - - return !this.start.equals(this.end); - }, - - // @return {double} length of the line - length: function() { - - return sqrt(this.squaredLength()); - }, - - // @return {point} my midpoint - midpoint: function() { - - return new Point( - (this.start.x + this.end.x) / 2, - (this.start.y + this.end.y) / 2 - ); - }, - - parallel: function(distance) { - const l = this.clone(); - if (!this.isDifferentiable()) return l; - const { start, end } = l; - const eRef = start.clone().rotate(end, 270); - const sRef = end.clone().rotate(start, 90); - start.move(sRef, distance); - end.move(eRef, distance); - return l; - }, - - // @return {point} my point at 't' <0,1> - pointAt: function(t) { - - var start = this.start; - var end = this.end; - - if (t <= 0) return start.clone(); - if (t >= 1) return end.clone(); - - return start.lerp(end, t); - }, - - pointAtLength: function(length) { - - var start = this.start; - var end = this.end; - - var fromStart = true; - if (length < 0) { - fromStart = false; // negative lengths mean start calculation from end point - length = -length; // absolute value - } - - var lineLength = this.length(); - if (length >= lineLength) return (fromStart ? end.clone() : start.clone()); - - return this.pointAt((fromStart ? (length) : (lineLength - length)) / lineLength); - }, - - // @return {number} the offset of the point `p` from the line. + if the point `p` is on the right side of the line, - if on the left and 0 if on the line. - pointOffset: function(p) { - - // Find the sign of the determinant of vectors (start,end), where p is the query point. - p = new Point(p); - var start = this.start; - var end = this.end; - var determinant = ((end.x - start.x) * (p.y - start.y) - (end.y - start.y) * (p.x - start.x)); - - return determinant / this.length(); - }, - - rotate: function(origin, angle) { - - this.start.rotate(origin, angle); - this.end.rotate(origin, angle); - return this; - }, - - round: function(precision) { - - this.start.round(precision); - this.end.round(precision); - return this; - }, - - scale: function(sx, sy, origin) { - - this.start.scale(sx, sy, origin); - this.end.scale(sx, sy, origin); - return this; - }, - - // @return {number} scale the line so that it has the requested length - setLength: function(length) { - - var currentLength = this.length(); - if (!currentLength) return this; - - var scaleFactor = length / currentLength; - return this.scale(scaleFactor, scaleFactor, this.start); - }, - - // @return {integer} length without sqrt - // @note for applications where the exact length is not necessary (e.g. compare only) - squaredLength: function() { - - var x0 = this.start.x; - var y0 = this.start.y; - var x1 = this.end.x; - var y1 = this.end.y; - return (x0 -= x1) * x0 + (y0 -= y1) * y0; - }, - - tangentAt: function(t) { - - if (!this.isDifferentiable()) return null; - - var start = this.start; - var end = this.end; - - var tangentStart = this.pointAt(t); // constrains `t` between 0 and 1 - - var tangentLine = new Line(start, end); - tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested - - return tangentLine; - }, - - tangentAtLength: function(length) { - - if (!this.isDifferentiable()) return null; - - var start = this.start; - var end = this.end; - - var tangentStart = this.pointAtLength(length); - - var tangentLine = new Line(start, end); - tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested - - return tangentLine; - }, - - toString: function() { - - return this.start.toString() + ' ' + this.end.toString(); - }, - - serialize: function() { - - return this.start.serialize() + ' ' + this.end.serialize(); - }, - - translate: function(tx, ty) { - - this.start.translate(tx, ty); - this.end.translate(tx, ty); - return this; - }, - - // @return vector {point} of the line - vector: function() { - - return new Point(this.end.x - this.start.x, this.end.y - this.start.y); - } -}; - -// For backwards compatibility: -Line.prototype.intersection = Line.prototype.intersect; - -// Accepts path data string, array of segments, array of Curves and/or Lines, or a Polyline. -// Path created is not guaranteed to be a valid (serializable) path (might not start with an M). -export const Path = function(arg) { - - if (!(this instanceof Path)) { - return new Path(arg); - } - - if (typeof arg === 'string') { // create from a path data string - return new Path.parse(arg); - } - - this.segments = []; - - var i; - var n; - - if (!arg) { - // don't do anything - - } else if (Array.isArray(arg) && arg.length !== 0) { // if arg is a non-empty array - // flatten one level deep - // so we can chain arbitrary Path.createSegment results - arg = arg.reduce(function(acc, val) { - return acc.concat(val); - }, []); - - n = arg.length; - if (arg[0].isSegment) { // create from an array of segments - for (i = 0; i < n; i++) { - - var segment = arg[i]; - - this.appendSegment(segment); - } - - } else { // create from an array of Curves and/or Lines - var previousObj = null; - for (i = 0; i < n; i++) { - - var obj = arg[i]; - - if (!((obj instanceof Line) || (obj instanceof Curve))) { - throw new Error('Cannot construct a path segment from the provided object.'); - } - - if (i === 0) this.appendSegment(Path.createSegment('M', obj.start)); - - // if objects do not link up, moveto segments are inserted to cover the gaps - if (previousObj && !previousObj.end.equals(obj.start)) this.appendSegment(Path.createSegment('M', obj.start)); - - if (obj instanceof Line) { - this.appendSegment(Path.createSegment('L', obj.end)); - - } else if (obj instanceof Curve) { - this.appendSegment(Path.createSegment('C', obj.controlPoint1, obj.controlPoint2, obj.end)); - } - - previousObj = obj; - } - } - - } else if (arg.isSegment) { // create from a single segment - this.appendSegment(arg); - - } else if (arg instanceof Line) { // create from a single Line - this.appendSegment(Path.createSegment('M', arg.start)); - this.appendSegment(Path.createSegment('L', arg.end)); - - } else if (arg instanceof Curve) { // create from a single Curve - this.appendSegment(Path.createSegment('M', arg.start)); - this.appendSegment(Path.createSegment('C', arg.controlPoint1, arg.controlPoint2, arg.end)); - - } else if (arg instanceof Polyline) { // create from a Polyline - if (!(arg.points && (arg.points.length !== 0))) return; // if Polyline has no points, leave Path empty - - n = arg.points.length; - for (i = 0; i < n; i++) { - - var point = arg.points[i]; - - if (i === 0) this.appendSegment(Path.createSegment('M', point)); - else this.appendSegment(Path.createSegment('L', point)); - } - - } else { // unknown object - throw new Error('Cannot construct a path from the provided object.'); - } -}; - -// More permissive than V.normalizePathData and Path.prototype.serialize. -// Allows path data strings that do not start with a Moveto command (unlike SVG specification). -// Does not require spaces between elements; commas are allowed, separators may be omitted when unambiguous (e.g. 'ZM10,10', 'L1.6.8', 'M100-200'). -// Allows for command argument chaining. -// Throws an error if wrong number of arguments is provided with a command. -// Throws an error if an unrecognized path command is provided (according to Path.segmentTypes). Only a subset of SVG commands is currently supported (L, C, M, Z). -Path.parse = function(pathData) { - - if (!pathData) return new Path(); - - var path = new Path(); - - var commandRe = /(?:[a-zA-Z] *)(?:(?:-?\d+(?:\.\d+)?(?:e[-+]?\d+)? *,? *)|(?:-?\.\d+ *,? *))+|(?:[a-zA-Z] *)(?! |\d|-|\.)/g; - var commands = pathData.match(commandRe); - - var numCommands = commands.length; - for (var i = 0; i < numCommands; i++) { - - var command = commands[i]; - var argRe = /(?:[a-zA-Z])|(?:(?:-?\d+(?:\.\d+)?(?:e[-+]?\d+)?))|(?:(?:-?\.\d+))/g; - var args = command.match(argRe); - - var segment = Path.createSegment.apply(this, args); // args = [type, coordinate1, coordinate2...] - path.appendSegment(segment); - } - - return path; -}; - -// Create a segment or an array of segments. -// Accepts unlimited points/coords arguments after `type`. -Path.createSegment = function(type) { - - if (!type) throw new Error('Type must be provided.'); - - var segmentConstructor = Path.segmentTypes[type]; - if (!segmentConstructor) throw new Error(type + ' is not a recognized path segment type.'); - - var args = []; - var n = arguments.length; - for (var i = 1; i < n; i++) { // do not add first element (`type`) to args array - args.push(arguments[i]); - } - - return applyToNew(segmentConstructor, args); -}; - -Path.prototype = { - - // Accepts one segment or an array of segments as argument. - // Throws an error if argument is not a segment or an array of segments. - appendSegment: function(arg) { - - var segments = this.segments; - var numSegments = segments.length; - // works even if path has no segments - - var currentSegment; - - var previousSegment = ((numSegments !== 0) ? segments[numSegments - 1] : null); // if we are appending to an empty path, previousSegment is null - var nextSegment = null; - - if (!Array.isArray(arg)) { // arg is a segment - if (!arg || !arg.isSegment) throw new Error('Segment required.'); - - currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); - segments.push(currentSegment); - - } else { // arg is an array of segments - // flatten one level deep - // so we can chain arbitrary Path.createSegment results - arg = arg.reduce(function(acc, val) { - return acc.concat(val); - }, []); - - if (!arg[0].isSegment) throw new Error('Segments required.'); - - var n = arg.length; - for (var i = 0; i < n; i++) { - - var currentArg = arg[i]; - currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); - segments.push(currentSegment); - previousSegment = currentSegment; - } - } - }, - - // Returns the bbox of the path. - // If path has no segments, returns null. - // If path has only invisible segments, returns bbox of the end point of last segment. - bbox: function() { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - var bbox; - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - if (segment.isVisible) { - var segmentBBox = segment.bbox(); - bbox = bbox ? bbox.union(segmentBBox) : segmentBBox; - } - } - - if (bbox) return bbox; - - // if the path has only invisible elements, return end point of last segment - var lastSegment = segments[numSegments - 1]; - return new Rect(lastSegment.end.x, lastSegment.end.y, 0, 0); - }, - - // Returns a new path that is a clone of this path. - clone: function() { - - var segments = this.segments; - var numSegments = segments.length; - // works even if path has no segments - - var path = new Path(); - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i].clone(); - path.appendSegment(segment); - } - - return path; - }, - - closestPoint: function(p, opt) { - - var t = this.closestPointT(p, opt); - if (!t) return null; - - return this.pointAtT(t); - }, - - closestPointLength: function(p, opt) { - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - - var t = this.closestPointT(p, localOpt); - if (!t) return 0; - - return this.lengthAtT(t, localOpt); - }, - - closestPointNormalizedLength: function(p, opt) { - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - - var cpLength = this.closestPointLength(p, localOpt); - if (cpLength === 0) return 0; // shortcut - - var length = this.length(localOpt); - if (length === 0) return 0; // prevents division by zero - - return cpLength / length; - }, - - // Private function. - closestPointT: function(p, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - // not using localOpt - - var closestPointT; - var minSquaredDistance = Infinity; - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - var subdivisions = segmentSubdivisions[i]; - - if (segment.isVisible) { - var segmentClosestPointT = segment.closestPointT(p, { - precision: precision, - subdivisions: subdivisions - }); - var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); - var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); - - if (squaredDistance < minSquaredDistance) { - closestPointT = { segmentIndex: i, value: segmentClosestPointT }; - minSquaredDistance = squaredDistance; - } - } - } - - if (closestPointT) return closestPointT; - - // if no visible segment, return end of last segment - return { segmentIndex: numSegments - 1, value: 1 }; - }, - - closestPointTangent: function(p, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - // not using localOpt - - var closestPointTangent; - var minSquaredDistance = Infinity; - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - var subdivisions = segmentSubdivisions[i]; - - if (segment.isDifferentiable()) { - var segmentClosestPointT = segment.closestPointT(p, { - precision: precision, - subdivisions: subdivisions - }); - var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); - var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); - - if (squaredDistance < minSquaredDistance) { - closestPointTangent = segment.tangentAtT(segmentClosestPointT); - minSquaredDistance = squaredDistance; - } - } - } - - if (closestPointTangent) return closestPointTangent; - - // if no valid segment, return null - return null; - }, - - // Returns `true` if the area surrounded by the path contains the point `p`. - // Implements the even-odd algorithm (self-intersections are "outside"). - // Closes open paths (always imagines a final closing segment). - // Precision may be adjusted by passing an `opt` object. - containsPoint: function(p, opt) { - - var polylines = this.toPolylines(opt); - if (!polylines) return false; // shortcut (this path has no polylines) - - var numPolylines = polylines.length; - - // how many component polylines does `p` lie within? - var numIntersections = 0; - for (var i = 0; i < numPolylines; i++) { - var polyline = polylines[i]; - if (polyline.containsPoint(p)) { - // `p` lies within this polyline - numIntersections++; - } - } - - // returns `true` for odd numbers of intersections (even-odd algorithm) - return ((numIntersections % 2) === 1); - }, - - // Divides the path into two at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. - divideAt: function(ratio, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - if (ratio < 0) ratio = 0; - if (ratio > 1) ratio = 1; - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - - var pathLength = this.length(localOpt); - var length = pathLength * ratio; - - return this.divideAtLength(length, localOpt); - }, - - // Divides the path into two at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. - divideAtLength: function(length, opt) { - - var numSegments = this.segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - var fromStart = true; - if (length < 0) { - fromStart = false; // negative lengths mean start calculation from end point - length = -length; // absolute value - } - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - // not using localOpt - - var i; - var segment; - - // identify the segment to divide: - - var l = 0; // length so far - var divided; - var dividedSegmentIndex; - var lastValidSegment; // visible AND differentiable - var lastValidSegmentIndex; - var t; - for (i = 0; i < numSegments; i++) { - var index = (fromStart ? i : (numSegments - 1 - i)); - - segment = this.getSegment(index); - var subdivisions = segmentSubdivisions[index]; - var d = segment.length({ precision: precision, subdivisions: subdivisions }); - - if (segment.isDifferentiable()) { // segment is not just a point - lastValidSegment = segment; - lastValidSegmentIndex = index; - - if (length <= (l + d)) { - dividedSegmentIndex = index; - divided = segment.divideAtLength(((fromStart ? 1 : -1) * (length - l)), { - precision: precision, - subdivisions: subdivisions - }); - break; - } - } - - l += d; - } - - if (!lastValidSegment) { // no valid segment found - return null; - } - - // else: the path contains at least one valid segment - - if (!divided) { // the desired length is greater than the length of the path - dividedSegmentIndex = lastValidSegmentIndex; - t = (fromStart ? 1 : 0); - divided = lastValidSegment.divideAtT(t); - } - - // create a copy of this path and replace the identified segment with its two divided parts: - - var pathCopy = this.clone(); - pathCopy.replaceSegment(dividedSegmentIndex, divided); - - var divisionStartIndex = dividedSegmentIndex; - var divisionMidIndex = dividedSegmentIndex + 1; - var divisionEndIndex = dividedSegmentIndex + 2; - - // do not insert the part if it looks like a point - if (!divided[0].isDifferentiable()) { - pathCopy.removeSegment(divisionStartIndex); - divisionMidIndex -= 1; - divisionEndIndex -= 1; - } - - // insert a Moveto segment to ensure secondPath will be valid: - var movetoEnd = pathCopy.getSegment(divisionMidIndex).start; - pathCopy.insertSegment(divisionMidIndex, Path.createSegment('M', movetoEnd)); - divisionEndIndex += 1; - - // do not insert the part if it looks like a point - if (!divided[1].isDifferentiable()) { - pathCopy.removeSegment(divisionEndIndex - 1); - divisionEndIndex -= 1; - } - - // ensure that Closepath segments in secondPath will be assigned correct subpathStartSegment: - - var secondPathSegmentIndexConversion = divisionEndIndex - divisionStartIndex - 1; - for (i = divisionEndIndex; i < pathCopy.segments.length; i++) { - - var originalSegment = this.getSegment(i - secondPathSegmentIndexConversion); - segment = pathCopy.getSegment(i); - - if ((segment.type === 'Z') && !originalSegment.subpathStartSegment.end.equals(segment.subpathStartSegment.end)) { - // pathCopy segment's subpathStartSegment is different from original segment's one - // convert this Closepath segment to a Lineto and replace it in pathCopy - var convertedSegment = Path.createSegment('L', originalSegment.end); - pathCopy.replaceSegment(i, convertedSegment); - } - } - - // distribute pathCopy segments into two paths and return those: - - var firstPath = new Path(pathCopy.segments.slice(0, divisionMidIndex)); - var secondPath = new Path(pathCopy.segments.slice(divisionMidIndex)); - - return [firstPath, secondPath]; - }, - - // Checks whether two paths are exactly the same. - // If `p` is undefined or null, returns false. - equals: function(p) { - - if (!p) return false; - - var segments = this.segments; - var otherSegments = p.segments; - - var numSegments = segments.length; - if (otherSegments.length !== numSegments) return false; // if the two paths have different number of segments, they cannot be equal - - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - var otherSegment = otherSegments[i]; - - // as soon as an inequality is found in segments, return false - if ((segment.type !== otherSegment.type) || (!segment.equals(otherSegment))) return false; - } - - // if no inequality found in segments, return true - return true; - }, - - // Accepts negative indices. - // Throws an error if path has no segments. - // Throws an error if index is out of range. - getSegment: function(index) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) throw new Error('Path has no segments.'); - - if (index < 0) index = numSegments + index; // convert negative indices to positive - if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - - return segments[index]; - }, - - // Returns an array of segment subdivisions, with precision better than requested `opt.precision`. - getSegmentSubdivisions: function(opt) { - - var segments = this.segments; - var numSegments = segments.length; - // works even if path has no segments - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - // not using opt.segmentSubdivisions - // not using localOpt - - var segmentSubdivisions = []; - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - var subdivisions = segment.getSubdivisions({ precision: precision }); - segmentSubdivisions.push(subdivisions); - } - - return segmentSubdivisions; - }, - - // Returns an array of subpaths of this path. - // Invalid paths are validated first. - // Returns `[]` if path has no segments. - getSubpaths: function() { - - const validatedPath = this.clone().validate(); - - const segments = validatedPath.segments; - const numSegments = segments.length; - - const subpaths = []; - for (let i = 0; i < numSegments; i++) { - - const segment = segments[i]; - if (segment.isSubpathStart) { - // we encountered a subpath start segment - // create a new path for segment, and push it to list of subpaths - subpaths.push(new Path(segment)); - - } else { - // append current segment to the last subpath - subpaths[subpaths.length - 1].appendSegment(segment); - } - } - - return subpaths; - }, - - // Insert `arg` at given `index`. - // `index = 0` means insert at the beginning. - // `index = segments.length` means insert at the end. - // Accepts negative indices, from `-1` to `-(segments.length + 1)`. - // Accepts one segment or an array of segments as argument. - // Throws an error if index is out of range. - // Throws an error if argument is not a segment or an array of segments. - insertSegment: function(index, arg) { - - var segments = this.segments; - var numSegments = segments.length; - // works even if path has no segments - - // note that these are incremented comapared to getSegments() - // we can insert after last element (note that this changes the meaning of index -1) - if (index < 0) index = numSegments + index + 1; // convert negative indices to positive - if (index > numSegments || index < 0) throw new Error('Index out of range.'); - - var currentSegment; - - var previousSegment = null; - var nextSegment = null; - - if (numSegments !== 0) { - if (index >= 1) { - previousSegment = segments[index - 1]; - nextSegment = previousSegment.nextSegment; // if we are inserting at end, nextSegment is null - - } else { // if index === 0 - // previousSegment is null - nextSegment = segments[0]; - } - } - - if (!Array.isArray(arg)) { - if (!arg || !arg.isSegment) throw new Error('Segment required.'); - - currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); - segments.splice(index, 0, currentSegment); - - } else { - // flatten one level deep - // so we can chain arbitrary Path.createSegment results - arg = arg.reduce(function(acc, val) { - return acc.concat(val); - }, []); - - if (!arg[0].isSegment) throw new Error('Segments required.'); - - var n = arg.length; - for (var i = 0; i < n; i++) { - - var currentArg = arg[i]; - currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); - segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments - previousSegment = currentSegment; - } - } - }, - - intersectionWithLine: function(line, opt) { - - var intersection = null; - var polylines = this.toPolylines(opt); - if (!polylines) return null; - for (var i = 0, n = polylines.length; i < n; i++) { - var polyline = polylines[i]; - var polylineIntersection = line.intersect(polyline); - if (polylineIntersection) { - intersection || (intersection = []); - if (Array.isArray(polylineIntersection)) { - Array.prototype.push.apply(intersection, polylineIntersection); - } else { - intersection.push(polylineIntersection); - } - } - } - - return intersection; - }, - - isDifferentiable: function() { - - var segments = this.segments; - var numSegments = segments.length; - - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - // as soon as a differentiable segment is found in segments, return true - if (segment.isDifferentiable()) return true; - } - - // if no differentiable segment is found in segments, return false - return false; - }, - - // Checks whether current path segments are valid. - // Note that d is allowed to be empty - should disable rendering of the path. - isValid: function() { - - var segments = this.segments; - var isValid = (segments.length === 0) || (segments[0].type === 'M'); // either empty or first segment is a Moveto - return isValid; - }, - - // Returns length of the path, with precision better than requested `opt.precision`; or using `opt.segmentSubdivisions` provided. - // If path has no segments, returns 0. - length: function(opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return 0; // if segments is an empty array - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSegmentSubdivisions() call - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - // not using localOpt - - var length = 0; - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - var subdivisions = segmentSubdivisions[i]; - length += segment.length({ subdivisions: subdivisions }); - } - - return length; - }, - - // Private function. - lengthAtT: function(t, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return 0; // if segments is an empty array - - var segmentIndex = t.segmentIndex; - if (segmentIndex < 0) return 0; // regardless of t.value - - var tValue = t.value; - if (segmentIndex >= numSegments) { - segmentIndex = numSegments - 1; - tValue = 1; - } else if (tValue < 0) tValue = 0; - else if (tValue > 1) tValue = 1; - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - // not using localOpt - - var subdivisions; - var length = 0; - for (var i = 0; i < segmentIndex; i++) { - - var segment = segments[i]; - subdivisions = segmentSubdivisions[i]; - length += segment.length({ precisison: precision, subdivisions: subdivisions }); - } - - segment = segments[segmentIndex]; - subdivisions = segmentSubdivisions[segmentIndex]; - length += segment.lengthAtT(tValue, { precisison: precision, subdivisions: subdivisions }); - - return length; - }, - - // Returns point at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. - pointAt: function(ratio, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - if (ratio <= 0) return this.start.clone(); - if (ratio >= 1) return this.end.clone(); - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - - var pathLength = this.length(localOpt); - var length = pathLength * ratio; - - return this.pointAtLength(length, localOpt); - }, - - // Returns point at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. - // Accepts negative length. - pointAtLength: function(length, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - if (length === 0) return this.start.clone(); - - var fromStart = true; - if (length < 0) { - fromStart = false; // negative lengths mean start calculation from end point - length = -length; // absolute value - } - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - // not using localOpt - - var lastVisibleSegment; - var l = 0; // length so far - for (var i = 0; i < numSegments; i++) { - var index = (fromStart ? i : (numSegments - 1 - i)); - - var segment = segments[index]; - var subdivisions = segmentSubdivisions[index]; - var d = segment.length({ precision: precision, subdivisions: subdivisions }); - - if (segment.isVisible) { - if (length <= (l + d)) { - return segment.pointAtLength(((fromStart ? 1 : -1) * (length - l)), { - precision: precision, - subdivisions: subdivisions - }); - } - - lastVisibleSegment = segment; - } - - l += d; - } - - // if length requested is higher than the length of the path, return last visible segment endpoint - if (lastVisibleSegment) return (fromStart ? lastVisibleSegment.end : lastVisibleSegment.start); - - // if no visible segment, return last segment end point (no matter if fromStart or no) - var lastSegment = segments[numSegments - 1]; - return lastSegment.end.clone(); - }, - - // Private function. - pointAtT: function(t) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - var segmentIndex = t.segmentIndex; - if (segmentIndex < 0) return segments[0].pointAtT(0); - if (segmentIndex >= numSegments) return segments[numSegments - 1].pointAtT(1); - - var tValue = t.value; - if (tValue < 0) tValue = 0; - else if (tValue > 1) tValue = 1; - - return segments[segmentIndex].pointAtT(tValue); - }, - - // Default precision - PRECISION: 3, - - // Helper method for adding segments. - prepareSegment: function(segment, previousSegment, nextSegment) { - - // insert after previous segment and before previous segment's next segment - segment.previousSegment = previousSegment; - segment.nextSegment = nextSegment; - if (previousSegment) previousSegment.nextSegment = segment; - if (nextSegment) nextSegment.previousSegment = segment; - - var updateSubpathStart = segment; - if (segment.isSubpathStart) { - segment.subpathStartSegment = segment; // assign self as subpath start segment - updateSubpathStart = nextSegment; // start updating from next segment - } - - // assign previous segment's subpath start (or self if it is a subpath start) to subsequent segments - if (updateSubpathStart) this.updateSubpathStartSegment(updateSubpathStart); - - return segment; - }, - - // Remove the segment at `index`. - // Accepts negative indices, from `-1` to `-segments.length`. - // Throws an error if path has no segments. - // Throws an error if index is out of range. - removeSegment: function(index) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) throw new Error('Path has no segments.'); - - if (index < 0) index = numSegments + index; // convert negative indices to positive - if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - - var removedSegment = segments.splice(index, 1)[0]; - var previousSegment = removedSegment.previousSegment; - var nextSegment = removedSegment.nextSegment; - - // link the previous and next segments together (if present) - if (previousSegment) previousSegment.nextSegment = nextSegment; // may be null - if (nextSegment) nextSegment.previousSegment = previousSegment; // may be null - - // if removed segment used to start a subpath, update all subsequent segments until another subpath start segment is reached - if (removedSegment.isSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); - }, - - // Replace the segment at `index` with `arg`. - // Accepts negative indices, from `-1` to `-segments.length`. - // Accepts one segment or an array of segments as argument. - // Throws an error if path has no segments. - // Throws an error if index is out of range. - // Throws an error if argument is not a segment or an array of segments. - replaceSegment: function(index, arg) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) throw new Error('Path has no segments.'); - - if (index < 0) index = numSegments + index; // convert negative indices to positive - if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - - var currentSegment; - - var replacedSegment = segments[index]; - var previousSegment = replacedSegment.previousSegment; - var nextSegment = replacedSegment.nextSegment; - - var updateSubpathStart = replacedSegment.isSubpathStart; // boolean: is an update of subpath starts necessary? - - if (!Array.isArray(arg)) { - if (!arg || !arg.isSegment) throw new Error('Segment required.'); - - currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); - segments.splice(index, 1, currentSegment); // directly replace - - if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` - - } else { - // flatten one level deep - // so we can chain arbitrary Path.createSegment results - arg = arg.reduce(function(acc, val) { - return acc.concat(val); - }, []); - - if (!arg[0].isSegment) throw new Error('Segments required.'); - - segments.splice(index, 1); - - var n = arg.length; - for (var i = 0; i < n; i++) { - - var currentArg = arg[i]; - currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); - segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments - previousSegment = currentSegment; - - if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` - } - } - - // if replaced segment used to start a subpath and no new subpath start was added, update all subsequent segments until another subpath start segment is reached - if (updateSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); - }, - - round: function(precision) { - - var segments = this.segments; - var numSegments = segments.length; - - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - segment.round(precision); - } - - return this; - }, - - scale: function(sx, sy, origin) { - - var segments = this.segments; - var numSegments = segments.length; - - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - segment.scale(sx, sy, origin); - } - - return this; - }, - - segmentAt: function(ratio, opt) { - - var index = this.segmentIndexAt(ratio, opt); - if (!index) return null; - - return this.getSegment(index); - }, - - // Accepts negative length. - segmentAtLength: function(length, opt) { - - var index = this.segmentIndexAtLength(length, opt); - if (!index) return null; - - return this.getSegment(index); - }, - - segmentIndexAt: function(ratio, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - if (ratio < 0) ratio = 0; - if (ratio > 1) ratio = 1; - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - - var pathLength = this.length(localOpt); - var length = pathLength * ratio; - - return this.segmentIndexAtLength(length, localOpt); - }, - - // Accepts negative length. - segmentIndexAtLength: function(length, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - var fromStart = true; - if (length < 0) { - fromStart = false; // negative lengths mean start calculation from end point - length = -length; // absolute value - } - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - // not using localOpt - - var lastVisibleSegmentIndex = null; - var l = 0; // length so far - for (var i = 0; i < numSegments; i++) { - var index = (fromStart ? i : (numSegments - 1 - i)); - - var segment = segments[index]; - var subdivisions = segmentSubdivisions[index]; - var d = segment.length({ precision: precision, subdivisions: subdivisions }); - - if (segment.isVisible) { - if (length <= (l + d)) return index; - lastVisibleSegmentIndex = index; - } - - l += d; - } - - // if length requested is higher than the length of the path, return last visible segment index - // if no visible segment, return null - return lastVisibleSegmentIndex; - }, - - // Returns a string that can be used to reconstruct the path. - // Additional error checking compared to toString (must start with M segment). - serialize: function() { - - if (!this.isValid()) throw new Error('Invalid path segments.'); - - return this.toString(); - }, - - // Returns tangent line at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. - tangentAt: function(ratio, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - if (ratio < 0) ratio = 0; - if (ratio > 1) ratio = 1; - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - - var pathLength = this.length(localOpt); - var length = pathLength * ratio; - - return this.tangentAtLength(length, localOpt); - }, - - // Returns tangent line at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. - // Accepts negative length. - tangentAtLength: function(length, opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - var fromStart = true; - if (length < 0) { - fromStart = false; // negative lengths mean start calculation from end point - length = -length; // absolute value - } - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - // not using localOpt - - var lastValidSegment; // visible AND differentiable (with a tangent) - var l = 0; // length so far - for (var i = 0; i < numSegments; i++) { - var index = (fromStart ? i : (numSegments - 1 - i)); - - var segment = segments[index]; - var subdivisions = segmentSubdivisions[index]; - var d = segment.length({ precision: precision, subdivisions: subdivisions }); - - if (segment.isDifferentiable()) { - if (length <= (l + d)) { - return segment.tangentAtLength(((fromStart ? 1 : -1) * (length - l)), { - precision: precision, - subdivisions: subdivisions - }); - } - - lastValidSegment = segment; - } - - l += d; - } - - // if length requested is higher than the length of the path, return tangent of endpoint of last valid segment - if (lastValidSegment) { - var t = (fromStart ? 1 : 0); - return lastValidSegment.tangentAtT(t); - } - - // if no valid segment, return null - return null; - }, - - // Private function. - tangentAtT: function(t) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - var segmentIndex = t.segmentIndex; - if (segmentIndex < 0) return segments[0].tangentAtT(0); - if (segmentIndex >= numSegments) return segments[numSegments - 1].tangentAtT(1); - - var tValue = t.value; - if (tValue < 0) tValue = 0; - else if (tValue > 1) tValue = 1; - - return segments[segmentIndex].tangentAtT(tValue); - }, - - toPoints: function(opt) { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; // if segments is an empty array - - opt = opt || {}; - var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; - var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; - - var points = []; - var partialPoints = []; - for (var i = 0; i < numSegments; i++) { - var segment = segments[i]; - if (segment.isVisible) { - var currentSegmentSubdivisions = segmentSubdivisions[i]; - if (currentSegmentSubdivisions.length > 0) { - var subdivisionPoints = currentSegmentSubdivisions.map(function(curve) { - return curve.start; - }); - Array.prototype.push.apply(partialPoints, subdivisionPoints); - } else { - partialPoints.push(segment.start); - } - } else if (partialPoints.length > 0) { - partialPoints.push(segments[i - 1].end); - points.push(partialPoints); - partialPoints = []; - } - } - - if (partialPoints.length > 0) { - partialPoints.push(this.end); - points.push(partialPoints); - } - return points; - }, - - toPolylines: function(opt) { - - var polylines = []; - var points = this.toPoints(opt); - if (!points) return null; - for (var i = 0, n = points.length; i < n; i++) { - polylines.push(new Polyline(points[i])); - } - - return polylines; - }, - - toString: function() { - - var segments = this.segments; - var numSegments = segments.length; - - var pathData = ''; - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - pathData += segment.serialize() + ' '; - } - - return pathData.trim(); - }, - - translate: function(tx, ty) { - - var segments = this.segments; - var numSegments = segments.length; - - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - segment.translate(tx, ty); - } - - return this; - }, - - // Helper method for updating subpath start of segments, starting with the one provided. - updateSubpathStartSegment: function(segment) { - - var previousSegment = segment.previousSegment; // may be null - while (segment && !segment.isSubpathStart) { - - // assign previous segment's subpath start segment to this segment - if (previousSegment) segment.subpathStartSegment = previousSegment.subpathStartSegment; // may be null - else segment.subpathStartSegment = null; // if segment had no previous segment, assign null - creates an invalid path! - - previousSegment = segment; - segment = segment.nextSegment; // move on to the segment after etc. - } - }, - - // If the path is not valid, insert M 0 0 at the beginning. - // Path with no segments is considered valid, so nothing is inserted. - validate: function() { - - if (!this.isValid()) this.insertSegment(0, Path.createSegment('M', 0, 0)); - return this; - } -}; - -Object.defineProperty(Path.prototype, 'start', { - // Getter for the first visible endpoint of the path. - - configurable: true, - - enumerable: true, - - get: function() { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; - - for (var i = 0; i < numSegments; i++) { - - var segment = segments[i]; - if (segment.isVisible) return segment.start; - } - - // if no visible segment, return last segment end point - return segments[numSegments - 1].end; - } -}); - -Object.defineProperty(Path.prototype, 'end', { - // Getter for the last visible endpoint of the path. - - configurable: true, - - enumerable: true, - - get: function() { - - var segments = this.segments; - var numSegments = segments.length; - if (numSegments === 0) return null; - - for (var i = numSegments - 1; i >= 0; i--) { - - var segment = segments[i]; - if (segment.isVisible) return segment.end; - } - - // if no visible segment, return last segment end point - return segments[numSegments - 1].end; - } -}); - -/* - Point is the most basic object consisting of x/y coordinate. - - Possible instantiations are: - * `Point(10, 20)` - * `new Point(10, 20)` - * `Point('10 20')` - * `Point(Point(10, 20))` -*/ -export const Point = function(x, y) { - - if (!(this instanceof Point)) { - return new Point(x, y); - } - - if (typeof x === 'string') { - var xy = x.split(x.indexOf('@') === -1 ? ' ' : '@'); - x = parseFloat(xy[0]); - y = parseFloat(xy[1]); - - } else if (Object(x) === x) { - y = x.y; - x = x.x; - } - - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; -}; - -// Alternative constructor, from polar coordinates. -// @param {number} Distance. -// @param {number} Angle in radians. -// @param {point} [optional] Origin. -Point.fromPolar = function(distance, angle, origin) { - - origin = new Point(origin); - var x = abs(distance * cos(angle)); - var y = abs(distance * sin(angle)); - var deg = normalizeAngle(toDeg(angle)); - - if (deg < 90) { - y = -y; - - } else if (deg < 180) { - x = -x; - y = -y; - - } else if (deg < 270) { - x = -x; - } - - return new Point(origin.x + x, origin.y + y); -}; - -// Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. -Point.random = function(x1, x2, y1, y2) { - - return new Point(random(x1, x2), random(y1, y2)); -}; - -Point.prototype = { - - chooseClosest: function(points) { - - var n = points.length; - if (n === 1) return new Point(points[0]); - var closest = null; - var minSqrDistance = Infinity; - for (var i = 0; i < n; i++) { - var p = new Point(points[i]); - var sqrDistance = this.squaredDistance(p); - if (sqrDistance < minSqrDistance) { - closest = p; - minSqrDistance = sqrDistance; - } - } - return closest; - }, - - // If point lies outside rectangle `r`, return the nearest point on the boundary of rect `r`, - // otherwise return point itself. - // (see Squeak Smalltalk, Point>>adhereTo:) - adhereToRect: function(r) { - - if (r.containsPoint(this)) { - return this; - } - - this.x = min(max(this.x, r.x), r.x + r.width); - this.y = min(max(this.y, r.y), r.y + r.height); - return this; - }, - - // Compute the angle between vector from me to p1 and the vector from me to p2. - // ordering of points p1 and p2 is important! - // theta function's angle convention: - // returns angles between 0 and 180 when the angle is counterclockwise - // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones - // returns NaN if any of the points p1, p2 is coincident with this point - angleBetween: function(p1, p2) { - - var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); - - if (angleBetween < 0) { - angleBetween += 360; // correction to keep angleBetween between 0 and 360 - } - - return angleBetween; - }, - - // Return the bearing between me and the given point. - bearing: function(point) { - - return (new Line(this, point)).bearing(); - }, - - // Returns change in angle from my previous position (-dx, -dy) to my new position - // relative to ref point. - changeInAngle: function(dx, dy, ref) { - - // Revert the translation and measure the change in angle around x-axis. - return this.clone().offset(-dx, -dy).theta(ref) - this.theta(ref); - }, - - clone: function() { - - return new Point(this); - }, - - // Returns the cross product of this point relative to two other points - // this point is the common point - // point p1 lies on the first vector, point p2 lies on the second vector - // watch out for the ordering of points p1 and p2! - // positive result indicates a clockwise ("right") turn from first to second vector - // negative result indicates a counterclockwise ("left") turn from first to second vector - // zero indicates that the first and second vector are collinear - // note that the above directions are reversed from the usual answer on the Internet - // that is because we are in a left-handed coord system (because the y-axis points downward) - cross: function(p1, p2) { - - return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; - }, - - difference: function(dx, dy) { - - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; - } - - return new Point(this.x - (dx || 0), this.y - (dy || 0)); - }, - - // Returns distance between me and point `p`. - distance: function(p) { - - return (new Line(this, p)).length(); - }, - - // Returns the dot product of this point with given other point - dot: function(p) { - - return p ? (this.x * p.x + this.y * p.y) : NaN; - }, - - equals: function(p) { - - return !!p && - this.x === p.x && - this.y === p.y; - }, - - // Linear interpolation - lerp: function(p, t) { - - var x = this.x; - var y = this.y; - return new Point((1 - t) * x + t * p.x, (1 - t) * y + t * p.y); - }, - - magnitude: function() { - - return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; - }, - - // Returns a manhattan (taxi-cab) distance between me and point `p`. - manhattanDistance: function(p) { - - return abs(p.x - this.x) + abs(p.y - this.y); - }, - - // Move point on line starting from ref ending at me by - // distance distance. - move: function(ref, distance) { - - var theta = toRad((new Point(ref)).theta(this)); - var offset = this.offset(cos(theta) * distance, -sin(theta) * distance); - return offset; - }, - - // Scales x and y such that the distance between the point and the origin (0,0) is equal to the given length. - normalize: function(length) { - - var scale = (length || 1) / this.magnitude(); - return this.scale(scale, scale); - }, - - // Offset me by the specified amount. - offset: function(dx, dy) { - - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; - } - - this.x += dx || 0; - this.y += dy || 0; - return this; - }, - - // Returns a point that is the reflection of me with - // the center of inversion in ref point. - reflection: function(ref) { - - return (new Point(ref)).move(this, this.distance(ref)); - }, - - // Rotate point by angle around origin. - // Angle is flipped because this is a left-handed coord system (y-axis points downward). - rotate: function(origin, angle) { - - if (angle === 0) return this; - - origin = origin || new Point(0, 0); - - angle = toRad(normalizeAngle(-angle)); - var cosAngle = cos(angle); - var sinAngle = sin(angle); - - var x = (cosAngle * (this.x - origin.x)) - (sinAngle * (this.y - origin.y)) + origin.x; - var y = (sinAngle * (this.x - origin.x)) + (cosAngle * (this.y - origin.y)) + origin.y; - - this.x = x; - this.y = y; - return this; - }, - - round: function(precision) { - - let f = 1; // case 0 - if (precision) { - switch (precision) { - case 1: f = 10; break; - case 2: f = 100; break; - case 3: f = 1000; break; - default: f = pow(10, precision); break; - } - } - - this.x = round(this.x * f) / f; - this.y = round(this.y * f) / f; - return this; - }, - - // Scale point with origin. - scale: function(sx, sy, origin) { - - origin = (origin && new Point(origin)) || new Point(0, 0); - this.x = origin.x + sx * (this.x - origin.x); - this.y = origin.y + sy * (this.y - origin.y); - return this; - }, - - snapToGrid: function(gx, gy) { - - this.x = snapToGrid(this.x, gx); - this.y = snapToGrid(this.y, gy || gx); - return this; - }, - - squaredDistance: function(p) { - - return (new Line(this, p)).squaredLength(); - }, - - // Compute the angle between me and `p` and the x axis. - // (cartesian-to-polar coordinates conversion) - // Return theta angle in degrees. - theta: function(p) { - - p = new Point(p); - - // Invert the y-axis. - var y = -(p.y - this.y); - var x = p.x - this.x; - var rad = atan2(y, x); // defined for all 0 corner cases - - // Correction for III. and IV. quadrant. - if (rad < 0) { - rad = 2 * PI + rad; - } - - return 180 * rad / PI; - }, - - toJSON: function() { - - return { x: this.x, y: this.y }; - }, - - // Converts rectangular to polar coordinates. - // An origin can be specified, otherwise it's 0@0. - toPolar: function(o) { - - o = (o && new Point(o)) || new Point(0, 0); - var x = this.x; - var y = this.y; - this.x = sqrt((x - o.x) * (x - o.x) + (y - o.y) * (y - o.y)); // r - this.y = toRad(o.theta(new Point(x, y))); - return this; - }, - - toString: function() { - - return this.x + '@' + this.y; - }, - - serialize: function() { - - return this.x + ',' + this.y; - }, - - update: function(x, y) { - - if ((Object(x) === x)) { - y = x.y; - x = x.x; - } - - this.x = x || 0; - this.y = y || 0; - return this; - }, - - // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. - // Returns NaN if p is at 0,0. - vectorAngle: function(p) { - - var zero = new Point(0, 0); - return zero.angleBetween(this, p); - } -}; - -Point.prototype.translate = Point.prototype.offset; - -export const Polyline = function(points) { - - if (!(this instanceof Polyline)) { - return new Polyline(points); - } - - if (typeof points === 'string') { - return new Polyline.parse(points); - } - - this.points = (Array.isArray(points) ? points.map(Point) : []); -}; - -Polyline.parse = function(svgString) { - svgString = svgString.trim(); - if (svgString === '') return new Polyline(); - - var points = []; - - var coords = svgString.split(/\s*,\s*|\s+/); - var n = coords.length; - for (var i = 0; i < n; i += 2) { - points.push({ x: +coords[i], y: +coords[i + 1] }); - } - - return new Polyline(points); -}; - -Polyline.prototype = { - - bbox: function() { - - var x1 = Infinity; - var x2 = -Infinity; - var y1 = Infinity; - var y2 = -Infinity; - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return null; // if points array is empty - - for (var i = 0; i < numPoints; i++) { - - var point = points[i]; - var x = point.x; - var y = point.y; - - if (x < x1) x1 = x; - if (x > x2) x2 = x; - if (y < y1) y1 = y; - if (y > y2) y2 = y; - } - - return new Rect(x1, y1, x2 - x1, y2 - y1); - }, - - clone: function() { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return new Polyline(); // if points array is empty - - var newPoints = []; - for (var i = 0; i < numPoints; i++) { - - var point = points[i].clone(); - newPoints.push(point); - } - - return new Polyline(newPoints); - }, - - closestPoint: function(p) { - - var cpLength = this.closestPointLength(p); - - return this.pointAtLength(cpLength); - }, - - closestPointLength: function(p) { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return 0; // if points array is empty - if (numPoints === 1) return 0; // if there is only one point - - var cpLength; - var minSqrDistance = Infinity; - var length = 0; - var n = numPoints - 1; - for (var i = 0; i < n; i++) { - - var line = new Line(points[i], points[i + 1]); - var lineLength = line.length(); - - var cpNormalizedLength = line.closestPointNormalizedLength(p); - var cp = line.pointAt(cpNormalizedLength); - - var sqrDistance = cp.squaredDistance(p); - if (sqrDistance < minSqrDistance) { - minSqrDistance = sqrDistance; - cpLength = length + (cpNormalizedLength * lineLength); - } - - length += lineLength; - } - - return cpLength; - }, - - closestPointNormalizedLength: function(p) { - - var cpLength = this.closestPointLength(p); - if (cpLength === 0) return 0; // shortcut - - var length = this.length(); - if (length === 0) return 0; // prevents division by zero - - return cpLength / length; - }, - - closestPointTangent: function(p) { - - var cpLength = this.closestPointLength(p); - - return this.tangentAtLength(cpLength); - }, - - // Returns `true` if the area surrounded by the polyline contains the point `p`. - // Implements the even-odd SVG algorithm (self-intersections are "outside"). - // (Uses horizontal rays to the right of `p` to look for intersections.) - // Closes open polylines (always imagines a final closing segment). - containsPoint: function(p) { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return false; // shortcut (this polyline has no points) - - var x = p.x; - var y = p.y; - - // initialize a final closing segment by creating one from last-first points on polyline - var startIndex = numPoints - 1; // start of current polyline segment - var endIndex = 0; // end of current polyline segment - var numIntersections = 0; - for (; endIndex < numPoints; endIndex++) { - var start = points[startIndex]; - var end = points[endIndex]; - if (p.equals(start)) return true; // shortcut (`p` is a point on polyline) - - var segment = new Line(start, end); // current polyline segment - if (segment.containsPoint(p)) return true; // shortcut (`p` lies on a polyline segment) - - // do we have an intersection? - if (((y <= start.y) && (y > end.y)) || ((y > start.y) && (y <= end.y))) { - // this conditional branch IS NOT entered when `segment` is collinear/coincident with `ray` - // (when `y === start.y === end.y`) - // this conditional branch IS entered when `segment` touches `ray` at only one point - // (e.g. when `y === start.y !== end.y`) - // since this branch is entered again for the following segment, the two touches cancel out - - var xDifference = (((start.x - x) > (end.x - x)) ? (start.x - x) : (end.x - x)); - if (xDifference >= 0) { - // segment lies at least partially to the right of `p` - var rayEnd = new Point((x + xDifference), y); // right - var ray = new Line(p, rayEnd); - - if (segment.intersect(ray)) { - // an intersection was detected to the right of `p` - numIntersections++; - } - } // else: `segment` lies completely to the left of `p` (i.e. no intersection to the right) - } - - // move to check the next polyline segment - startIndex = endIndex; - } - - // returns `true` for odd numbers of intersections (even-odd algorithm) - return ((numIntersections % 2) === 1); - }, - - // Returns a convex-hull polyline from this polyline. - // Implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan). - // Output polyline starts at the first element of the original polyline that is on the hull, then continues clockwise. - // Minimal polyline is found (only vertices of the hull are reported, no collinear points). - convexHull: function() { - - var i; - var n; - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return new Polyline(); // if points array is empty - - // step 1: find the starting point - point with the lowest y (if equality, highest x) - var startPoint; - for (i = 0; i < numPoints; i++) { - if (startPoint === undefined) { - // if this is the first point we see, set it as start point - startPoint = points[i]; - - } else if (points[i].y < startPoint.y) { - // start point should have lowest y from all points - startPoint = points[i]; - - } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { - // if two points have the lowest y, choose the one that has highest x - // there are no points to the right of startPoint - no ambiguity about theta 0 - // if there are several coincident start point candidates, first one is reported - startPoint = points[i]; - } - } - - // step 2: sort the list of points - // sorting by angle between line from startPoint to point and the x-axis (theta) - - // step 2a: create the point records = [point, originalIndex, angle] - var sortedPointRecords = []; - for (i = 0; i < numPoints; i++) { - - var angle = startPoint.theta(points[i]); - if (angle === 0) { - angle = 360; // give highest angle to start point - // the start point will end up at end of sorted list - // the start point will end up at beginning of hull points list - } - - var entry = [points[i], i, angle]; - sortedPointRecords.push(entry); - } - - // step 2b: sort the list in place - sortedPointRecords.sort(function(record1, record2) { - // returning a negative number here sorts record1 before record2 - // if first angle is smaller than second, first angle should come before second - - var sortOutput = record1[2] - record2[2]; // negative if first angle smaller - if (sortOutput === 0) { - // if the two angles are equal, sort by originalIndex - sortOutput = record2[1] - record1[1]; // negative if first index larger - // coincident points will be sorted in reverse-numerical order - // so the coincident points with lower original index will be considered first - } - - return sortOutput; - }); - - // step 2c: duplicate start record from the top of the stack to the bottom of the stack - if (sortedPointRecords.length > 2) { - var startPointRecord = sortedPointRecords[sortedPointRecords.length - 1]; - sortedPointRecords.unshift(startPointRecord); - } - - // step 3a: go through sorted points in order and find those with right turns - // we want to get our results in clockwise order - var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull - var hullPointRecords = []; // stack of records with right turns - hull point candidates - - var currentPointRecord; - var currentPoint; - var lastHullPointRecord; - var lastHullPoint; - var secondLastHullPointRecord; - var secondLastHullPoint; - while (sortedPointRecords.length !== 0) { - - currentPointRecord = sortedPointRecords.pop(); - currentPoint = currentPointRecord[0]; - - // check if point has already been discarded - // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' - if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { - // this point had an incorrect turn at some previous iteration of this loop - // this disqualifies it from possibly being on the hull - continue; - } - - var correctTurnFound = false; - while (!correctTurnFound) { - - if (hullPointRecords.length < 2) { - // not enough points for comparison, just add current point - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; - - } else { - lastHullPointRecord = hullPointRecords.pop(); - lastHullPoint = lastHullPointRecord[0]; - secondLastHullPointRecord = hullPointRecords.pop(); - secondLastHullPoint = secondLastHullPointRecord[0]; - - var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); - - if (crossProduct < 0) { - // found a right turn - hullPointRecords.push(secondLastHullPointRecord); - hullPointRecords.push(lastHullPointRecord); - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; - - } else if (crossProduct === 0) { - // the three points are collinear - // three options: - // there may be a 180 or 0 degree angle at lastHullPoint - // or two of the three points are coincident - var THRESHOLD = 1e-10; // we have to take rounding errors into account - var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); - if (abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 - // if the cross product is 0 because the angle is 180 degrees - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { - // if the cross product is 0 because two points are the same - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 - // if the cross product is 0 because the angle is 0 degrees - // remove last hull point from hull BUT do not discard it - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // put last hull point back into the sorted point records list - sortedPointRecords.push(lastHullPointRecord); - // we are switching the order of the 0deg and 180deg points - // correct turn not found - } - - } else { - // found a left turn - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter of loop) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - } - } - } - } - // at this point, hullPointRecords contains the output points in clockwise order - // the points start with lowest-y,highest-x startPoint, and end at the same point - - // step 3b: remove duplicated startPointRecord from the end of the array - if (hullPointRecords.length > 2) { - hullPointRecords.pop(); - } - - // step 4: find the lowest originalIndex record and put it at the beginning of hull - var lowestHullIndex; // the lowest originalIndex on the hull - var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex - n = hullPointRecords.length; - for (i = 0; i < n; i++) { - - var currentHullIndex = hullPointRecords[i][1]; - - if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { - lowestHullIndex = currentHullIndex; - indexOfLowestHullIndexRecord = i; - } - } - - var hullPointRecordsReordered = []; - if (indexOfLowestHullIndexRecord > 0) { - var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); - var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); - hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); - - } else { - hullPointRecordsReordered = hullPointRecords; - } - - var hullPoints = []; - n = hullPointRecordsReordered.length; - for (i = 0; i < n; i++) { - hullPoints.push(hullPointRecordsReordered[i][0]); - } - - return new Polyline(hullPoints); - }, - - // Checks whether two polylines are exactly the same. - // If `p` is undefined or null, returns false. - equals: function(p) { - - if (!p) return false; - - var points = this.points; - var otherPoints = p.points; - - var numPoints = points.length; - if (otherPoints.length !== numPoints) return false; // if the two polylines have different number of points, they cannot be equal - - for (var i = 0; i < numPoints; i++) { - - var point = points[i]; - var otherPoint = p.points[i]; - - // as soon as an inequality is found in points, return false - if (!point.equals(otherPoint)) return false; - } - - // if no inequality found in points, return true - return true; - }, - - intersectionWithLine: function(l) { - var line = new Line(l); - var intersections = []; - var points = this.points; - for (var i = 0, n = points.length - 1; i < n; i++) { - var a = points[i]; - var b = points[i + 1]; - var l2 = new Line(a, b); - var int = line.intersectionWithLine(l2); - if (int) intersections.push(int[0]); - } - return (intersections.length > 0) ? intersections : null; - }, - - isDifferentiable: function() { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return false; - - var n = numPoints - 1; - for (var i = 0; i < n; i++) { - - var a = points[i]; - var b = points[i + 1]; - var line = new Line(a, b); - - // as soon as a differentiable line is found between two points, return true - if (line.isDifferentiable()) return true; - } - - // if no differentiable line is found between pairs of points, return false - return false; - }, - - length: function() { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return 0; // if points array is empty - - var length = 0; - var n = numPoints - 1; - for (var i = 0; i < n; i++) { - length += points[i].distance(points[i + 1]); - } - - return length; - }, - - pointAt: function(ratio) { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return null; // if points array is empty - if (numPoints === 1) return points[0].clone(); // if there is only one point - - if (ratio <= 0) return points[0].clone(); - if (ratio >= 1) return points[numPoints - 1].clone(); - - var polylineLength = this.length(); - var length = polylineLength * ratio; - - return this.pointAtLength(length); - }, - - pointAtLength: function(length) { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return null; // if points array is empty - if (numPoints === 1) return points[0].clone(); // if there is only one point - - var fromStart = true; - if (length < 0) { - fromStart = false; // negative lengths mean start calculation from end point - length = -length; // absolute value - } - - var l = 0; - var n = numPoints - 1; - for (var i = 0; i < n; i++) { - var index = (fromStart ? i : (n - 1 - i)); - - var a = points[index]; - var b = points[index + 1]; - var line = new Line(a, b); - var d = a.distance(b); - - if (length <= (l + d)) { - return line.pointAtLength((fromStart ? 1 : -1) * (length - l)); - } - - l += d; - } - - // if length requested is higher than the length of the polyline, return last endpoint - var lastPoint = (fromStart ? points[numPoints - 1] : points[0]); - return lastPoint.clone(); - }, - - round: function(precision) { - - var points = this.points; - var numPoints = points.length; - - for (var i = 0; i < numPoints; i++) { - points[i].round(precision); - } - - return this; - }, - - scale: function(sx, sy, origin) { - - var points = this.points; - var numPoints = points.length; - - for (var i = 0; i < numPoints; i++) { - points[i].scale(sx, sy, origin); - } - - return this; - }, - - simplify: function(opt = {}) { - - const points = this.points; - if (points.length < 3) return this; // we need at least 3 points - - // TODO: we may also accept startIndex and endIndex to specify where to start and end simplification - const threshold = opt.threshold || 0; // = max distance of middle point from chord to be simplified - - // start at the beginning of the polyline and go forward - let currentIndex = 0; - // we need at least one intermediate point (3 points) in every iteration - // as soon as that stops being true, we know we reached the end of the polyline - while (points[currentIndex + 2]) { - const firstIndex = currentIndex; - const middleIndex = (currentIndex + 1); - const lastIndex = (currentIndex + 2); - - const firstPoint = points[firstIndex]; - const middlePoint = points[middleIndex]; - const lastPoint = points[lastIndex]; - - const chord = new Line(firstPoint, lastPoint); // = connection between first and last point - const closestPoint = chord.closestPoint(middlePoint); // = closest point on chord from middle point - const closestPointDistance = closestPoint.distance(middlePoint); - if (closestPointDistance <= threshold) { - // middle point is close enough to the chord = simplify - // 1) remove middle point: - points.splice(middleIndex, 1); - // 2) in next iteration, investigate the newly-created triplet of points - // - do not change `currentIndex` - // = (first point stays, point after removed point becomes middle point) - } else { - // middle point is far from the chord - // 1) preserve middle point - // 2) in next iteration, move `currentIndex` by one step: - currentIndex += 1; - // = (point after first point becomes first point) - } - } - - // `points` array was modified in-place - return this; - }, - - tangentAt: function(ratio) { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return null; // if points array is empty - if (numPoints === 1) return null; // if there is only one point - - if (ratio < 0) ratio = 0; - if (ratio > 1) ratio = 1; - - var polylineLength = this.length(); - var length = polylineLength * ratio; - - return this.tangentAtLength(length); - }, - - tangentAtLength: function(length) { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return null; // if points array is empty - if (numPoints === 1) return null; // if there is only one point - - var fromStart = true; - if (length < 0) { - fromStart = false; // negative lengths mean start calculation from end point - length = -length; // absolute value - } - - var lastValidLine; // differentiable (with a tangent) - var l = 0; // length so far - var n = numPoints - 1; - for (var i = 0; i < n; i++) { - var index = (fromStart ? i : (n - 1 - i)); - - var a = points[index]; - var b = points[index + 1]; - var line = new Line(a, b); - var d = a.distance(b); - - if (line.isDifferentiable()) { // has a tangent line (line length is not 0) - if (length <= (l + d)) { - return line.tangentAtLength((fromStart ? 1 : -1) * (length - l)); - } - - lastValidLine = line; - } - - l += d; - } - - // if length requested is higher than the length of the polyline, return last valid endpoint - if (lastValidLine) { - var ratio = (fromStart ? 1 : 0); - return lastValidLine.tangentAt(ratio); - } - - // if no valid line, return null - return null; - }, - - toString: function() { - - return this.points + ''; - }, - - translate: function(tx, ty) { - - var points = this.points; - var numPoints = points.length; - - for (var i = 0; i < numPoints; i++) { - points[i].translate(tx, ty); - } - - return this; - }, - - // Return svgString that can be used to recreate this line. - serialize: function() { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return ''; // if points array is empty - - var output = ''; - for (var i = 0; i < numPoints; i++) { - - var point = points[i]; - output += point.x + ',' + point.y + ' '; - } - - return output.trim(); - } -}; - -Object.defineProperty(Polyline.prototype, 'start', { - // Getter for the first point of the polyline. - - configurable: true, - - enumerable: true, - - get: function() { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return null; // if points array is empty - - return this.points[0]; - }, -}); - -Object.defineProperty(Polyline.prototype, 'end', { - // Getter for the last point of the polyline. - - configurable: true, - - enumerable: true, - - get: function() { - - var points = this.points; - var numPoints = points.length; - if (numPoints === 0) return null; // if points array is empty - - return this.points[numPoints - 1]; - }, -}); - -export const Rect = function(x, y, w, h) { - - if (!(this instanceof Rect)) { - return new Rect(x, y, w, h); - } - - if ((Object(x) === x)) { - y = x.y; - w = x.width; - h = x.height; - x = x.x; - } - - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; - this.width = w === undefined ? 0 : w; - this.height = h === undefined ? 0 : h; -}; - -Rect.fromEllipse = function(e) { - - e = new Ellipse(e); - return new Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); -}; - -Rect.fromPointUnion = function(...points) { - - if (points.length === 0) return null; - - const p = new Point(); - let minX, minY, maxX, maxY; - minX = minY = Infinity; - maxX = maxY = -Infinity; - - for (let i = 0; i < points.length; i++) { - p.update(points[i]); - const x = p.x; - const y = p.y; - - if (x < minX) minX = x; - if (x > maxX) maxX = x; - if (y < minY) minY = y; - if (y > maxY) maxY = y; - } - - return new Rect(minX, minY, maxX - minX, maxY - minY); -}; - -Rect.fromRectUnion = function(...rects) { - - if (rects.length === 0) return null; - - const r = new Rect(); - let minX, minY, maxX, maxY; - minX = minY = Infinity; - maxX = maxY = -Infinity; - - for (let i = 0; i < rects.length; i++) { - r.update(rects[i]); - const x = r.x; - const y = r.y; - const mX = x + r.width; - const mY = y + r.height; - - if (x < minX) minX = x; - if (mX > maxX) maxX = mX; - if (y < minY) minY = y; - if (mY > maxY) maxY = mY; - } - - return new Rect(minX, minY, maxX - minX, maxY - minY); -}; - -Rect.prototype = { - - // Find my bounding box when I'm rotated with the center of rotation in the center of me. - // @return r {rectangle} representing a bounding box - bbox: function(angle) { - - if (!angle) return this.clone(); - - var theta = toRad(angle); - var st = abs(sin(theta)); - var ct = abs(cos(theta)); - var w = this.width * ct + this.height * st; - var h = this.width * st + this.height * ct; - return new Rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h); - }, - - bottomLeft: function() { - - return new Point(this.x, this.y + this.height); - }, - - bottomLine: function() { - - return new Line(this.bottomLeft(), this.bottomRight()); - }, - - bottomMiddle: function() { - - return new Point(this.x + this.width / 2, this.y + this.height); - }, - - center: function() { - - return new Point(this.x + this.width / 2, this.y + this.height / 2); - }, - - clone: function() { - - return new Rect(this); - }, - - // @return {bool} true if point p is inside me. - containsPoint: function(p) { - - p = new Point(p); - return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height; - }, - - // @return {bool} true if rectangle `r` is inside me. - containsRect: function(r) { - - var r0 = new Rect(this).normalize(); - var r1 = new Rect(r).normalize(); - var w0 = r0.width; - var h0 = r0.height; - var w1 = r1.width; - var h1 = r1.height; - - if (!w0 || !h0 || !w1 || !h1) { - // At least one of the dimensions is 0 - return false; - } - - var x0 = r0.x; - var y0 = r0.y; - var x1 = r1.x; - var y1 = r1.y; - - w1 += x1; - w0 += x0; - h1 += y1; - h0 += y0; - - return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0; - }, - - corner: function() { - - return new Point(this.x + this.width, this.y + this.height); - }, - - // @return {boolean} true if rectangles are equal. - equals: function(r) { - - var mr = (new Rect(this)).normalize(); - var nr = (new Rect(r)).normalize(); - return mr.x === nr.x && mr.y === nr.y && mr.width === nr.width && mr.height === nr.height; - }, - - // inflate by dx and dy, recompute origin [x, y] - // @param dx {delta_x} representing additional size to x - // @param dy {delta_y} representing additional size to y - - // dy param is not required -> in that case y is sized by dx - inflate: function(dx, dy) { - - if (dx === undefined) { - dx = 0; - } - - if (dy === undefined) { - dy = dx; - } - - this.x -= dx; - this.y -= dy; - this.width += 2 * dx; - this.height += 2 * dy; - - return this; - }, - - // @return {rect} if rectangles intersect, {null} if not. - intersect: function(r) { - - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = r.origin(); - var rCorner = r.corner(); - - // No intersection found - if (rCorner.x <= myOrigin.x || - rCorner.y <= myOrigin.y || - rOrigin.x >= myCorner.x || - rOrigin.y >= myCorner.y) return null; - - var x = max(myOrigin.x, rOrigin.x); - var y = max(myOrigin.y, rOrigin.y); - - return new Rect(x, y, min(myCorner.x, rCorner.x) - x, min(myCorner.y, rCorner.y) - y); - }, - - intersectionWithLine: function(line) { - - var r = this; - var rectLines = [r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine()]; - var points = []; - var dedupeArr = []; - var pt, i; - - var n = rectLines.length; - for (i = 0; i < n; i++) { - - pt = line.intersect(rectLines[i]); - if (pt !== null && dedupeArr.indexOf(pt.toString()) < 0) { - points.push(pt); - dedupeArr.push(pt.toString()); - } - } - - return points.length > 0 ? points : null; - }, - - // Find point on my boundary where line starting - // from my center ending in point p intersects me. - // @param {number} angle If angle is specified, intersection with rotated rectangle is computed. - intersectionWithLineFromCenterToPoint: function(p, angle) { - - p = new Point(p); - var center = new Point(this.x + this.width / 2, this.y + this.height / 2); - var result; - - if (angle) p.rotate(center, angle); - - // (clockwise, starting from the top side) - var sides = [ - this.topLine(), - this.rightLine(), - this.bottomLine(), - this.leftLine() - ]; - var connector = new Line(center, p); - - for (var i = sides.length - 1; i >= 0; --i) { - var intersection = sides[i].intersection(connector); - if (intersection !== null) { - result = intersection; - break; - } - } - if (result && angle) result.rotate(center, -angle); - return result; - }, - - leftLine: function() { - - return new Line(this.topLeft(), this.bottomLeft()); - }, - - leftMiddle: function() { - - return new Point(this.x, this.y + this.height / 2); - }, - - maxRectScaleToFit: function(rect, origin) { - - rect = new Rect(rect); - origin || (origin = rect.center()); - - var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4; - var ox = origin.x; - var oy = origin.y; - - // Here we find the maximal possible scale for all corner points (for x and y axis) of the rectangle, - // so when the scale is applied the point is still inside the rectangle. - - sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity; - - // Top Left - var p1 = rect.topLeft(); - if (p1.x < ox) { - sx1 = (this.x - ox) / (p1.x - ox); - } - if (p1.y < oy) { - sy1 = (this.y - oy) / (p1.y - oy); - } - // Bottom Right - var p2 = rect.bottomRight(); - if (p2.x > ox) { - sx2 = (this.x + this.width - ox) / (p2.x - ox); - } - if (p2.y > oy) { - sy2 = (this.y + this.height - oy) / (p2.y - oy); - } - // Top Right - var p3 = rect.topRight(); - if (p3.x > ox) { - sx3 = (this.x + this.width - ox) / (p3.x - ox); - } - if (p3.y < oy) { - sy3 = (this.y - oy) / (p3.y - oy); - } - // Bottom Left - var p4 = rect.bottomLeft(); - if (p4.x < ox) { - sx4 = (this.x - ox) / (p4.x - ox); - } - if (p4.y > oy) { - sy4 = (this.y + this.height - oy) / (p4.y - oy); - } - - return { - sx: min(sx1, sx2, sx3, sx4), - sy: min(sy1, sy2, sy3, sy4) - }; - }, - - maxRectUniformScaleToFit: function(rect, origin) { - - var scale = this.maxRectScaleToFit(rect, origin); - return min(scale.sx, scale.sy); - }, - - // Move and expand me. - // @param r {rectangle} representing deltas - moveAndExpand: function(r) { - - this.x += r.x || 0; - this.y += r.y || 0; - this.width += r.width || 0; - this.height += r.height || 0; - return this; - }, - - // Normalize the rectangle; i.e., make it so that it has a non-negative width and height. - // If width < 0 the function swaps the left and right corners, - // and it swaps the top and bottom corners if height < 0 - // like in http://qt-project.org/doc/qt-4.8/qrectf.html#normalized - normalize: function() { - - var newx = this.x; - var newy = this.y; - var newwidth = this.width; - var newheight = this.height; - if (this.width < 0) { - newx = this.x + this.width; - newwidth = -this.width; - } - if (this.height < 0) { - newy = this.y + this.height; - newheight = -this.height; - } - this.x = newx; - this.y = newy; - this.width = newwidth; - this.height = newheight; - return this; - }, - - // Offset me by the specified amount. - offset: function(dx, dy) { - - // pretend that this is a point and call offset() - // rewrites x and y according to dx and dy - return Point.prototype.offset.call(this, dx, dy); - }, - - origin: function() { - - return new Point(this.x, this.y); - }, - - // @return {point} a point on my boundary nearest to the given point. - // @see Squeak Smalltalk, Rectangle>>pointNearestTo: - pointNearestToPoint: function(point) { - - point = new Point(point); - if (this.containsPoint(point)) { - var side = this.sideNearestToPoint(point); - switch (side) { - case 'right': - return new Point(this.x + this.width, point.y); - case 'left': - return new Point(this.x, point.y); - case 'bottom': - return new Point(point.x, this.y + this.height); - case 'top': - return new Point(point.x, this.y); - } - } - return point.adhereToRect(this); - }, - - rightLine: function() { - - return new Line(this.topRight(), this.bottomRight()); - }, - - rightMiddle: function() { - - return new Point(this.x + this.width, this.y + this.height / 2); - }, - - round: function(precision) { - - let f = 1; // case 0 - if (precision) { - switch (precision) { - case 1: f = 10; break; - case 2: f = 100; break; - case 3: f = 1000; break; - default: f = pow(10, precision); break; - } - } - - this.x = round(this.x * f) / f; - this.y = round(this.y * f) / f; - this.width = round(this.width * f) / f; - this.height = round(this.height * f) / f; - return this; - }, - - // Scale rectangle with origin. - scale: function(sx, sy, origin) { - - origin = this.origin().scale(sx, sy, origin); - this.x = origin.x; - this.y = origin.y; - this.width *= sx; - this.height *= sy; - return this; - }, - - // @return {string} (left|right|top|bottom) side which is nearest to point - // @see Squeak Smalltalk, Rectangle>>sideNearestTo: - sideNearestToPoint: function(point) { - - point = new Point(point); - var distToLeft = point.x - this.x; - var distToRight = (this.x + this.width) - point.x; - var distToTop = point.y - this.y; - var distToBottom = (this.y + this.height) - point.y; - var closest = distToLeft; - var side = 'left'; - - if (distToRight < closest) { - closest = distToRight; - side = 'right'; - } - if (distToTop < closest) { - closest = distToTop; - side = 'top'; - } - if (distToBottom < closest) { - // closest = distToBottom; - side = 'bottom'; - } - return side; - }, - - snapToGrid: function(gx, gy) { - - var origin = this.origin().snapToGrid(gx, gy); - var corner = this.corner().snapToGrid(gx, gy); - this.x = origin.x; - this.y = origin.y; - this.width = corner.x - origin.x; - this.height = corner.y - origin.y; - return this; - }, - - toJSON: function() { - - return { x: this.x, y: this.y, width: this.width, height: this.height }; - }, - - topLine: function() { - - return new Line(this.topLeft(), this.topRight()); - }, - - topMiddle: function() { - - return new Point(this.x + this.width / 2, this.y); - }, - - topRight: function() { - - return new Point(this.x + this.width, this.y); - }, - - toString: function() { - - return this.origin().toString() + ' ' + this.corner().toString(); - }, - - // @return {rect} representing the union of both rectangles. - union: function(rect) { - - return Rect.fromRectUnion(this, rect); - }, - - update: function(x, y, w, h) { - - if ((Object(x) === x)) { - y = x.y; - w = x.width; - h = x.height; - x = x.x; - } - - this.x = x || 0; - this.y = y || 0; - this.width = w || 0; - this.height = h || 0; - return this; - } -}; - -Rect.prototype.bottomRight = Rect.prototype.corner; - -Rect.prototype.topLeft = Rect.prototype.origin; - -Rect.prototype.translate = Rect.prototype.offset; - -export const scale = { - - // Return the `value` from the `domain` interval scaled to the `range` interval. - linear: function(domain, range, value) { - - var domainSpan = domain[1] - domain[0]; - var rangeSpan = range[1] - range[0]; - return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0; - } -}; - -export const normalizeAngle = function(angle) { - - return (angle % 360) + (angle < 0 ? 360 : 0); -}; - -export const snapToGrid = function(value, gridSize) { - - return gridSize * round(value / gridSize); -}; - -export const toDeg = function(rad) { - - return (180 * rad / PI) % 360; -}; - -export const toRad = function(deg, over360) { - - over360 = over360 || false; - deg = over360 ? deg : (deg % 360); - return deg * PI / 180; -}; - -// Return a random integer from the interval [min,max], inclusive. -export const random = function(min, max) { - - if (max === undefined) { - // use first argument as max, min is 0 - max = (min === undefined) ? 1 : min; - min = 0; - - } else if (max < min) { - // switch max and min - const temp = min; - min = max; - max = temp; - } - - return floor((math.random() * (max - min + 1)) + min); -}; - -// For backwards compatibility: -export const ellipse = Ellipse; -export const line = Line; -export const point = Point; -export const rect = Rect; - -// Local helper function. -// Use an array of arguments to call a constructor (function called with `new`). -// Adapted from https://stackoverflow.com/a/8843181/2263595 -// It is not necessary to use this function if the arguments can be passed separately (i.e. if the number of arguments is limited). -// - If that is the case, use `new constructor(arg1, arg2)`, for example. -// It is not necessary to use this function if the function that needs an array of arguments is not supposed to be used as a constructor. -// - If that is the case, use `f.apply(thisArg, [arg1, arg2...])`, for example. -function applyToNew(constructor, argsArray) { - // The `new` keyword can only be applied to functions that take a limited number of arguments. - // - We can fake that with .bind(). - // - It calls a function (`constructor`, here) with the arguments that were provided to it - effectively transforming an unlimited number of arguments into limited. - // - So `new (constructor.bind(thisArg, arg1, arg2...))` - // - `thisArg` can be anything (e.g. null) because `new` keyword resets context to the constructor object. - // We need to pass in a variable number of arguments to the bind() call. - // - We can use .apply(). - // - So `new (constructor.bind.apply(constructor, [thisArg, arg1, arg2...]))` - // - `thisArg` can still be anything because `new` overwrites it. - // Finally, to make sure that constructor.bind overwriting is not a problem, we switch to `Function.prototype.bind`. - // - So, the final version is `new (Function.prototype.bind.apply(constructor, [thisArg, arg1, arg2...]))` - - // The function expects `argsArray[0]` to be `thisArg`. - // - This means that whatever is sent as the first element will be ignored. - // - The constructor will only see arguments starting from argsArray[1]. - // - So, a new dummy element is inserted at the start of the array. - argsArray.unshift(null); - - return new (Function.prototype.bind.apply(constructor, argsArray)); -} - -// Local helper function. -// Add properties from arguments on top of properties from `obj`. -// This allows for rudimentary inheritance. -// - The `obj` argument acts as parent. -// - This function creates a new object that inherits all `obj` properties and adds/replaces those that are present in arguments. -// - A high-level example: calling `extend(Vehicle, Car)` would be akin to declaring `class Car extends Vehicle`. -function extend(obj) { - // In JavaScript, the combination of a constructor function (e.g. `g.Line = function(...) {...}`) and prototype (e.g. `g.Line.prototype = {...}) is akin to a C++ class. - // - When inheritance is not necessary, we can leave it at that. (This would be akin to calling extend with only `obj`.) - // - But, what if we wanted the `g.Line` quasiclass to inherit from another quasiclass (let's call it `g.GeometryObject`) in JavaScript? - // - First, realize that both of those quasiclasses would still have their own separate constructor function. - // - So what we are actually saying is that we want the `g.Line` prototype to inherit from `g.GeometryObject` prototype. - // - This method provides a way to do exactly that. - // - It copies parent prototype's properties, then adds extra ones from child prototype/overrides parent prototype properties with child prototype properties. - // - Therefore, to continue with the example above: - // - `g.Line.prototype = extend(g.GeometryObject.prototype, linePrototype)` - // - Where `linePrototype` is a properties object that looks just like `g.Line.prototype` does right now. - // - Then, `g.Line` would allow the programmer to access to all methods currently in `g.Line.Prototype`, plus any non-overridden methods from `g.GeometryObject.prototype`. - // - In that aspect, `g.GeometryObject` would then act like the parent of `g.Line`. - // - Multiple inheritance is also possible, if multiple arguments are provided. - // - What if we wanted to add another level of abstraction between `g.GeometryObject` and `g.Line` (let's call it `g.LinearObject`)? - // - `g.Line.prototype = extend(g.GeometryObject.prototype, g.LinearObject.prototype, linePrototype)` - // - The ancestors are applied in order of appearance. - // - That means that `g.Line` would have inherited from `g.LinearObject` that would have inherited from `g.GeometryObject`. - // - Any number of ancestors may be provided. - // - Note that neither `obj` nor any of the arguments need to actually be prototypes of any JavaScript quasiclass, that was just a simplified explanation. - // - We can create a new object composed from the properties of any number of other objects (since they do not have a constructor, we can think of those as interfaces). - // - `extend({ a: 1, b: 2 }, { b: 10, c: 20 }, { c: 100, d: 200 })` gives `{ a: 1, b: 10, c: 100, d: 200 }`. - // - Basically, with this function, we can emulate the `extends` keyword as well as the `implements` keyword. - // - Therefore, both of the following are valid: - // - `Lineto.prototype = extend(Line.prototype, segmentPrototype, linetoPrototype)` - // - `Moveto.prototype = extend(segmentPrototype, movetoPrototype)` - - var i; - var n; - - var args = []; - n = arguments.length; - for (i = 1; i < n; i++) { // skip over obj - args.push(arguments[i]); - } - - if (!obj) throw new Error('Missing a parent object.'); - var child = Object.create(obj); - - n = args.length; - for (i = 0; i < n; i++) { - - var src = args[i]; - - var inheritedProperty; - var key; - for (key in src) { - - if (src.hasOwnProperty(key)) { - delete child[key]; // delete property inherited from parent - inheritedProperty = Object.getOwnPropertyDescriptor(src, key); // get new definition of property from src - Object.defineProperty(child, key, inheritedProperty); // re-add property with new definition (includes getter/setter methods) - } - } - } - - return child; -} - -// Path segment interface: -var segmentPrototype = { - - // virtual - bbox: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - clone: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - closestPoint: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - closestPointLength: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - closestPointNormalizedLength: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // Redirect calls to closestPointNormalizedLength() function if closestPointT() is not defined for segment. - closestPointT: function(p) { - - if (this.closestPointNormalizedLength) return this.closestPointNormalizedLength(p); - - throw new Error('Neither closestPointT() nor closestPointNormalizedLength() function is implemented.'); - }, - - // virtual - closestPointTangent: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - divideAt: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - divideAtLength: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // Redirect calls to divideAt() function if divideAtT() is not defined for segment. - divideAtT: function(t) { - - if (this.divideAt) return this.divideAt(t); - - throw new Error('Neither divideAtT() nor divideAt() function is implemented.'); - }, - - // virtual - equals: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - getSubdivisions: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - isDifferentiable: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - isSegment: true, - - isSubpathStart: false, // true for Moveto segments - - isVisible: true, // false for Moveto segments - - // virtual - length: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // Return a fraction of result of length() function if lengthAtT() is not defined for segment. - lengthAtT: function(t) { - - if (t <= 0) return 0; - - var length = this.length(); - - if (t >= 1) return length; - - return length * t; - }, - - nextSegment: null, // needed for subpath start segment updating - - // virtual - pointAt: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - pointAtLength: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // Redirect calls to pointAt() function if pointAtT() is not defined for segment. - pointAtT: function(t) { - - if (this.pointAt) return this.pointAt(t); - - throw new Error('Neither pointAtT() nor pointAt() function is implemented.'); - }, - - previousSegment: null, // needed to get segment start property - - // virtual - round: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - subpathStartSegment: null, // needed to get Closepath segment end property - - // virtual - scale: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - serialize: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - tangentAt: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - tangentAtLength: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // Redirect calls to tangentAt() function if tangentAtT() is not defined for segment. - tangentAtT: function(t) { - - if (this.tangentAt) return this.tangentAt(t); - - throw new Error('Neither tangentAtT() nor tangentAt() function is implemented.'); - }, - - // virtual - toString: function() { - - throw new Error('Declaration missing for virtual function.'); - }, - - // virtual - translate: function() { - - throw new Error('Declaration missing for virtual function.'); - } -}; - -// usually directly assigned -// getter for Closepath -Object.defineProperty(segmentPrototype, 'end', { - - configurable: true, - - enumerable: true, - - writable: true -}); - -// always a getter -// always throws error for Moveto -Object.defineProperty(segmentPrototype, 'start', { - // get a reference to the end point of previous segment - - configurable: true, - - enumerable: true, - - get: function() { - - if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - - return this.previousSegment.end; - } -}); - -// virtual -Object.defineProperty(segmentPrototype, 'type', { - - configurable: true, - - enumerable: true, - - get: function() { - - throw new Error('Bad segment declaration. No type specified.'); - } -}); - -// Path segment implementations: -var Lineto = function() { - - var args = []; - var n = arguments.length; - for (var i = 0; i < n; i++) { - args.push(arguments[i]); - } - - if (!(this instanceof Lineto)) { // switching context of `this` to Lineto when called without `new` - return applyToNew(Lineto, args); - } - - if (n === 0) { - throw new Error('Lineto constructor expects a line, 1 point, or 2 coordinates (none provided).'); - } - - var outputArray; - - if (args[0] instanceof Line) { // lines provided - if (n === 1) { - this.end = args[0].end.clone(); - return this; - - } else { - throw new Error('Lineto constructor expects a line, 1 point, or 2 coordinates (' + n + ' lines provided).'); - } - - } else if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided - if (n === 2) { - this.end = new Point(+args[0], +args[1]); - return this; - - } else if (n < 2) { - throw new Error('Lineto constructor expects a line, 1 point, or 2 coordinates (' + n + ' coordinates provided).'); - - } else { // this is a poly-line segment - var segmentCoords; - outputArray = []; - for (i = 0; i < n; i += 2) { // coords come in groups of two - - segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 - outputArray.push(applyToNew(Lineto, segmentCoords)); - } - return outputArray; - } - - } else { // points provided (needs to be last to also cover plain objects with x and y) - if (n === 1) { - this.end = new Point(args[0]); - return this; - - } else { // this is a poly-line segment - var segmentPoint; - outputArray = []; - for (i = 0; i < n; i += 1) { - - segmentPoint = args[i]; - outputArray.push(new Lineto(segmentPoint)); - } - return outputArray; - } - } -}; - -var linetoPrototype = { - - clone: function() { - - return new Lineto(this.end); - }, - - divideAt: function(ratio) { - - var line = new Line(this.start, this.end); - var divided = line.divideAt(ratio); - return [ - new Lineto(divided[0]), - new Lineto(divided[1]) - ]; - }, - - divideAtLength: function(length) { - - var line = new Line(this.start, this.end); - var divided = line.divideAtLength(length); - return [ - new Lineto(divided[0]), - new Lineto(divided[1]) - ]; - }, - - getSubdivisions: function() { - - return []; - }, - - isDifferentiable: function() { - - if (!this.previousSegment) return false; - - return !this.start.equals(this.end); - }, - - round: function(precision) { - - this.end.round(precision); - return this; - }, - - scale: function(sx, sy, origin) { - - this.end.scale(sx, sy, origin); - return this; - }, - - serialize: function() { - - var end = this.end; - return this.type + ' ' + end.x + ' ' + end.y; - }, - - toString: function() { - - return this.type + ' ' + this.start + ' ' + this.end; - }, - - translate: function(tx, ty) { - - this.end.translate(tx, ty); - return this; - } -}; - -Object.defineProperty(linetoPrototype, 'type', { - - configurable: true, - - enumerable: true, - - value: 'L' -}); - -Lineto.prototype = extend(segmentPrototype, Line.prototype, linetoPrototype); - -var Curveto = function() { - - var args = []; - var n = arguments.length; - for (var i = 0; i < n; i++) { - args.push(arguments[i]); - } - - if (!(this instanceof Curveto)) { // switching context of `this` to Curveto when called without `new` - return applyToNew(Curveto, args); - } - - if (n === 0) { - throw new Error('Curveto constructor expects a curve, 3 points, or 6 coordinates (none provided).'); - } - - var outputArray; - - if (args[0] instanceof Curve) { // curves provided - if (n === 1) { - this.controlPoint1 = args[0].controlPoint1.clone(); - this.controlPoint2 = args[0].controlPoint2.clone(); - this.end = args[0].end.clone(); - return this; - - } else { - throw new Error('Curveto constructor expects a curve, 3 points, or 6 coordinates (' + n + ' curves provided).'); - } - - } else if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided - if (n === 6) { - this.controlPoint1 = new Point(+args[0], +args[1]); - this.controlPoint2 = new Point(+args[2], +args[3]); - this.end = new Point(+args[4], +args[5]); - return this; - - } else if (n < 6) { - throw new Error('Curveto constructor expects a curve, 3 points, or 6 coordinates (' + n + ' coordinates provided).'); - - } else { // this is a poly-bezier segment - var segmentCoords; - outputArray = []; - for (i = 0; i < n; i += 6) { // coords come in groups of six - - segmentCoords = args.slice(i, i + 6); // will send fewer than six coords if args.length not divisible by 6 - outputArray.push(applyToNew(Curveto, segmentCoords)); - } - return outputArray; - } - - } else { // points provided (needs to be last to also cover plain objects with x and y) - if (n === 3) { - this.controlPoint1 = new Point(args[0]); - this.controlPoint2 = new Point(args[1]); - this.end = new Point(args[2]); - return this; - - } else if (n < 3) { - throw new Error('Curveto constructor expects a curve, 3 points, or 6 coordinates (' + n + ' points provided).'); - - } else { // this is a poly-bezier segment - var segmentPoints; - outputArray = []; - for (i = 0; i < n; i += 3) { // points come in groups of three - - segmentPoints = args.slice(i, i + 3); // will send fewer than three points if args.length is not divisible by 3 - outputArray.push(applyToNew(Curveto, segmentPoints)); - } - return outputArray; - } - } -}; - -var curvetoPrototype = { - - clone: function() { - - return new Curveto(this.controlPoint1, this.controlPoint2, this.end); - }, - - divideAt: function(ratio, opt) { - - var curve = new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); - var divided = curve.divideAt(ratio, opt); - return [ - new Curveto(divided[0]), - new Curveto(divided[1]) - ]; - }, - - divideAtLength: function(length, opt) { - - var curve = new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); - var divided = curve.divideAtLength(length, opt); - return [ - new Curveto(divided[0]), - new Curveto(divided[1]) - ]; - }, - - divideAtT: function(t) { - - var curve = new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); - var divided = curve.divideAtT(t); - return [ - new Curveto(divided[0]), - new Curveto(divided[1]) - ]; - }, - - isDifferentiable: function() { - - if (!this.previousSegment) return false; - - var start = this.start; - var control1 = this.controlPoint1; - var control2 = this.controlPoint2; - var end = this.end; - - return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); - }, - - round: function(precision) { - - this.controlPoint1.round(precision); - this.controlPoint2.round(precision); - this.end.round(precision); - return this; - }, - - scale: function(sx, sy, origin) { - - this.controlPoint1.scale(sx, sy, origin); - this.controlPoint2.scale(sx, sy, origin); - this.end.scale(sx, sy, origin); - return this; - }, - - serialize: function() { - - var c1 = this.controlPoint1; - var c2 = this.controlPoint2; - var end = this.end; - return this.type + ' ' + c1.x + ' ' + c1.y + ' ' + c2.x + ' ' + c2.y + ' ' + end.x + ' ' + end.y; - }, - - toString: function() { - - return this.type + ' ' + this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; - }, - - translate: function(tx, ty) { - - this.controlPoint1.translate(tx, ty); - this.controlPoint2.translate(tx, ty); - this.end.translate(tx, ty); - return this; - } -}; - -Object.defineProperty(curvetoPrototype, 'type', { - - configurable: true, - - enumerable: true, - - value: 'C' -}); - -Curveto.prototype = extend(segmentPrototype, Curve.prototype, curvetoPrototype); - -var Moveto = function() { - - var args = []; - var n = arguments.length; - for (var i = 0; i < n; i++) { - args.push(arguments[i]); - } - - if (!(this instanceof Moveto)) { // switching context of `this` to Moveto when called without `new` - return applyToNew(Moveto, args); - } - - if (n === 0) { - throw new Error('Moveto constructor expects a line, a curve, 1 point, or 2 coordinates (none provided).'); - } - - var outputArray; - - if (args[0] instanceof Line) { // lines provided - if (n === 1) { - this.end = args[0].end.clone(); - return this; - - } else { - throw new Error('Moveto constructor expects a line, a curve, 1 point, or 2 coordinates (' + n + ' lines provided).'); - } - - } else if (args[0] instanceof Curve) { // curves provided - if (n === 1) { - this.end = args[0].end.clone(); - return this; - - } else { - throw new Error('Moveto constructor expects a line, a curve, 1 point, or 2 coordinates (' + n + ' curves provided).'); - } - - } else if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided - if (n === 2) { - this.end = new Point(+args[0], +args[1]); - return this; - - } else if (n < 2) { - throw new Error('Moveto constructor expects a line, a curve, 1 point, or 2 coordinates (' + n + ' coordinates provided).'); - - } else { // this is a moveto-with-subsequent-poly-line segment - var segmentCoords; - outputArray = []; - for (i = 0; i < n; i += 2) { // coords come in groups of two - - segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 - if (i === 0) outputArray.push(applyToNew(Moveto, segmentCoords)); - else outputArray.push(applyToNew(Lineto, segmentCoords)); - } - return outputArray; - } - - } else { // points provided (needs to be last to also cover plain objects with x and y) - if (n === 1) { - this.end = new Point(args[0]); - return this; - - } else { // this is a moveto-with-subsequent-poly-line segment - var segmentPoint; - outputArray = []; - for (i = 0; i < n; i += 1) { // points come one by one - - segmentPoint = args[i]; - if (i === 0) outputArray.push(new Moveto(segmentPoint)); - else outputArray.push(new Lineto(segmentPoint)); - } - return outputArray; - } - } -}; - -var movetoPrototype = { - - bbox: function() { - - return null; - }, - - clone: function() { - - return new Moveto(this.end); - }, - - closestPoint: function() { - - return this.end.clone(); - }, - - closestPointNormalizedLength: function() { - - return 0; - }, - - closestPointLength: function() { - - return 0; - }, - - closestPointT: function() { - - return 1; - }, - - closestPointTangent: function() { - - return null; - }, - - divideAt: function() { - - return [ - this.clone(), - this.clone() - ]; - }, - - divideAtLength: function() { - - return [ - this.clone(), - this.clone() - ]; - }, - - equals: function(m) { - - return this.end.equals(m.end); - }, - - getSubdivisions: function() { - - return []; - }, - - isDifferentiable: function() { - - return false; - }, - - isSubpathStart: true, - - isVisible: false, - - length: function() { - - return 0; - }, - - lengthAtT: function() { - - return 0; - }, - - pointAt: function() { - - return this.end.clone(); - }, - - pointAtLength: function() { - - return this.end.clone(); - }, - - pointAtT: function() { - - return this.end.clone(); - }, - - round: function(precision) { - - this.end.round(precision); - return this; - }, - - scale: function(sx, sy, origin) { - - this.end.scale(sx, sy, origin); - return this; - }, - - serialize: function() { - - var end = this.end; - return this.type + ' ' + end.x + ' ' + end.y; - }, - - tangentAt: function() { - - return null; - }, - - tangentAtLength: function() { - - return null; - }, - - tangentAtT: function() { - - return null; - }, - - toString: function() { - - return this.type + ' ' + this.end; - }, - - translate: function(tx, ty) { - - this.end.translate(tx, ty); - return this; - } -}; - -Object.defineProperty(movetoPrototype, 'start', { - - configurable: true, - - enumerable: true, - - get: function() { - - throw new Error('Illegal access. Moveto segments should not need a start property.'); - } -}); - -Object.defineProperty(movetoPrototype, 'type', { - - configurable: true, - - enumerable: true, - - value: 'M' -}); - -Moveto.prototype = extend(segmentPrototype, movetoPrototype); // does not inherit from any other geometry object - -var Closepath = function() { - - var args = []; - var n = arguments.length; - for (var i = 0; i < n; i++) { - args.push(arguments[i]); - } - - if (!(this instanceof Closepath)) { // switching context of `this` to Closepath when called without `new` - return applyToNew(Closepath, args); - } - - if (n > 0) { - throw new Error('Closepath constructor expects no arguments.'); - } - - return this; -}; - -var closepathPrototype = { - - clone: function() { - - return new Closepath(); - }, - - divideAt: function(ratio) { - - var line = new Line(this.start, this.end); - var divided = line.divideAt(ratio); - return [ - // if we didn't actually cut into the segment, first divided part can stay as Z - (divided[1].isDifferentiable() ? new Lineto(divided[0]) : this.clone()), - new Lineto(divided[1]) - ]; - }, - - divideAtLength: function(length) { - - var line = new Line(this.start, this.end); - var divided = line.divideAtLength(length); - return [ - // if we didn't actually cut into the segment, first divided part can stay as Z - (divided[1].isDifferentiable() ? new Lineto(divided[0]) : this.clone()), - new Lineto(divided[1]) - ]; - }, - - getSubdivisions: function() { - - return []; - }, - - isDifferentiable: function() { - - if (!this.previousSegment || !this.subpathStartSegment) return false; - - return !this.start.equals(this.end); - }, - - round: function() { - - return this; - }, - - scale: function() { - - return this; - }, - - serialize: function() { - - return this.type; - }, - - toString: function() { - - return this.type + ' ' + this.start + ' ' + this.end; - }, - - translate: function() { - - return this; - } -}; - -Object.defineProperty(closepathPrototype, 'end', { - // get a reference to the end point of subpath start segment - - configurable: true, - - enumerable: true, - - get: function() { - - if (!this.subpathStartSegment) throw new Error('Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)'); - - return this.subpathStartSegment.end; - } -}); - -Object.defineProperty(closepathPrototype, 'type', { - - configurable: true, - - enumerable: true, - - value: 'Z' -}); - -Closepath.prototype = extend(segmentPrototype, Line.prototype, closepathPrototype); - -var segmentTypes = Path.segmentTypes = { - L: Lineto, - C: Curveto, - M: Moveto, - Z: Closepath, - z: Closepath -}; - -Path.regexSupportedData = new RegExp('^[\\s\\d' + Object.keys(segmentTypes).join('') + ',.]*$'); - -Path.isDataSupported = function(data) { - - if (typeof data !== 'string') return false; - return this.regexSupportedData.test(data); -}; - +export * from './geometry.helpers.mjs'; +export * from './bezier.mjs'; +export * from './curve.mjs'; +export * from './ellipse.mjs'; +export * from './line.mjs'; +export * from './path.mjs'; +export * from './point.mjs'; +export * from './polyline.mjs'; +export * from './rect.mjs'; diff --git a/src/g/line.bearing.mjs b/src/g/line.bearing.mjs new file mode 100644 index 0000000000..76b410c68a --- /dev/null +++ b/src/g/line.bearing.mjs @@ -0,0 +1,29 @@ +// @return the bearing (cardinal direction) of the line. For example N, W, or SE. +// @returns {String} One of the following bearings : NE, E, SE, S, SW, W, NW, N. +import { toDeg, toRad } from './geometry.helpers.mjs'; + +const math = Math; +const cos = math.cos; +const sin = math.sin; +const atan2 = math.atan2; + +export const bearing = function(p, q) { + + var lat1 = toRad(p.y); + var lat2 = toRad(q.y); + var lon1 = p.x; + var lon2 = q.x; + var dLon = toRad(lon2 - lon1); + var y = sin(dLon) * cos(lat2); + var x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon); + var brng = toDeg(atan2(y, x)); + + var bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N']; + + var index = brng - 22.5; + if (index < 0) + index += 360; + index = parseInt(index / 45); + + return bearings[index]; +}; diff --git a/src/g/line.length.mjs b/src/g/line.length.mjs new file mode 100644 index 0000000000..b3a37d3077 --- /dev/null +++ b/src/g/line.length.mjs @@ -0,0 +1,5 @@ +import { squaredLength } from './line.squaredLength.mjs'; + +export const length = function(start, end) { + return Math.sqrt(squaredLength(start, end)); +}; diff --git a/src/g/line.mjs b/src/g/line.mjs new file mode 100644 index 0000000000..6d70976169 --- /dev/null +++ b/src/g/line.mjs @@ -0,0 +1,352 @@ +import { Point } from './point.mjs'; +import { Rect } from './rect.mjs'; +import { bearing } from './line.bearing.mjs'; +import { squaredLength } from './line.squaredLength.mjs'; +import { length } from './line.length.mjs'; + +const math = Math; +const min = math.min; +const max = math.max; + +export const Line = function(p1, p2) { + + if (!(this instanceof Line)) { + return new Line(p1, p2); + } + + if (p1 instanceof Line) { + return new Line(p1.start, p1.end); + } + + this.start = new Point(p1); + this.end = new Point(p2); +}; + +Line.prototype = { + + // @returns the angle of incline of the line. + angle: function() { + + var horizontalPoint = new Point(this.start.x + 1, this.start.y); + return this.start.angleBetween(this.end, horizontalPoint); + }, + + bbox: function() { + + var left = min(this.start.x, this.end.x); + var top = min(this.start.y, this.end.y); + var right = max(this.start.x, this.end.x); + var bottom = max(this.start.y, this.end.y); + + return new Rect(left, top, (right - left), (bottom - top)); + }, + + // @return the bearing (cardinal direction) of the line. For example N, W, or SE. + // @returns {String} One of the following bearings : NE, E, SE, S, SW, W, NW, N. + bearing: function() { + return bearing(this.start, this.end); + }, + + clone: function() { + + return new Line(this.start, this.end); + }, + + // @return {point} the closest point on the line to point `p` + closestPoint: function(p) { + + return this.pointAt(this.closestPointNormalizedLength(p)); + }, + + closestPointLength: function(p) { + + return this.closestPointNormalizedLength(p) * this.length(); + }, + + // @return {number} the normalized length of the closest point on the line to point `p` + closestPointNormalizedLength: function(p) { + + var product = this.vector().dot((new Line(this.start, p)).vector()); + var cpNormalizedLength = min(1, max(0, product / this.squaredLength())); + + // cpNormalizedLength returns `NaN` if this line has zero length + // we can work with that - if `NaN`, return 0 + if (cpNormalizedLength !== cpNormalizedLength) return 0; // condition evaluates to `true` if and only if cpNormalizedLength is `NaN` + // (`NaN` is the only value that is not equal to itself) + + return cpNormalizedLength; + }, + + closestPointTangent: function(p) { + + return this.tangentAt(this.closestPointNormalizedLength(p)); + }, + + // Returns `true` if the point lies on the line. + containsPoint: function(p) { + + var start = this.start; + var end = this.end; + + if (start.cross(p, end) !== 0) return false; + // else: cross product of 0 indicates that this line and the vector to `p` are collinear + + var length = this.length(); + if ((new Line(start, p)).length() > length) return false; + if ((new Line(p, end)).length() > length) return false; + // else: `p` lies between start and end of the line + + return true; + }, + + // Divides the line into two at requested `ratio` between 0 and 1. + divideAt: function(ratio) { + + var dividerPoint = this.pointAt(ratio); + + // return array with two lines + return [ + new Line(this.start, dividerPoint), + new Line(dividerPoint, this.end) + ]; + }, + + // Divides the line into two at requested `length`. + divideAtLength: function(length) { + + var dividerPoint = this.pointAtLength(length); + + // return array with two new lines + return [ + new Line(this.start, dividerPoint), + new Line(dividerPoint, this.end) + ]; + }, + + equals: function(l) { + + return !!l && + this.start.x === l.start.x && + this.start.y === l.start.y && + this.end.x === l.end.x && + this.end.y === l.end.y; + }, + + // @return {point} Point where I'm intersecting a line. + // @return [point] Points where I'm intersecting a rectangle. + // @see Squeak Smalltalk, LineSegment>>intersectionWith: + intersect: function(shape, opt) { + + if (shape && shape.intersectionWithLine) { + var intersection = shape.intersectionWithLine(this, opt); + + // Backwards compatibility + if (intersection && (shape instanceof Line)) { + intersection = intersection[0]; + } + + return intersection; + } + + return null; + }, + + intersectionWithLine: function(line) { + + var pt1Dir = new Point(this.end.x - this.start.x, this.end.y - this.start.y); + var pt2Dir = new Point(line.end.x - line.start.x, line.end.y - line.start.y); + var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x); + var deltaPt = new Point(line.start.x - this.start.x, line.start.y - this.start.y); + var alpha = (deltaPt.x * pt2Dir.y) - (deltaPt.y * pt2Dir.x); + var beta = (deltaPt.x * pt1Dir.y) - (deltaPt.y * pt1Dir.x); + + if (det === 0 || alpha * det < 0 || beta * det < 0) { + // No intersection found. + return null; + } + + if (det > 0) { + if (alpha > det || beta > det) { + return null; + } + + } else { + if (alpha < det || beta < det) { + return null; + } + } + + return [new Point( + this.start.x + (alpha * pt1Dir.x / det), + this.start.y + (alpha * pt1Dir.y / det) + )]; + }, + + isDifferentiable: function() { + + return !this.start.equals(this.end); + }, + + // @return {double} length of the line + length: function() { + return length(this.start, this.end); + }, + + // @return {point} my midpoint + midpoint: function() { + + return new Point( + (this.start.x + this.end.x) / 2, + (this.start.y + this.end.y) / 2 + ); + }, + + parallel: function(distance) { + const l = this.clone(); + if (!this.isDifferentiable()) return l; + const { start, end } = l; + const eRef = start.clone().rotate(end, 270); + const sRef = end.clone().rotate(start, 90); + start.move(sRef, distance); + end.move(eRef, distance); + return l; + }, + + // @return {point} my point at 't' <0,1> + pointAt: function(t) { + + var start = this.start; + var end = this.end; + + if (t <= 0) return start.clone(); + if (t >= 1) return end.clone(); + + return start.lerp(end, t); + }, + + pointAtLength: function(length) { + + var start = this.start; + var end = this.end; + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + var lineLength = this.length(); + if (length >= lineLength) return (fromStart ? end.clone() : start.clone()); + + return this.pointAt((fromStart ? (length) : (lineLength - length)) / lineLength); + }, + + // @return {number} the offset of the point `p` from the line. + if the point `p` is on the right side of the line, - if on the left and 0 if on the line. + pointOffset: function(p) { + + // Find the sign of the determinant of vectors (start,end), where p is the query point. + p = new Point(p); + var start = this.start; + var end = this.end; + var determinant = ((end.x - start.x) * (p.y - start.y) - (end.y - start.y) * (p.x - start.x)); + + return determinant / this.length(); + }, + + rotate: function(origin, angle) { + + this.start.rotate(origin, angle); + this.end.rotate(origin, angle); + return this; + }, + + round: function(precision) { + + this.start.round(precision); + this.end.round(precision); + return this; + }, + + scale: function(sx, sy, origin) { + + this.start.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, + + // @return {number} scale the line so that it has the requested length + setLength: function(length) { + + var currentLength = this.length(); + if (!currentLength) return this; + + var scaleFactor = length / currentLength; + return this.scale(scaleFactor, scaleFactor, this.start); + }, + + // @return {integer} length without sqrt + // @note for applications where the exact length is not necessary (e.g. compare only) + squaredLength: function() { + return squaredLength(this.start, this.end); + }, + + tangentAt: function(t) { + + if (!this.isDifferentiable()) return null; + + var start = this.start; + var end = this.end; + + var tangentStart = this.pointAt(t); // constrains `t` between 0 and 1 + + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested + + return tangentLine; + }, + + tangentAtLength: function(length) { + + if (!this.isDifferentiable()) return null; + + var start = this.start; + var end = this.end; + + var tangentStart = this.pointAtLength(length); + + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested + + return tangentLine; + }, + + toString: function() { + + return this.start.toString() + ' ' + this.end.toString(); + }, + + serialize: function() { + + return this.start.serialize() + ' ' + this.end.serialize(); + }, + + translate: function(tx, ty) { + + this.start.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, + + // @return vector {point} of the line + vector: function() { + + return new Point(this.end.x - this.start.x, this.end.y - this.start.y); + } +}; + +// For backwards compatibility: +Line.prototype.intersection = Line.prototype.intersect; + + +// For backwards compatibility: +export const line = Line; diff --git a/src/g/line.squaredLength.mjs b/src/g/line.squaredLength.mjs new file mode 100644 index 0000000000..38c5b23e71 --- /dev/null +++ b/src/g/line.squaredLength.mjs @@ -0,0 +1,10 @@ +// @return {integer} length without sqrt +// @note for applications where the exact length is not necessary (e.g. compare only) +export const squaredLength = function(start, end) { + + var x0 = start.x; + var y0 = start.y; + var x1 = end.x; + var y1 = end.y; + return (x0 -= x1) * x0 + (y0 -= y1) * y0; +}; diff --git a/src/g/path.mjs b/src/g/path.mjs new file mode 100644 index 0000000000..8df11c9df5 --- /dev/null +++ b/src/g/path.mjs @@ -0,0 +1,2322 @@ +// Accepts path data string, array of segments, array of Curves and/or Lines, or a Polyline. +// Path created is not guaranteed to be a valid (serializable) path (might not start with an M). +import { Polyline } from './polyline.mjs'; +import { Rect } from './rect.mjs'; +import { Point } from './point.mjs'; +import { Line } from './line.mjs'; +import { Curve } from './curve.mjs'; + +export const Path = function(arg) { + + if (!(this instanceof Path)) { + return new Path(arg); + } + + if (typeof arg === 'string') { // create from a path data string + return new Path.parse(arg); + } + + this.segments = []; + + var i; + var n; + + if (!arg) { + // don't do anything + + } else if (Array.isArray(arg) && arg.length !== 0) { // if arg is a non-empty array + // flatten one level deep + // so we can chain arbitrary Path.createSegment results + arg = arg.reduce(function(acc, val) { + return acc.concat(val); + }, []); + + n = arg.length; + if (arg[0].isSegment) { // create from an array of segments + for (i = 0; i < n; i++) { + + var segment = arg[i]; + + this.appendSegment(segment); + } + + } else { // create from an array of Curves and/or Lines + var previousObj = null; + for (i = 0; i < n; i++) { + + var obj = arg[i]; + + if (!((obj instanceof Line) || (obj instanceof Curve))) { + throw new Error('Cannot construct a path segment from the provided object.'); + } + + if (i === 0) this.appendSegment(Path.createSegment('M', obj.start)); + + // if objects do not link up, moveto segments are inserted to cover the gaps + if (previousObj && !previousObj.end.equals(obj.start)) this.appendSegment(Path.createSegment('M', obj.start)); + + if (obj instanceof Line) { + this.appendSegment(Path.createSegment('L', obj.end)); + + } else if (obj instanceof Curve) { + this.appendSegment(Path.createSegment('C', obj.controlPoint1, obj.controlPoint2, obj.end)); + } + + previousObj = obj; + } + } + + } else if (arg.isSegment) { // create from a single segment + this.appendSegment(arg); + + } else if (arg instanceof Line) { // create from a single Line + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('L', arg.end)); + + } else if (arg instanceof Curve) { // create from a single Curve + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('C', arg.controlPoint1, arg.controlPoint2, arg.end)); + + } else if (arg instanceof Polyline) { // create from a Polyline + if (!(arg.points && (arg.points.length !== 0))) return; // if Polyline has no points, leave Path empty + + n = arg.points.length; + for (i = 0; i < n; i++) { + + var point = arg.points[i]; + + if (i === 0) this.appendSegment(Path.createSegment('M', point)); + else this.appendSegment(Path.createSegment('L', point)); + } + + } else { // unknown object + throw new Error('Cannot construct a path from the provided object.'); + } +}; + +// More permissive than V.normalizePathData and Path.prototype.serialize. +// Allows path data strings that do not start with a Moveto command (unlike SVG specification). +// Does not require spaces between elements; commas are allowed, separators may be omitted when unambiguous (e.g. 'ZM10,10', 'L1.6.8', 'M100-200'). +// Allows for command argument chaining. +// Throws an error if wrong number of arguments is provided with a command. +// Throws an error if an unrecognized path command is provided (according to Path.segmentTypes). Only a subset of SVG commands is currently supported (L, C, M, Z). +Path.parse = function(pathData) { + + if (!pathData) return new Path(); + + var path = new Path(); + + var commandRe = /(?:[a-zA-Z] *)(?:(?:-?\d+(?:\.\d+)?(?:e[-+]?\d+)? *,? *)|(?:-?\.\d+ *,? *))+|(?:[a-zA-Z] *)(?! |\d|-|\.)/g; + var commands = pathData.match(commandRe); + + var numCommands = commands.length; + for (var i = 0; i < numCommands; i++) { + + var command = commands[i]; + var argRe = /(?:[a-zA-Z])|(?:(?:-?\d+(?:\.\d+)?(?:e[-+]?\d+)?))|(?:(?:-?\.\d+))/g; + var args = command.match(argRe); + + var segment = Path.createSegment.apply(this, args); // args = [type, coordinate1, coordinate2...] + path.appendSegment(segment); + } + + return path; +}; + +// Create a segment or an array of segments. +// Accepts unlimited points/coords arguments after `type`. +Path.createSegment = function(type) { + + if (!type) throw new Error('Type must be provided.'); + + var segmentConstructor = Path.segmentTypes[type]; + if (!segmentConstructor) throw new Error(type + ' is not a recognized path segment type.'); + + var args = []; + var n = arguments.length; + for (var i = 1; i < n; i++) { // do not add first element (`type`) to args array + args.push(arguments[i]); + } + + return applyToNew(segmentConstructor, args); +}; + +Path.prototype = { + + // Accepts one segment or an array of segments as argument. + // Throws an error if argument is not a segment or an array of segments. + appendSegment: function(arg) { + + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + var currentSegment; + + var previousSegment = ((numSegments !== 0) ? segments[numSegments - 1] : null); // if we are appending to an empty path, previousSegment is null + var nextSegment = null; + + if (!Array.isArray(arg)) { // arg is a segment + if (!arg || !arg.isSegment) throw new Error('Segment required.'); + + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.push(currentSegment); + + } else { // arg is an array of segments + // flatten one level deep + // so we can chain arbitrary Path.createSegment results + arg = arg.reduce(function(acc, val) { + return acc.concat(val); + }, []); + + if (!arg[0].isSegment) throw new Error('Segments required.'); + + var n = arg.length; + for (var i = 0; i < n; i++) { + + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.push(currentSegment); + previousSegment = currentSegment; + } + } + }, + + // Returns the bbox of the path. + // If path has no segments, returns null. + // If path has only invisible segments, returns bbox of the end point of last segment. + bbox: function() { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var bbox; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + if (segment.isVisible) { + var segmentBBox = segment.bbox(); + bbox = bbox ? bbox.union(segmentBBox) : segmentBBox; + } + } + + if (bbox) return bbox; + + // if the path has only invisible elements, return end point of last segment + var lastSegment = segments[numSegments - 1]; + return new Rect(lastSegment.end.x, lastSegment.end.y, 0, 0); + }, + + // Returns a new path that is a clone of this path. + clone: function() { + + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + var path = new Path(); + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i].clone(); + path.appendSegment(segment); + } + + return path; + }, + + closestPoint: function(p, opt) { + + var t = this.closestPointT(p, opt); + if (!t) return null; + + return this.pointAtT(t); + }, + + closestPointLength: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var t = this.closestPointT(p, localOpt); + if (!t) return 0; + + return this.lengthAtT(t, localOpt); + }, + + closestPointNormalizedLength: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var cpLength = this.closestPointLength(p, localOpt); + if (cpLength === 0) return 0; // shortcut + + var length = this.length(localOpt); + if (length === 0) return 0; // prevents division by zero + + return cpLength / length; + }, + + // Private function. + closestPointT: function(p, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var closestPointT; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + + if (segment.isVisible) { + var segmentClosestPointT = segment.closestPointT(p, { + precision: precision, + subdivisions: subdivisions + }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); + + if (squaredDistance < minSquaredDistance) { + closestPointT = { segmentIndex: i, value: segmentClosestPointT }; + minSquaredDistance = squaredDistance; + } + } + } + + if (closestPointT) return closestPointT; + + // if no visible segment, return end of last segment + return { segmentIndex: numSegments - 1, value: 1 }; + }, + + closestPointTangent: function(p, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var closestPointTangent; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + + if (segment.isDifferentiable()) { + var segmentClosestPointT = segment.closestPointT(p, { + precision: precision, + subdivisions: subdivisions + }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); + + if (squaredDistance < minSquaredDistance) { + closestPointTangent = segment.tangentAtT(segmentClosestPointT); + minSquaredDistance = squaredDistance; + } + } + } + + if (closestPointTangent) return closestPointTangent; + + // if no valid segment, return null + return null; + }, + + // Returns `true` if the area surrounded by the path contains the point `p`. + // Implements the even-odd algorithm (self-intersections are "outside"). + // Closes open paths (always imagines a final closing segment). + // Precision may be adjusted by passing an `opt` object. + containsPoint: function(p, opt) { + + var polylines = this.toPolylines(opt); + if (!polylines) return false; // shortcut (this path has no polylines) + + var numPolylines = polylines.length; + + // how many component polylines does `p` lie within? + var numIntersections = 0; + for (var i = 0; i < numPolylines; i++) { + var polyline = polylines[i]; + if (polyline.containsPoint(p)) { + // `p` lies within this polyline + numIntersections++; + } + } + + // returns `true` for odd numbers of intersections (even-odd algorithm) + return ((numIntersections % 2) === 1); + }, + + // Divides the path into two at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. + divideAt: function(ratio, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var pathLength = this.length(localOpt); + var length = pathLength * ratio; + + return this.divideAtLength(length, localOpt); + }, + + // Divides the path into two at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + divideAtLength: function(length, opt) { + + var numSegments = this.segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var i; + var segment; + + // identify the segment to divide: + + var l = 0; // length so far + var divided; + var dividedSegmentIndex; + var lastValidSegment; // visible AND differentiable + var lastValidSegmentIndex; + var t; + for (i = 0; i < numSegments; i++) { + var index = (fromStart ? i : (numSegments - 1 - i)); + + segment = this.getSegment(index); + var subdivisions = segmentSubdivisions[index]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); + + if (segment.isDifferentiable()) { // segment is not just a point + lastValidSegment = segment; + lastValidSegmentIndex = index; + + if (length <= (l + d)) { + dividedSegmentIndex = index; + divided = segment.divideAtLength(((fromStart ? 1 : -1) * (length - l)), { + precision: precision, + subdivisions: subdivisions + }); + break; + } + } + + l += d; + } + + if (!lastValidSegment) { // no valid segment found + return null; + } + + // else: the path contains at least one valid segment + + if (!divided) { // the desired length is greater than the length of the path + dividedSegmentIndex = lastValidSegmentIndex; + t = (fromStart ? 1 : 0); + divided = lastValidSegment.divideAtT(t); + } + + // create a copy of this path and replace the identified segment with its two divided parts: + + var pathCopy = this.clone(); + pathCopy.replaceSegment(dividedSegmentIndex, divided); + + var divisionStartIndex = dividedSegmentIndex; + var divisionMidIndex = dividedSegmentIndex + 1; + var divisionEndIndex = dividedSegmentIndex + 2; + + // do not insert the part if it looks like a point + if (!divided[0].isDifferentiable()) { + pathCopy.removeSegment(divisionStartIndex); + divisionMidIndex -= 1; + divisionEndIndex -= 1; + } + + // insert a Moveto segment to ensure secondPath will be valid: + var movetoEnd = pathCopy.getSegment(divisionMidIndex).start; + pathCopy.insertSegment(divisionMidIndex, Path.createSegment('M', movetoEnd)); + divisionEndIndex += 1; + + // do not insert the part if it looks like a point + if (!divided[1].isDifferentiable()) { + pathCopy.removeSegment(divisionEndIndex - 1); + divisionEndIndex -= 1; + } + + // ensure that Closepath segments in secondPath will be assigned correct subpathStartSegment: + + var secondPathSegmentIndexConversion = divisionEndIndex - divisionStartIndex - 1; + for (i = divisionEndIndex; i < pathCopy.segments.length; i++) { + + var originalSegment = this.getSegment(i - secondPathSegmentIndexConversion); + segment = pathCopy.getSegment(i); + + if ((segment.type === 'Z') && !originalSegment.subpathStartSegment.end.equals(segment.subpathStartSegment.end)) { + // pathCopy segment's subpathStartSegment is different from original segment's one + // convert this Closepath segment to a Lineto and replace it in pathCopy + var convertedSegment = Path.createSegment('L', originalSegment.end); + pathCopy.replaceSegment(i, convertedSegment); + } + } + + // distribute pathCopy segments into two paths and return those: + + var firstPath = new Path(pathCopy.segments.slice(0, divisionMidIndex)); + var secondPath = new Path(pathCopy.segments.slice(divisionMidIndex)); + + return [firstPath, secondPath]; + }, + + // Checks whether two paths are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { + + if (!p) return false; + + var segments = this.segments; + var otherSegments = p.segments; + + var numSegments = segments.length; + if (otherSegments.length !== numSegments) return false; // if the two paths have different number of segments, they cannot be equal + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var otherSegment = otherSegments[i]; + + // as soon as an inequality is found in segments, return false + if ((segment.type !== otherSegment.type) || (!segment.equals(otherSegment))) return false; + } + + // if no inequality found in segments, return true + return true; + }, + + // Accepts negative indices. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + getSegment: function(index) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); + + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); + + return segments[index]; + }, + + // Returns an array of segment subdivisions, with precision better than requested `opt.precision`. + getSegmentSubdivisions: function(opt) { + + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.segmentSubdivisions + // not using localOpt + + var segmentSubdivisions = []; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segment.getSubdivisions({ precision: precision }); + segmentSubdivisions.push(subdivisions); + } + + return segmentSubdivisions; + }, + + // Returns an array of subpaths of this path. + // Invalid paths are validated first. + // Returns `[]` if path has no segments. + getSubpaths: function() { + + const validatedPath = this.clone().validate(); + + const segments = validatedPath.segments; + const numSegments = segments.length; + + const subpaths = []; + for (let i = 0; i < numSegments; i++) { + + const segment = segments[i]; + if (segment.isSubpathStart) { + // we encountered a subpath start segment + // create a new path for segment, and push it to list of subpaths + subpaths.push(new Path(segment)); + + } else { + // append current segment to the last subpath + subpaths[subpaths.length - 1].appendSegment(segment); + } + } + + return subpaths; + }, + + // Insert `arg` at given `index`. + // `index = 0` means insert at the beginning. + // `index = segments.length` means insert at the end. + // Accepts negative indices, from `-1` to `-(segments.length + 1)`. + // Accepts one segment or an array of segments as argument. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + insertSegment: function(index, arg) { + + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + // note that these are incremented compared to getSegments() + // we can insert after last element (note that this changes the meaning of index -1) + if (index < 0) index = numSegments + index + 1; // convert negative indices to positive + if (index > numSegments || index < 0) throw new Error('Index out of range.'); + + var currentSegment; + + var previousSegment = null; + var nextSegment = null; + + if (numSegments !== 0) { + if (index >= 1) { + previousSegment = segments[index - 1]; + nextSegment = previousSegment.nextSegment; // if we are inserting at end, nextSegment is null + + } else { // if index === 0 + // previousSegment is null + nextSegment = segments[0]; + } + } + + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); + + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 0, currentSegment); + + } else { + // flatten one level deep + // so we can chain arbitrary Path.createSegment results + arg = arg.reduce(function(acc, val) { + return acc.concat(val); + }, []); + + if (!arg[0].isSegment) throw new Error('Segments required.'); + + var n = arg.length; + for (var i = 0; i < n; i++) { + + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; + } + } + }, + + intersectionWithLine: function(line, opt) { + + var intersection = null; + var polylines = this.toPolylines(opt); + if (!polylines) return null; + for (var i = 0, n = polylines.length; i < n; i++) { + var polyline = polylines[i]; + var polylineIntersection = line.intersect(polyline); + if (polylineIntersection) { + intersection || (intersection = []); + if (Array.isArray(polylineIntersection)) { + Array.prototype.push.apply(intersection, polylineIntersection); + } else { + intersection.push(polylineIntersection); + } + } + } + + return intersection; + }, + + isDifferentiable: function() { + + var segments = this.segments; + var numSegments = segments.length; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + // as soon as a differentiable segment is found in segments, return true + if (segment.isDifferentiable()) return true; + } + + // if no differentiable segment is found in segments, return false + return false; + }, + + // Checks whether current path segments are valid. + // Note that d is allowed to be empty - should disable rendering of the path. + isValid: function() { + + var segments = this.segments; + var isValid = (segments.length === 0) || (segments[0].type === 'M'); // either empty or first segment is a Moveto + return isValid; + }, + + // Returns length of the path, with precision better than requested `opt.precision`; or using `opt.segmentSubdivisions` provided. + // If path has no segments, returns 0. + length: function(opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSegmentSubdivisions() call + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var length = 0; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + length += segment.length({ subdivisions: subdivisions }); + } + + return length; + }, + + // Private function. + lengthAtT: function(t, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array + + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return 0; // regardless of t.value + + var tValue = t.value; + if (segmentIndex >= numSegments) { + segmentIndex = numSegments - 1; + tValue = 1; + } else if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var subdivisions; + var length = 0; + for (var i = 0; i < segmentIndex; i++) { + + var segment = segments[i]; + subdivisions = segmentSubdivisions[i]; + length += segment.length({ precisison: precision, subdivisions: subdivisions }); + } + + segment = segments[segmentIndex]; + subdivisions = segmentSubdivisions[segmentIndex]; + length += segment.lengthAtT(tValue, { precisison: precision, subdivisions: subdivisions }); + + return length; + }, + + // Returns point at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + pointAt: function(ratio, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var pathLength = this.length(localOpt); + var length = pathLength * ratio; + + return this.pointAtLength(length, localOpt); + }, + + // Returns point at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + pointAtLength: function(length, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + if (length === 0) return this.start.clone(); + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var lastVisibleSegment; + var l = 0; // length so far + for (var i = 0; i < numSegments; i++) { + var index = (fromStart ? i : (numSegments - 1 - i)); + + var segment = segments[index]; + var subdivisions = segmentSubdivisions[index]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); + + if (segment.isVisible) { + if (length <= (l + d)) { + return segment.pointAtLength(((fromStart ? 1 : -1) * (length - l)), { + precision: precision, + subdivisions: subdivisions + }); + } + + lastVisibleSegment = segment; + } + + l += d; + } + + // if length requested is higher than the length of the path, return last visible segment endpoint + if (lastVisibleSegment) return (fromStart ? lastVisibleSegment.end : lastVisibleSegment.start); + + // if no visible segment, return last segment end point (no matter if fromStart or no) + var lastSegment = segments[numSegments - 1]; + return lastSegment.end.clone(); + }, + + // Private function. + pointAtT: function(t) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].pointAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].pointAtT(1); + + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; + + return segments[segmentIndex].pointAtT(tValue); + }, + + // Default precision + PRECISION: 3, + + // Helper method for adding segments. + prepareSegment: function(segment, previousSegment, nextSegment) { + + // insert after previous segment and before previous segment's next segment + segment.previousSegment = previousSegment; + segment.nextSegment = nextSegment; + if (previousSegment) previousSegment.nextSegment = segment; + if (nextSegment) nextSegment.previousSegment = segment; + + var updateSubpathStart = segment; + if (segment.isSubpathStart) { + segment.subpathStartSegment = segment; // assign self as subpath start segment + updateSubpathStart = nextSegment; // start updating from next segment + } + + // assign previous segment's subpath start (or self if it is a subpath start) to subsequent segments + if (updateSubpathStart) this.updateSubpathStartSegment(updateSubpathStart); + + return segment; + }, + + // Remove the segment at `index`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + removeSegment: function(index) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); + + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); + + var removedSegment = segments.splice(index, 1)[0]; + var previousSegment = removedSegment.previousSegment; + var nextSegment = removedSegment.nextSegment; + + // link the previous and next segments together (if present) + if (previousSegment) previousSegment.nextSegment = nextSegment; // may be null + if (nextSegment) nextSegment.previousSegment = previousSegment; // may be null + + // if removed segment used to start a subpath, update all subsequent segments until another subpath start segment is reached + if (removedSegment.isSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, + + // Replace the segment at `index` with `arg`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Accepts one segment or an array of segments as argument. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + replaceSegment: function(index, arg) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); + + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); + + var currentSegment; + + var replacedSegment = segments[index]; + var previousSegment = replacedSegment.previousSegment; + var nextSegment = replacedSegment.nextSegment; + + var updateSubpathStart = replacedSegment.isSubpathStart; // boolean: is an update of subpath starts necessary? + + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); + + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 1, currentSegment); // directly replace + + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` + + } else { + // flatten one level deep + // so we can chain arbitrary Path.createSegment results + arg = arg.reduce(function(acc, val) { + return acc.concat(val); + }, []); + + if (!arg[0].isSegment) throw new Error('Segments required.'); + + segments.splice(index, 1); + + var n = arg.length; + for (var i = 0; i < n; i++) { + + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; + + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` + } + } + + // if replaced segment used to start a subpath and no new subpath start was added, update all subsequent segments until another subpath start segment is reached + if (updateSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, + + round: function(precision) { + + var segments = this.segments; + var numSegments = segments.length; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + segment.round(precision); + } + + return this; + }, + + scale: function(sx, sy, origin) { + + var segments = this.segments; + var numSegments = segments.length; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + segment.scale(sx, sy, origin); + } + + return this; + }, + + segmentAt: function(ratio, opt) { + + var index = this.segmentIndexAt(ratio, opt); + if (!index) return null; + + return this.getSegment(index); + }, + + // Accepts negative length. + segmentAtLength: function(length, opt) { + + var index = this.segmentIndexAtLength(length, opt); + if (!index) return null; + + return this.getSegment(index); + }, + + segmentIndexAt: function(ratio, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var pathLength = this.length(localOpt); + var length = pathLength * ratio; + + return this.segmentIndexAtLength(length, localOpt); + }, + + // Accepts negative length. + segmentIndexAtLength: function(length, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var lastVisibleSegmentIndex = null; + var l = 0; // length so far + for (var i = 0; i < numSegments; i++) { + var index = (fromStart ? i : (numSegments - 1 - i)); + + var segment = segments[index]; + var subdivisions = segmentSubdivisions[index]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); + + if (segment.isVisible) { + if (length <= (l + d)) return index; + lastVisibleSegmentIndex = index; + } + + l += d; + } + + // if length requested is higher than the length of the path, return last visible segment index + // if no visible segment, return null + return lastVisibleSegmentIndex; + }, + + // Returns a string that can be used to reconstruct the path. + // Additional error checking compared to toString (must start with M segment). + serialize: function() { + + if (!this.isValid()) throw new Error('Invalid path segments.'); + + return this.toString(); + }, + + // Returns tangent line at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + tangentAt: function(ratio, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var pathLength = this.length(localOpt); + var length = pathLength * ratio; + + return this.tangentAtLength(length, localOpt); + }, + + // Returns tangent line at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + tangentAtLength: function(length, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var lastValidSegment; // visible AND differentiable (with a tangent) + var l = 0; // length so far + for (var i = 0; i < numSegments; i++) { + var index = (fromStart ? i : (numSegments - 1 - i)); + + var segment = segments[index]; + var subdivisions = segmentSubdivisions[index]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); + + if (segment.isDifferentiable()) { + if (length <= (l + d)) { + return segment.tangentAtLength(((fromStart ? 1 : -1) * (length - l)), { + precision: precision, + subdivisions: subdivisions + }); + } + + lastValidSegment = segment; + } + + l += d; + } + + // if length requested is higher than the length of the path, return tangent of endpoint of last valid segment + if (lastValidSegment) { + var t = (fromStart ? 1 : 0); + return lastValidSegment.tangentAtT(t); + } + + // if no valid segment, return null + return null; + }, + + // Private function. + tangentAtT: function(t) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].tangentAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].tangentAtT(1); + + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; + + return segments[segmentIndex].tangentAtT(tValue); + }, + + toPoints: function(opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + + var points = []; + var partialPoints = []; + for (var i = 0; i < numSegments; i++) { + var segment = segments[i]; + if (segment.isVisible) { + var currentSegmentSubdivisions = segmentSubdivisions[i]; + if (currentSegmentSubdivisions.length > 0) { + var subdivisionPoints = currentSegmentSubdivisions.map(function(curve) { + return curve.start; + }); + Array.prototype.push.apply(partialPoints, subdivisionPoints); + } else { + partialPoints.push(segment.start); + } + } else if (partialPoints.length > 0) { + partialPoints.push(segments[i - 1].end); + points.push(partialPoints); + partialPoints = []; + } + } + + if (partialPoints.length > 0) { + partialPoints.push(this.end); + points.push(partialPoints); + } + return points; + }, + + toPolylines: function(opt) { + + var polylines = []; + var points = this.toPoints(opt); + if (!points) return null; + for (var i = 0, n = points.length; i < n; i++) { + polylines.push(new Polyline(points[i])); + } + + return polylines; + }, + + toString: function() { + + var segments = this.segments; + var numSegments = segments.length; + + var pathData = ''; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + pathData += segment.serialize() + ' '; + } + + return pathData.trim(); + }, + + translate: function(tx, ty) { + + var segments = this.segments; + var numSegments = segments.length; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + segment.translate(tx, ty); + } + + return this; + }, + + // Helper method for updating subpath start of segments, starting with the one provided. + updateSubpathStartSegment: function(segment) { + + var previousSegment = segment.previousSegment; // may be null + while (segment && !segment.isSubpathStart) { + + // assign previous segment's subpath start segment to this segment + if (previousSegment) segment.subpathStartSegment = previousSegment.subpathStartSegment; // may be null + else segment.subpathStartSegment = null; // if segment had no previous segment, assign null - creates an invalid path! + + previousSegment = segment; + segment = segment.nextSegment; // move on to the segment after etc. + } + }, + + // If the path is not valid, insert M 0 0 at the beginning. + // Path with no segments is considered valid, so nothing is inserted. + validate: function() { + + if (!this.isValid()) this.insertSegment(0, Path.createSegment('M', 0, 0)); + return this; + } +}; + +Object.defineProperty(Path.prototype, 'start', { + // Getter for the first visible endpoint of the path. + + configurable: true, + + enumerable: true, + + get: function() { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + if (segment.isVisible) return segment.start; + } + + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; + } +}); + +Object.defineProperty(Path.prototype, 'end', { + // Getter for the last visible endpoint of the path. + + configurable: true, + + enumerable: true, + + get: function() { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; + + for (var i = numSegments - 1; i >= 0; i--) { + + var segment = segments[i]; + if (segment.isVisible) return segment.end; + } + + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; + } +}); + + +// Local helper function. +// Use an array of arguments to call a constructor (function called with `new`). +// Adapted from https://stackoverflow.com/a/8843181/2263595 +// It is not necessary to use this function if the arguments can be passed separately (i.e. if the number of arguments is limited). +// - If that is the case, use `new constructor(arg1, arg2)`, for example. +// It is not necessary to use this function if the function that needs an array of arguments is not supposed to be used as a constructor. +// - If that is the case, use `f.apply(thisArg, [arg1, arg2...])`, for example. +function applyToNew(constructor, argsArray) { + // The `new` keyword can only be applied to functions that take a limited number of arguments. + // - We can fake that with .bind(). + // - It calls a function (`constructor`, here) with the arguments that were provided to it - effectively transforming an unlimited number of arguments into limited. + // - So `new (constructor.bind(thisArg, arg1, arg2...))` + // - `thisArg` can be anything (e.g. null) because `new` keyword resets context to the constructor object. + // We need to pass in a variable number of arguments to the bind() call. + // - We can use .apply(). + // - So `new (constructor.bind.apply(constructor, [thisArg, arg1, arg2...]))` + // - `thisArg` can still be anything because `new` overwrites it. + // Finally, to make sure that constructor.bind overwriting is not a problem, we switch to `Function.prototype.bind`. + // - So, the final version is `new (Function.prototype.bind.apply(constructor, [thisArg, arg1, arg2...]))` + + // The function expects `argsArray[0]` to be `thisArg`. + // - This means that whatever is sent as the first element will be ignored. + // - The constructor will only see arguments starting from argsArray[1]. + // - So, a new dummy element is inserted at the start of the array. + argsArray.unshift(null); + + return new (Function.prototype.bind.apply(constructor, argsArray)); +} + +// Local helper function. +// Add properties from arguments on top of properties from `obj`. +// This allows for rudimentary inheritance. +// - The `obj` argument acts as parent. +// - This function creates a new object that inherits all `obj` properties and adds/replaces those that are present in arguments. +// - A high-level example: calling `extend(Vehicle, Car)` would be akin to declaring `class Car extends Vehicle`. +function extend(obj) { + // In JavaScript, the combination of a constructor function (e.g. `g.Line = function(...) {...}`) and prototype (e.g. `g.Line.prototype = {...}) is akin to a C++ class. + // - When inheritance is not necessary, we can leave it at that. (This would be akin to calling extend with only `obj`.) + // - But, what if we wanted the `g.Line` quasiclass to inherit from another quasiclass (let's call it `g.GeometryObject`) in JavaScript? + // - First, realize that both of those quasiclasses would still have their own separate constructor function. + // - So what we are actually saying is that we want the `g.Line` prototype to inherit from `g.GeometryObject` prototype. + // - This method provides a way to do exactly that. + // - It copies parent prototype's properties, then adds extra ones from child prototype/overrides parent prototype properties with child prototype properties. + // - Therefore, to continue with the example above: + // - `g.Line.prototype = extend(g.GeometryObject.prototype, linePrototype)` + // - Where `linePrototype` is a properties object that looks just like `g.Line.prototype` does right now. + // - Then, `g.Line` would allow the programmer to access to all methods currently in `g.Line.Prototype`, plus any non-overridden methods from `g.GeometryObject.prototype`. + // - In that aspect, `g.GeometryObject` would then act like the parent of `g.Line`. + // - Multiple inheritance is also possible, if multiple arguments are provided. + // - What if we wanted to add another level of abstraction between `g.GeometryObject` and `g.Line` (let's call it `g.LinearObject`)? + // - `g.Line.prototype = extend(g.GeometryObject.prototype, g.LinearObject.prototype, linePrototype)` + // - The ancestors are applied in order of appearance. + // - That means that `g.Line` would have inherited from `g.LinearObject` that would have inherited from `g.GeometryObject`. + // - Any number of ancestors may be provided. + // - Note that neither `obj` nor any of the arguments need to actually be prototypes of any JavaScript quasiclass, that was just a simplified explanation. + // - We can create a new object composed from the properties of any number of other objects (since they do not have a constructor, we can think of those as interfaces). + // - `extend({ a: 1, b: 2 }, { b: 10, c: 20 }, { c: 100, d: 200 })` gives `{ a: 1, b: 10, c: 100, d: 200 }`. + // - Basically, with this function, we can emulate the `extends` keyword as well as the `implements` keyword. + // - Therefore, both of the following are valid: + // - `Lineto.prototype = extend(Line.prototype, segmentPrototype, linetoPrototype)` + // - `Moveto.prototype = extend(segmentPrototype, movetoPrototype)` + + var i; + var n; + + var args = []; + n = arguments.length; + for (i = 1; i < n; i++) { // skip over obj + args.push(arguments[i]); + } + + if (!obj) throw new Error('Missing a parent object.'); + var child = Object.create(obj); + + n = args.length; + for (i = 0; i < n; i++) { + + var src = args[i]; + + var inheritedProperty; + var key; + for (key in src) { + + if (src.hasOwnProperty(key)) { + delete child[key]; // delete property inherited from parent + inheritedProperty = Object.getOwnPropertyDescriptor(src, key); // get new definition of property from src + Object.defineProperty(child, key, inheritedProperty); // re-add property with new definition (includes getter/setter methods) + } + } + } + + return child; +} + +// Path segment interface: +var segmentPrototype = { + + // virtual + bbox: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + clone: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + closestPoint: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + closestPointLength: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + closestPointNormalizedLength: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // Redirect calls to closestPointNormalizedLength() function if closestPointT() is not defined for segment. + closestPointT: function(p) { + + if (this.closestPointNormalizedLength) return this.closestPointNormalizedLength(p); + + throw new Error('Neither closestPointT() nor closestPointNormalizedLength() function is implemented.'); + }, + + // virtual + closestPointTangent: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + divideAt: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + divideAtLength: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // Redirect calls to divideAt() function if divideAtT() is not defined for segment. + divideAtT: function(t) { + + if (this.divideAt) return this.divideAt(t); + + throw new Error('Neither divideAtT() nor divideAt() function is implemented.'); + }, + + // virtual + equals: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + getSubdivisions: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + isDifferentiable: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + isSegment: true, + + isSubpathStart: false, // true for Moveto segments + + isVisible: true, // false for Moveto segments + + // virtual + length: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // Return a fraction of result of length() function if lengthAtT() is not defined for segment. + lengthAtT: function(t) { + + if (t <= 0) return 0; + + var length = this.length(); + + if (t >= 1) return length; + + return length * t; + }, + + nextSegment: null, // needed for subpath start segment updating + + // virtual + pointAt: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + pointAtLength: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // Redirect calls to pointAt() function if pointAtT() is not defined for segment. + pointAtT: function(t) { + + if (this.pointAt) return this.pointAt(t); + + throw new Error('Neither pointAtT() nor pointAt() function is implemented.'); + }, + + previousSegment: null, // needed to get segment start property + + // virtual + round: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + subpathStartSegment: null, // needed to get Closepath segment end property + + // virtual + scale: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + serialize: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + tangentAt: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + tangentAtLength: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // Redirect calls to tangentAt() function if tangentAtT() is not defined for segment. + tangentAtT: function(t) { + + if (this.tangentAt) return this.tangentAt(t); + + throw new Error('Neither tangentAtT() nor tangentAt() function is implemented.'); + }, + + // virtual + toString: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + // virtual + translate: function() { + + throw new Error('Declaration missing for virtual function.'); + } +}; + +// usually directly assigned +// getter for Closepath +Object.defineProperty(segmentPrototype, 'end', { + + configurable: true, + + enumerable: true, + + writable: true +}); + +// always a getter +// always throws error for Moveto +Object.defineProperty(segmentPrototype, 'start', { + // get a reference to the end point of previous segment + + configurable: true, + + enumerable: true, + + get: function() { + + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); + + return this.previousSegment.end; + } +}); + +// virtual +Object.defineProperty(segmentPrototype, 'type', { + + configurable: true, + + enumerable: true, + + get: function() { + + throw new Error('Bad segment declaration. No type specified.'); + } +}); + +// Path segment implementations: +var Lineto = function() { + + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } + + if (!(this instanceof Lineto)) { // switching context of `this` to Lineto when called without `new` + return applyToNew(Lineto, args); + } + + if (n === 0) { + throw new Error('Lineto constructor expects a line, 1 point, or 2 coordinates (none provided).'); + } + + var outputArray; + + if (args[0] instanceof Line) { // lines provided + if (n === 1) { + this.end = args[0].end.clone(); + return this; + + } else { + throw new Error('Lineto constructor expects a line, 1 point, or 2 coordinates (' + n + ' lines provided).'); + } + + } else if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; + + } else if (n < 2) { + throw new Error('Lineto constructor expects a line, 1 point, or 2 coordinates (' + n + ' coordinates provided).'); + + } else { // this is a poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two + + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + outputArray.push(applyToNew(Lineto, segmentCoords)); + } + return outputArray; + } + + } else { // points provided (needs to be last to also cover plain objects with x and y) + if (n === 1) { + this.end = new Point(args[0]); + return this; + + } else { // this is a poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { + + segmentPoint = args[i]; + outputArray.push(new Lineto(segmentPoint)); + } + return outputArray; + } + } +}; + +var linetoPrototype = { + + clone: function() { + + return new Lineto(this.end); + }, + + divideAt: function(ratio) { + + var line = new Line(this.start, this.end); + var divided = line.divideAt(ratio); + return [ + new Lineto(divided[0]), + new Lineto(divided[1]) + ]; + }, + + divideAtLength: function(length) { + + var line = new Line(this.start, this.end); + var divided = line.divideAtLength(length); + return [ + new Lineto(divided[0]), + new Lineto(divided[1]) + ]; + }, + + getSubdivisions: function() { + + return []; + }, + + isDifferentiable: function() { + + if (!this.previousSegment) return false; + + return !this.start.equals(this.end); + }, + + round: function(precision) { + + this.end.round(precision); + return this; + }, + + scale: function(sx, sy, origin) { + + this.end.scale(sx, sy, origin); + return this; + }, + + serialize: function() { + + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, + + toString: function() { + + return this.type + ' ' + this.start + ' ' + this.end; + }, + + translate: function(tx, ty) { + + this.end.translate(tx, ty); + return this; + } +}; + +Object.defineProperty(linetoPrototype, 'type', { + + configurable: true, + + enumerable: true, + + value: 'L' +}); + +Lineto.prototype = extend(segmentPrototype, Line.prototype, linetoPrototype); + +var Curveto = function() { + + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } + + if (!(this instanceof Curveto)) { // switching context of `this` to Curveto when called without `new` + return applyToNew(Curveto, args); + } + + if (n === 0) { + throw new Error('Curveto constructor expects a curve, 3 points, or 6 coordinates (none provided).'); + } + + var outputArray; + + if (args[0] instanceof Curve) { // curves provided + if (n === 1) { + this.controlPoint1 = args[0].controlPoint1.clone(); + this.controlPoint2 = args[0].controlPoint2.clone(); + this.end = args[0].end.clone(); + return this; + + } else { + throw new Error('Curveto constructor expects a curve, 3 points, or 6 coordinates (' + n + ' curves provided).'); + } + + } else if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 6) { + this.controlPoint1 = new Point(+args[0], +args[1]); + this.controlPoint2 = new Point(+args[2], +args[3]); + this.end = new Point(+args[4], +args[5]); + return this; + + } else if (n < 6) { + throw new Error('Curveto constructor expects a curve, 3 points, or 6 coordinates (' + n + ' coordinates provided).'); + + } else { // this is a poly-bezier segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 6) { // coords come in groups of six + + segmentCoords = args.slice(i, i + 6); // will send fewer than six coords if args.length not divisible by 6 + outputArray.push(applyToNew(Curveto, segmentCoords)); + } + return outputArray; + } + + } else { // points provided (needs to be last to also cover plain objects with x and y) + if (n === 3) { + this.controlPoint1 = new Point(args[0]); + this.controlPoint2 = new Point(args[1]); + this.end = new Point(args[2]); + return this; + + } else if (n < 3) { + throw new Error('Curveto constructor expects a curve, 3 points, or 6 coordinates (' + n + ' points provided).'); + + } else { // this is a poly-bezier segment + var segmentPoints; + outputArray = []; + for (i = 0; i < n; i += 3) { // points come in groups of three + + segmentPoints = args.slice(i, i + 3); // will send fewer than three points if args.length is not divisible by 3 + outputArray.push(applyToNew(Curveto, segmentPoints)); + } + return outputArray; + } + } +}; + +var curvetoPrototype = { + + clone: function() { + + return new Curveto(this.controlPoint1, this.controlPoint2, this.end); + }, + + divideAt: function(ratio, opt) { + + var curve = new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); + var divided = curve.divideAt(ratio, opt); + return [ + new Curveto(divided[0]), + new Curveto(divided[1]) + ]; + }, + + divideAtLength: function(length, opt) { + + var curve = new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); + var divided = curve.divideAtLength(length, opt); + return [ + new Curveto(divided[0]), + new Curveto(divided[1]) + ]; + }, + + divideAtT: function(t) { + + var curve = new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); + var divided = curve.divideAtT(t); + return [ + new Curveto(divided[0]), + new Curveto(divided[1]) + ]; + }, + + isDifferentiable: function() { + + if (!this.previousSegment) return false; + + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); + }, + + round: function(precision) { + + this.controlPoint1.round(precision); + this.controlPoint2.round(precision); + this.end.round(precision); + return this; + }, + + scale: function(sx, sy, origin) { + + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, + + serialize: function() { + + var c1 = this.controlPoint1; + var c2 = this.controlPoint2; + var end = this.end; + return this.type + ' ' + c1.x + ' ' + c1.y + ' ' + c2.x + ' ' + c2.y + ' ' + end.x + ' ' + end.y; + }, + + toString: function() { + + return this.type + ' ' + this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + }, + + translate: function(tx, ty) { + + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; + } +}; + +Object.defineProperty(curvetoPrototype, 'type', { + + configurable: true, + + enumerable: true, + + value: 'C' +}); + +Curveto.prototype = extend(segmentPrototype, Curve.prototype, curvetoPrototype); + +var Moveto = function() { + + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } + + if (!(this instanceof Moveto)) { // switching context of `this` to Moveto when called without `new` + return applyToNew(Moveto, args); + } + + if (n === 0) { + throw new Error('Moveto constructor expects a line, a curve, 1 point, or 2 coordinates (none provided).'); + } + + var outputArray; + + if (args[0] instanceof Line) { // lines provided + if (n === 1) { + this.end = args[0].end.clone(); + return this; + + } else { + throw new Error('Moveto constructor expects a line, a curve, 1 point, or 2 coordinates (' + n + ' lines provided).'); + } + + } else if (args[0] instanceof Curve) { // curves provided + if (n === 1) { + this.end = args[0].end.clone(); + return this; + + } else { + throw new Error('Moveto constructor expects a line, a curve, 1 point, or 2 coordinates (' + n + ' curves provided).'); + } + + } else if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; + + } else if (n < 2) { + throw new Error('Moveto constructor expects a line, a curve, 1 point, or 2 coordinates (' + n + ' coordinates provided).'); + + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two + + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + if (i === 0) outputArray.push(applyToNew(Moveto, segmentCoords)); + else outputArray.push(applyToNew(Lineto, segmentCoords)); + } + return outputArray; + } + + } else { // points provided (needs to be last to also cover plain objects with x and y) + if (n === 1) { + this.end = new Point(args[0]); + return this; + + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { // points come one by one + + segmentPoint = args[i]; + if (i === 0) outputArray.push(new Moveto(segmentPoint)); + else outputArray.push(new Lineto(segmentPoint)); + } + return outputArray; + } + } +}; + +var movetoPrototype = { + + bbox: function() { + + return null; + }, + + clone: function() { + + return new Moveto(this.end); + }, + + closestPoint: function() { + + return this.end.clone(); + }, + + closestPointNormalizedLength: function() { + + return 0; + }, + + closestPointLength: function() { + + return 0; + }, + + closestPointT: function() { + + return 1; + }, + + closestPointTangent: function() { + + return null; + }, + + divideAt: function() { + + return [ + this.clone(), + this.clone() + ]; + }, + + divideAtLength: function() { + + return [ + this.clone(), + this.clone() + ]; + }, + + equals: function(m) { + + return this.end.equals(m.end); + }, + + getSubdivisions: function() { + + return []; + }, + + isDifferentiable: function() { + + return false; + }, + + isSubpathStart: true, + + isVisible: false, + + length: function() { + + return 0; + }, + + lengthAtT: function() { + + return 0; + }, + + pointAt: function() { + + return this.end.clone(); + }, + + pointAtLength: function() { + + return this.end.clone(); + }, + + pointAtT: function() { + + return this.end.clone(); + }, + + round: function(precision) { + + this.end.round(precision); + return this; + }, + + scale: function(sx, sy, origin) { + + this.end.scale(sx, sy, origin); + return this; + }, + + serialize: function() { + + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, + + tangentAt: function() { + + return null; + }, + + tangentAtLength: function() { + + return null; + }, + + tangentAtT: function() { + + return null; + }, + + toString: function() { + + return this.type + ' ' + this.end; + }, + + translate: function(tx, ty) { + + this.end.translate(tx, ty); + return this; + } +}; + +Object.defineProperty(movetoPrototype, 'start', { + + configurable: true, + + enumerable: true, + + get: function() { + + throw new Error('Illegal access. Moveto segments should not need a start property.'); + } +}); + +Object.defineProperty(movetoPrototype, 'type', { + + configurable: true, + + enumerable: true, + + value: 'M' +}); + +Moveto.prototype = extend(segmentPrototype, movetoPrototype); // does not inherit from any other geometry object + +var Closepath = function() { + + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } + + if (!(this instanceof Closepath)) { // switching context of `this` to Closepath when called without `new` + return applyToNew(Closepath, args); + } + + if (n > 0) { + throw new Error('Closepath constructor expects no arguments.'); + } + + return this; +}; + +var closepathPrototype = { + + clone: function() { + + return new Closepath(); + }, + + divideAt: function(ratio) { + + var line = new Line(this.start, this.end); + var divided = line.divideAt(ratio); + return [ + // if we didn't actually cut into the segment, first divided part can stay as Z + (divided[1].isDifferentiable() ? new Lineto(divided[0]) : this.clone()), + new Lineto(divided[1]) + ]; + }, + + divideAtLength: function(length) { + + var line = new Line(this.start, this.end); + var divided = line.divideAtLength(length); + return [ + // if we didn't actually cut into the segment, first divided part can stay as Z + (divided[1].isDifferentiable() ? new Lineto(divided[0]) : this.clone()), + new Lineto(divided[1]) + ]; + }, + + getSubdivisions: function() { + + return []; + }, + + isDifferentiable: function() { + + if (!this.previousSegment || !this.subpathStartSegment) return false; + + return !this.start.equals(this.end); + }, + + round: function() { + + return this; + }, + + scale: function() { + + return this; + }, + + serialize: function() { + + return this.type; + }, + + toString: function() { + + return this.type + ' ' + this.start + ' ' + this.end; + }, + + translate: function() { + + return this; + } +}; + +Object.defineProperty(closepathPrototype, 'end', { + // get a reference to the end point of subpath start segment + + configurable: true, + + enumerable: true, + + get: function() { + + if (!this.subpathStartSegment) throw new Error('Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)'); + + return this.subpathStartSegment.end; + } +}); + +Object.defineProperty(closepathPrototype, 'type', { + + configurable: true, + + enumerable: true, + + value: 'Z' +}); + +Closepath.prototype = extend(segmentPrototype, Line.prototype, closepathPrototype); + +var segmentTypes = Path.segmentTypes = { + L: Lineto, + C: Curveto, + M: Moveto, + Z: Closepath, + z: Closepath +}; + +Path.regexSupportedData = new RegExp('^[\\s\\d' + Object.keys(segmentTypes).join('') + ',.]*$'); + +Path.isDataSupported = function(data) { + + if (typeof data !== 'string') return false; + return this.regexSupportedData.test(data); +}; diff --git a/src/g/point.mjs b/src/g/point.mjs new file mode 100644 index 0000000000..6c94c45501 --- /dev/null +++ b/src/g/point.mjs @@ -0,0 +1,371 @@ +/* + Point is the most basic object consisting of x/y coordinate. + + Possible instantiations are: + * `Point(10, 20)` + * `new Point(10, 20)` + * `Point('10 20')` + * `Point(Point(10, 20))` +*/ +import { normalizeAngle, random, snapToGrid, toDeg, toRad } from './geometry.helpers.mjs'; +import { bearing } from './line.bearing.mjs'; +import { squaredLength } from './line.squaredLength.mjs'; +import { length } from './line.length.mjs'; + +const math = Math; +const abs = math.abs; +const cos = math.cos; +const sin = math.sin; +const sqrt = math.sqrt; +const min = math.min; +const max = math.max; +const atan2 = math.atan2; +const round = math.round; +const PI = math.PI; +const pow = math.pow; + +export const Point = function(x, y) { + + if (!(this instanceof Point)) { + return new Point(x, y); + } + + if (typeof x === 'string') { + var xy = x.split(x.indexOf('@') === -1 ? ' ' : '@'); + x = parseFloat(xy[0]); + y = parseFloat(xy[1]); + + } else if (Object(x) === x) { + y = x.y; + x = x.x; + } + + this.x = x === undefined ? 0 : x; + this.y = y === undefined ? 0 : y; +}; + +// Alternative constructor, from polar coordinates. +// @param {number} Distance. +// @param {number} Angle in radians. +// @param {point} [optional] Origin. +Point.fromPolar = function(distance, angle, origin) { + + origin = new Point(origin); + var x = abs(distance * cos(angle)); + var y = abs(distance * sin(angle)); + var deg = normalizeAngle(toDeg(angle)); + + if (deg < 90) { + y = -y; + + } else if (deg < 180) { + x = -x; + y = -y; + + } else if (deg < 270) { + x = -x; + } + + return new Point(origin.x + x, origin.y + y); +}; + +// Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. +Point.random = function(x1, x2, y1, y2) { + + return new Point(random(x1, x2), random(y1, y2)); +}; + +Point.prototype = { + + chooseClosest: function(points) { + + var n = points.length; + if (n === 1) return new Point(points[0]); + var closest = null; + var minSqrDistance = Infinity; + for (var i = 0; i < n; i++) { + var p = new Point(points[i]); + var sqrDistance = this.squaredDistance(p); + if (sqrDistance < minSqrDistance) { + closest = p; + minSqrDistance = sqrDistance; + } + } + return closest; + }, + + // If point lies outside rectangle `r`, return the nearest point on the boundary of rect `r`, + // otherwise return point itself. + // (see Squeak Smalltalk, Point>>adhereTo:) + adhereToRect: function(r) { + + if (r.containsPoint(this)) { + return this; + } + + this.x = min(max(this.x, r.x), r.x + r.width); + this.y = min(max(this.y, r.y), r.y + r.height); + return this; + }, + + // Compute the angle between vector from me to p1 and the vector from me to p2. + // ordering of points p1 and p2 is important! + // theta function's angle convention: + // returns angles between 0 and 180 when the angle is counterclockwise + // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones + // returns NaN if any of the points p1, p2 is coincident with this point + angleBetween: function(p1, p2) { + + var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); + + if (angleBetween < 0) { + angleBetween += 360; // correction to keep angleBetween between 0 and 360 + } + + return angleBetween; + }, + + // Return the bearing between me and the given point. + bearing: function(point) { + return bearing(this, point); + }, + + // Returns change in angle from my previous position (-dx, -dy) to my new position + // relative to ref point. + changeInAngle: function(dx, dy, ref) { + + // Revert the translation and measure the change in angle around x-axis. + return this.clone().offset(-dx, -dy).theta(ref) - this.theta(ref); + }, + + clone: function() { + + return new Point(this); + }, + + // Returns the cross product of this point relative to two other points + // this point is the common point + // point p1 lies on the first vector, point p2 lies on the second vector + // watch out for the ordering of points p1 and p2! + // positive result indicates a clockwise ("right") turn from first to second vector + // negative result indicates a counterclockwise ("left") turn from first to second vector + // zero indicates that the first and second vector are collinear + // note that the above directions are reversed from the usual answer on the Internet + // that is because we are in a left-handed coord system (because the y-axis points downward) + cross: function(p1, p2) { + + return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; + }, + + difference: function(dx, dy) { + + if ((Object(dx) === dx)) { + dy = dx.y; + dx = dx.x; + } + + return new Point(this.x - (dx || 0), this.y - (dy || 0)); + }, + + // Returns distance between me and point `p`. + distance: function(p) { + return length(this, p); + }, + + // Returns the dot product of this point with given other point + dot: function(p) { + + return p ? (this.x * p.x + this.y * p.y) : NaN; + }, + + equals: function(p) { + + return !!p && + this.x === p.x && + this.y === p.y; + }, + + // Linear interpolation + lerp: function(p, t) { + + var x = this.x; + var y = this.y; + return new Point((1 - t) * x + t * p.x, (1 - t) * y + t * p.y); + }, + + magnitude: function() { + + return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; + }, + + // Returns a manhattan (taxi-cab) distance between me and point `p`. + manhattanDistance: function(p) { + + return abs(p.x - this.x) + abs(p.y - this.y); + }, + + // Move point on line starting from ref ending at me by + // distance distance. + move: function(ref, distance) { + + var theta = toRad((new Point(ref)).theta(this)); + var offset = this.offset(cos(theta) * distance, -sin(theta) * distance); + return offset; + }, + + // Scales x and y such that the distance between the point and the origin (0,0) is equal to the given length. + normalize: function(length) { + + var scale = (length || 1) / this.magnitude(); + return this.scale(scale, scale); + }, + + // Offset me by the specified amount. + offset: function(dx, dy) { + + if ((Object(dx) === dx)) { + dy = dx.y; + dx = dx.x; + } + + this.x += dx || 0; + this.y += dy || 0; + return this; + }, + + // Returns a point that is the reflection of me with + // the center of inversion in ref point. + reflection: function(ref) { + + return (new Point(ref)).move(this, this.distance(ref)); + }, + + // Rotate point by angle around origin. + // Angle is flipped because this is a left-handed coord system (y-axis points downward). + rotate: function(origin, angle) { + + if (angle === 0) return this; + + origin = origin || new Point(0, 0); + + angle = toRad(normalizeAngle(-angle)); + var cosAngle = cos(angle); + var sinAngle = sin(angle); + + var x = (cosAngle * (this.x - origin.x)) - (sinAngle * (this.y - origin.y)) + origin.x; + var y = (sinAngle * (this.x - origin.x)) + (cosAngle * (this.y - origin.y)) + origin.y; + + this.x = x; + this.y = y; + return this; + }, + + round: function(precision) { + + let f = 1; // case 0 + if (precision) { + switch (precision) { + case 1: f = 10; break; + case 2: f = 100; break; + case 3: f = 1000; break; + default: f = pow(10, precision); break; + } + } + + this.x = round(this.x * f) / f; + this.y = round(this.y * f) / f; + return this; + }, + + // Scale point with origin. + scale: function(sx, sy, origin) { + + origin = (origin && new Point(origin)) || new Point(0, 0); + this.x = origin.x + sx * (this.x - origin.x); + this.y = origin.y + sy * (this.y - origin.y); + return this; + }, + + snapToGrid: function(gx, gy) { + + this.x = snapToGrid(this.x, gx); + this.y = snapToGrid(this.y, gy || gx); + return this; + }, + + squaredDistance: function(p) { + return squaredLength(this, p); + }, + + // Compute the angle between me and `p` and the x axis. + // (cartesian-to-polar coordinates conversion) + // Return theta angle in degrees. + theta: function(p) { + + p = new Point(p); + + // Invert the y-axis. + var y = -(p.y - this.y); + var x = p.x - this.x; + var rad = atan2(y, x); // defined for all 0 corner cases + + // Correction for III. and IV. quadrant. + if (rad < 0) { + rad = 2 * PI + rad; + } + + return 180 * rad / PI; + }, + + toJSON: function() { + + return { x: this.x, y: this.y }; + }, + + // Converts rectangular to polar coordinates. + // An origin can be specified, otherwise it's 0@0. + toPolar: function(o) { + + o = (o && new Point(o)) || new Point(0, 0); + var x = this.x; + var y = this.y; + this.x = sqrt((x - o.x) * (x - o.x) + (y - o.y) * (y - o.y)); // r + this.y = toRad(o.theta(new Point(x, y))); + return this; + }, + + toString: function() { + + return this.x + '@' + this.y; + }, + + serialize: function() { + + return this.x + ',' + this.y; + }, + + update: function(x, y) { + + if ((Object(x) === x)) { + y = x.y; + x = x.x; + } + + this.x = x || 0; + this.y = y || 0; + return this; + }, + + // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. + // Returns NaN if p is at 0,0. + vectorAngle: function(p) { + + var zero = new Point(0, 0); + return zero.angleBetween(this, p); + } +}; + +Point.prototype.translate = Point.prototype.offset; + +// For backwards compatibility: +export const point = Point; diff --git a/src/g/polyline.mjs b/src/g/polyline.mjs new file mode 100644 index 0000000000..3ab395ef5a --- /dev/null +++ b/src/g/polyline.mjs @@ -0,0 +1,722 @@ +import { Rect } from './rect.mjs'; +import { Point } from './point.mjs'; +import { Line } from './line.mjs'; + +const abs = Math.abs; + +export const Polyline = function(points) { + + if (!(this instanceof Polyline)) { + return new Polyline(points); + } + + if (typeof points === 'string') { + return new Polyline.parse(points); + } + + this.points = (Array.isArray(points) ? points.map(Point) : []); +}; + +Polyline.parse = function(svgString) { + svgString = svgString.trim(); + if (svgString === '') return new Polyline(); + + var points = []; + + var coords = svgString.split(/\s*,\s*|\s+/); + var n = coords.length; + for (var i = 0; i < n; i += 2) { + points.push({ x: +coords[i], y: +coords[i + 1] }); + } + + return new Polyline(points); +}; + +Polyline.prototype = { + + bbox: function() { + + var x1 = Infinity; + var x2 = -Infinity; + var y1 = Infinity; + var y2 = -Infinity; + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + + for (var i = 0; i < numPoints; i++) { + + var point = points[i]; + var x = point.x; + var y = point.y; + + if (x < x1) x1 = x; + if (x > x2) x2 = x; + if (y < y1) y1 = y; + if (y > y2) y2 = y; + } + + return new Rect(x1, y1, x2 - x1, y2 - y1); + }, + + clone: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty + + var newPoints = []; + for (var i = 0; i < numPoints; i++) { + + var point = points[i].clone(); + newPoints.push(point); + } + + return new Polyline(newPoints); + }, + + closestPoint: function(p) { + + var cpLength = this.closestPointLength(p); + + return this.pointAtLength(cpLength); + }, + + closestPointLength: function(p) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty + if (numPoints === 1) return 0; // if there is only one point + + var cpLength; + var minSqrDistance = Infinity; + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + + var line = new Line(points[i], points[i + 1]); + var lineLength = line.length(); + + var cpNormalizedLength = line.closestPointNormalizedLength(p); + var cp = line.pointAt(cpNormalizedLength); + + var sqrDistance = cp.squaredDistance(p); + if (sqrDistance < minSqrDistance) { + minSqrDistance = sqrDistance; + cpLength = length + (cpNormalizedLength * lineLength); + } + + length += lineLength; + } + + return cpLength; + }, + + closestPointNormalizedLength: function(p) { + + var cpLength = this.closestPointLength(p); + if (cpLength === 0) return 0; // shortcut + + var length = this.length(); + if (length === 0) return 0; // prevents division by zero + + return cpLength / length; + }, + + closestPointTangent: function(p) { + + var cpLength = this.closestPointLength(p); + + return this.tangentAtLength(cpLength); + }, + + // Returns `true` if the area surrounded by the polyline contains the point `p`. + // Implements the even-odd SVG algorithm (self-intersections are "outside"). + // (Uses horizontal rays to the right of `p` to look for intersections.) + // Closes open polylines (always imagines a final closing segment). + containsPoint: function(p) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return false; // shortcut (this polyline has no points) + + var x = p.x; + var y = p.y; + + // initialize a final closing segment by creating one from last-first points on polyline + var startIndex = numPoints - 1; // start of current polyline segment + var endIndex = 0; // end of current polyline segment + var numIntersections = 0; + for (; endIndex < numPoints; endIndex++) { + var start = points[startIndex]; + var end = points[endIndex]; + if (p.equals(start)) return true; // shortcut (`p` is a point on polyline) + + var segment = new Line(start, end); // current polyline segment + if (segment.containsPoint(p)) return true; // shortcut (`p` lies on a polyline segment) + + // do we have an intersection? + if (((y <= start.y) && (y > end.y)) || ((y > start.y) && (y <= end.y))) { + // this conditional branch IS NOT entered when `segment` is collinear/coincident with `ray` + // (when `y === start.y === end.y`) + // this conditional branch IS entered when `segment` touches `ray` at only one point + // (e.g. when `y === start.y !== end.y`) + // since this branch is entered again for the following segment, the two touches cancel out + + var xDifference = (((start.x - x) > (end.x - x)) ? (start.x - x) : (end.x - x)); + if (xDifference >= 0) { + // segment lies at least partially to the right of `p` + var rayEnd = new Point((x + xDifference), y); // right + var ray = new Line(p, rayEnd); + + if (segment.intersect(ray)) { + // an intersection was detected to the right of `p` + numIntersections++; + } + } // else: `segment` lies completely to the left of `p` (i.e. no intersection to the right) + } + + // move to check the next polyline segment + startIndex = endIndex; + } + + // returns `true` for odd numbers of intersections (even-odd algorithm) + return ((numIntersections % 2) === 1); + }, + + // Returns a convex-hull polyline from this polyline. + // Implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan). + // Output polyline starts at the first element of the original polyline that is on the hull, then continues clockwise. + // Minimal polyline is found (only vertices of the hull are reported, no collinear points). + convexHull: function() { + + var i; + var n; + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty + + // step 1: find the starting point - point with the lowest y (if equality, highest x) + var startPoint; + for (i = 0; i < numPoints; i++) { + if (startPoint === undefined) { + // if this is the first point we see, set it as start point + startPoint = points[i]; + + } else if (points[i].y < startPoint.y) { + // start point should have lowest y from all points + startPoint = points[i]; + + } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { + // if two points have the lowest y, choose the one that has highest x + // there are no points to the right of startPoint - no ambiguity about theta 0 + // if there are several coincident start point candidates, first one is reported + startPoint = points[i]; + } + } + + // step 2: sort the list of points + // sorting by angle between line from startPoint to point and the x-axis (theta) + + // step 2a: create the point records = [point, originalIndex, angle] + var sortedPointRecords = []; + for (i = 0; i < numPoints; i++) { + + var angle = startPoint.theta(points[i]); + if (angle === 0) { + angle = 360; // give highest angle to start point + // the start point will end up at end of sorted list + // the start point will end up at beginning of hull points list + } + + var entry = [points[i], i, angle]; + sortedPointRecords.push(entry); + } + + // step 2b: sort the list in place + sortedPointRecords.sort(function(record1, record2) { + // returning a negative number here sorts record1 before record2 + // if first angle is smaller than second, first angle should come before second + + var sortOutput = record1[2] - record2[2]; // negative if first angle smaller + if (sortOutput === 0) { + // if the two angles are equal, sort by originalIndex + sortOutput = record2[1] - record1[1]; // negative if first index larger + // coincident points will be sorted in reverse-numerical order + // so the coincident points with lower original index will be considered first + } + + return sortOutput; + }); + + // step 2c: duplicate start record from the top of the stack to the bottom of the stack + if (sortedPointRecords.length > 2) { + var startPointRecord = sortedPointRecords[sortedPointRecords.length - 1]; + sortedPointRecords.unshift(startPointRecord); + } + + // step 3a: go through sorted points in order and find those with right turns + // we want to get our results in clockwise order + var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull + var hullPointRecords = []; // stack of records with right turns - hull point candidates + + var currentPointRecord; + var currentPoint; + var lastHullPointRecord; + var lastHullPoint; + var secondLastHullPointRecord; + var secondLastHullPoint; + while (sortedPointRecords.length !== 0) { + + currentPointRecord = sortedPointRecords.pop(); + currentPoint = currentPointRecord[0]; + + // check if point has already been discarded + // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' + if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { + // this point had an incorrect turn at some previous iteration of this loop + // this disqualifies it from possibly being on the hull + continue; + } + + var correctTurnFound = false; + while (!correctTurnFound) { + + if (hullPointRecords.length < 2) { + // not enough points for comparison, just add current point + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; + + } else { + lastHullPointRecord = hullPointRecords.pop(); + lastHullPoint = lastHullPointRecord[0]; + secondLastHullPointRecord = hullPointRecords.pop(); + secondLastHullPoint = secondLastHullPointRecord[0]; + + var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); + + if (crossProduct < 0) { + // found a right turn + hullPointRecords.push(secondLastHullPointRecord); + hullPointRecords.push(lastHullPointRecord); + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; + + } else if (crossProduct === 0) { + // the three points are collinear + // three options: + // there may be a 180 or 0 degree angle at lastHullPoint + // or two of the three points are coincident + var THRESHOLD = 1e-10; // we have to take rounding errors into account + var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); + if (abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 + // if the cross product is 0 because the angle is 180 degrees + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found + + } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { + // if the cross product is 0 because two points are the same + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found + + } else if (abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 + // if the cross product is 0 because the angle is 0 degrees + // remove last hull point from hull BUT do not discard it + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // put last hull point back into the sorted point records list + sortedPointRecords.push(lastHullPointRecord); + // we are switching the order of the 0deg and 180deg points + // correct turn not found + } + + } else { + // found a left turn + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter of loop) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found + } + } + } + } + // at this point, hullPointRecords contains the output points in clockwise order + // the points start with lowest-y,highest-x startPoint, and end at the same point + + // step 3b: remove duplicated startPointRecord from the end of the array + if (hullPointRecords.length > 2) { + hullPointRecords.pop(); + } + + // step 4: find the lowest originalIndex record and put it at the beginning of hull + var lowestHullIndex; // the lowest originalIndex on the hull + var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex + n = hullPointRecords.length; + for (i = 0; i < n; i++) { + + var currentHullIndex = hullPointRecords[i][1]; + + if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { + lowestHullIndex = currentHullIndex; + indexOfLowestHullIndexRecord = i; + } + } + + var hullPointRecordsReordered = []; + if (indexOfLowestHullIndexRecord > 0) { + var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); + var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); + hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); + + } else { + hullPointRecordsReordered = hullPointRecords; + } + + var hullPoints = []; + n = hullPointRecordsReordered.length; + for (i = 0; i < n; i++) { + hullPoints.push(hullPointRecordsReordered[i][0]); + } + + return new Polyline(hullPoints); + }, + + // Checks whether two polylines are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { + + if (!p) return false; + + var points = this.points; + var otherPoints = p.points; + + var numPoints = points.length; + if (otherPoints.length !== numPoints) return false; // if the two polylines have different number of points, they cannot be equal + + for (var i = 0; i < numPoints; i++) { + + var point = points[i]; + var otherPoint = p.points[i]; + + // as soon as an inequality is found in points, return false + if (!point.equals(otherPoint)) return false; + } + + // if no inequality found in points, return true + return true; + }, + + intersectionWithLine: function(l) { + var line = new Line(l); + var intersections = []; + var points = this.points; + for (var i = 0, n = points.length - 1; i < n; i++) { + var a = points[i]; + var b = points[i + 1]; + var l2 = new Line(a, b); + var int = line.intersectionWithLine(l2); + if (int) intersections.push(int[0]); + } + return (intersections.length > 0) ? intersections : null; + }, + + isDifferentiable: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return false; + + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + + // as soon as a differentiable line is found between two points, return true + if (line.isDifferentiable()) return true; + } + + // if no differentiable line is found between pairs of points, return false + return false; + }, + + length: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty + + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + length += points[i].distance(points[i + 1]); + } + + return length; + }, + + pointAt: function(ratio) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point + + if (ratio <= 0) return points[0].clone(); + if (ratio >= 1) return points[numPoints - 1].clone(); + + var polylineLength = this.length(); + var length = polylineLength * ratio; + + return this.pointAtLength(length); + }, + + pointAtLength: function(length) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + var l = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + var index = (fromStart ? i : (n - 1 - i)); + + var a = points[index]; + var b = points[index + 1]; + var line = new Line(a, b); + var d = a.distance(b); + + if (length <= (l + d)) { + return line.pointAtLength((fromStart ? 1 : -1) * (length - l)); + } + + l += d; + } + + // if length requested is higher than the length of the polyline, return last endpoint + var lastPoint = (fromStart ? points[numPoints - 1] : points[0]); + return lastPoint.clone(); + }, + + round: function(precision) { + + var points = this.points; + var numPoints = points.length; + + for (var i = 0; i < numPoints; i++) { + points[i].round(precision); + } + + return this; + }, + + scale: function(sx, sy, origin) { + + var points = this.points; + var numPoints = points.length; + + for (var i = 0; i < numPoints; i++) { + points[i].scale(sx, sy, origin); + } + + return this; + }, + + simplify: function(opt = {}) { + + const points = this.points; + if (points.length < 3) return this; // we need at least 3 points + + // TODO: we may also accept startIndex and endIndex to specify where to start and end simplification + const threshold = opt.threshold || 0; // = max distance of middle point from chord to be simplified + + // start at the beginning of the polyline and go forward + let currentIndex = 0; + // we need at least one intermediate point (3 points) in every iteration + // as soon as that stops being true, we know we reached the end of the polyline + while (points[currentIndex + 2]) { + const firstIndex = currentIndex; + const middleIndex = (currentIndex + 1); + const lastIndex = (currentIndex + 2); + + const firstPoint = points[firstIndex]; + const middlePoint = points[middleIndex]; + const lastPoint = points[lastIndex]; + + const chord = new Line(firstPoint, lastPoint); // = connection between first and last point + const closestPoint = chord.closestPoint(middlePoint); // = closest point on chord from middle point + const closestPointDistance = closestPoint.distance(middlePoint); + if (closestPointDistance <= threshold) { + // middle point is close enough to the chord = simplify + // 1) remove middle point: + points.splice(middleIndex, 1); + // 2) in next iteration, investigate the newly-created triplet of points + // - do not change `currentIndex` + // = (first point stays, point after removed point becomes middle point) + } else { + // middle point is far from the chord + // 1) preserve middle point + // 2) in next iteration, move `currentIndex` by one step: + currentIndex += 1; + // = (point after first point becomes first point) + } + } + + // `points` array was modified in-place + return this; + }, + + tangentAt: function(ratio) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point + + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + + var polylineLength = this.length(); + var length = polylineLength * ratio; + + return this.tangentAtLength(length); + }, + + tangentAtLength: function(length) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + var lastValidLine; // differentiable (with a tangent) + var l = 0; // length so far + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + var index = (fromStart ? i : (n - 1 - i)); + + var a = points[index]; + var b = points[index + 1]; + var line = new Line(a, b); + var d = a.distance(b); + + if (line.isDifferentiable()) { // has a tangent line (line length is not 0) + if (length <= (l + d)) { + return line.tangentAtLength((fromStart ? 1 : -1) * (length - l)); + } + + lastValidLine = line; + } + + l += d; + } + + // if length requested is higher than the length of the polyline, return last valid endpoint + if (lastValidLine) { + var ratio = (fromStart ? 1 : 0); + return lastValidLine.tangentAt(ratio); + } + + // if no valid line, return null + return null; + }, + + toString: function() { + + return this.points + ''; + }, + + translate: function(tx, ty) { + + var points = this.points; + var numPoints = points.length; + + for (var i = 0; i < numPoints; i++) { + points[i].translate(tx, ty); + } + + return this; + }, + + // Return svgString that can be used to recreate this line. + serialize: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return ''; // if points array is empty + + var output = ''; + for (var i = 0; i < numPoints; i++) { + + var point = points[i]; + output += point.x + ',' + point.y + ' '; + } + + return output.trim(); + } +}; + +Object.defineProperty(Polyline.prototype, 'start', { + // Getter for the first point of the polyline. + + configurable: true, + + enumerable: true, + + get: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + + return this.points[0]; + }, +}); + +Object.defineProperty(Polyline.prototype, 'end', { + // Getter for the last point of the polyline. + + configurable: true, + + enumerable: true, + + get: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + + return this.points[numPoints - 1]; + }, +}); diff --git a/src/g/rect.mjs b/src/g/rect.mjs new file mode 100644 index 0000000000..56f1acf87a --- /dev/null +++ b/src/g/rect.mjs @@ -0,0 +1,543 @@ +import { toRad } from './geometry.helpers.mjs'; +import { Line } from './line.mjs'; +import { Point } from './point.mjs'; +import { Ellipse } from './ellipse.mjs'; + +const math = Math; +const abs = math.abs; +const cos = math.cos; +const sin = math.sin; +const min = math.min; +const max = math.max; +const round = math.round; +const pow = math.pow; + +export const Rect = function(x, y, w, h) { + + if (!(this instanceof Rect)) { + return new Rect(x, y, w, h); + } + + if ((Object(x) === x)) { + y = x.y; + w = x.width; + h = x.height; + x = x.x; + } + + this.x = x === undefined ? 0 : x; + this.y = y === undefined ? 0 : y; + this.width = w === undefined ? 0 : w; + this.height = h === undefined ? 0 : h; +}; + +Rect.fromEllipse = function(e) { + + e = new Ellipse(e); + return new Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); +}; + +Rect.fromPointUnion = function(...points) { + + if (points.length === 0) return null; + + const p = new Point(); + let minX, minY, maxX, maxY; + minX = minY = Infinity; + maxX = maxY = -Infinity; + + for (let i = 0; i < points.length; i++) { + p.update(points[i]); + const x = p.x; + const y = p.y; + + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + + return new Rect(minX, minY, maxX - minX, maxY - minY); +}; + +Rect.fromRectUnion = function(...rects) { + + if (rects.length === 0) return null; + + const r = new Rect(); + let minX, minY, maxX, maxY; + minX = minY = Infinity; + maxX = maxY = -Infinity; + + for (let i = 0; i < rects.length; i++) { + r.update(rects[i]); + const x = r.x; + const y = r.y; + const mX = x + r.width; + const mY = y + r.height; + + if (x < minX) minX = x; + if (mX > maxX) maxX = mX; + if (y < minY) minY = y; + if (mY > maxY) maxY = mY; + } + + return new Rect(minX, minY, maxX - minX, maxY - minY); +}; + +Rect.prototype = { + + // Find my bounding box when I'm rotated with the center of rotation in the center of me. + // @return r {rectangle} representing a bounding box + bbox: function(angle) { + + if (!angle) return this.clone(); + + var theta = toRad(angle); + var st = abs(sin(theta)); + var ct = abs(cos(theta)); + var w = this.width * ct + this.height * st; + var h = this.width * st + this.height * ct; + return new Rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h); + }, + + bottomLeft: function() { + + return new Point(this.x, this.y + this.height); + }, + + bottomLine: function() { + + return new Line(this.bottomLeft(), this.bottomRight()); + }, + + bottomMiddle: function() { + + return new Point(this.x + this.width / 2, this.y + this.height); + }, + + center: function() { + + return new Point(this.x + this.width / 2, this.y + this.height / 2); + }, + + clone: function() { + + return new Rect(this); + }, + + // @return {bool} true if point p is inside me. + containsPoint: function(p) { + + p = new Point(p); + return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height; + }, + + // @return {bool} true if rectangle `r` is inside me. + containsRect: function(r) { + + var r0 = new Rect(this).normalize(); + var r1 = new Rect(r).normalize(); + var w0 = r0.width; + var h0 = r0.height; + var w1 = r1.width; + var h1 = r1.height; + + if (!w0 || !h0 || !w1 || !h1) { + // At least one of the dimensions is 0 + return false; + } + + var x0 = r0.x; + var y0 = r0.y; + var x1 = r1.x; + var y1 = r1.y; + + w1 += x1; + w0 += x0; + h1 += y1; + h0 += y0; + + return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0; + }, + + corner: function() { + + return new Point(this.x + this.width, this.y + this.height); + }, + + // @return {boolean} true if rectangles are equal. + equals: function(r) { + + var mr = (new Rect(this)).normalize(); + var nr = (new Rect(r)).normalize(); + return mr.x === nr.x && mr.y === nr.y && mr.width === nr.width && mr.height === nr.height; + }, + + // inflate by dx and dy, recompute origin [x, y] + // @param dx {delta_x} representing additional size to x + // @param dy {delta_y} representing additional size to y - + // dy param is not required -> in that case y is sized by dx + inflate: function(dx, dy) { + + if (dx === undefined) { + dx = 0; + } + + if (dy === undefined) { + dy = dx; + } + + this.x -= dx; + this.y -= dy; + this.width += 2 * dx; + this.height += 2 * dy; + + return this; + }, + + // @return {rect} if rectangles intersect, {null} if not. + intersect: function(r) { + + var myOrigin = this.origin(); + var myCorner = this.corner(); + var rOrigin = r.origin(); + var rCorner = r.corner(); + + // No intersection found + if (rCorner.x <= myOrigin.x || + rCorner.y <= myOrigin.y || + rOrigin.x >= myCorner.x || + rOrigin.y >= myCorner.y) return null; + + var x = max(myOrigin.x, rOrigin.x); + var y = max(myOrigin.y, rOrigin.y); + + return new Rect(x, y, min(myCorner.x, rCorner.x) - x, min(myCorner.y, rCorner.y) - y); + }, + + intersectionWithLine: function(line) { + + var r = this; + var rectLines = [r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine()]; + var points = []; + var dedupeArr = []; + var pt, i; + + var n = rectLines.length; + for (i = 0; i < n; i++) { + + pt = line.intersect(rectLines[i]); + if (pt !== null && dedupeArr.indexOf(pt.toString()) < 0) { + points.push(pt); + dedupeArr.push(pt.toString()); + } + } + + return points.length > 0 ? points : null; + }, + + // Find point on my boundary where line starting + // from my center ending in point p intersects me. + // @param {number} angle If angle is specified, intersection with rotated rectangle is computed. + intersectionWithLineFromCenterToPoint: function(p, angle) { + + p = new Point(p); + var center = new Point(this.x + this.width / 2, this.y + this.height / 2); + var result; + + if (angle) p.rotate(center, angle); + + // (clockwise, starting from the top side) + var sides = [ + this.topLine(), + this.rightLine(), + this.bottomLine(), + this.leftLine() + ]; + var connector = new Line(center, p); + + for (var i = sides.length - 1; i >= 0; --i) { + var intersection = sides[i].intersection(connector); + if (intersection !== null) { + result = intersection; + break; + } + } + if (result && angle) result.rotate(center, -angle); + return result; + }, + + leftLine: function() { + + return new Line(this.topLeft(), this.bottomLeft()); + }, + + leftMiddle: function() { + + return new Point(this.x, this.y + this.height / 2); + }, + + maxRectScaleToFit: function(rect, origin) { + + rect = new Rect(rect); + origin || (origin = rect.center()); + + var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4; + var ox = origin.x; + var oy = origin.y; + + // Here we find the maximal possible scale for all corner points (for x and y axis) of the rectangle, + // so when the scale is applied the point is still inside the rectangle. + + sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity; + + // Top Left + var p1 = rect.topLeft(); + if (p1.x < ox) { + sx1 = (this.x - ox) / (p1.x - ox); + } + if (p1.y < oy) { + sy1 = (this.y - oy) / (p1.y - oy); + } + // Bottom Right + var p2 = rect.bottomRight(); + if (p2.x > ox) { + sx2 = (this.x + this.width - ox) / (p2.x - ox); + } + if (p2.y > oy) { + sy2 = (this.y + this.height - oy) / (p2.y - oy); + } + // Top Right + var p3 = rect.topRight(); + if (p3.x > ox) { + sx3 = (this.x + this.width - ox) / (p3.x - ox); + } + if (p3.y < oy) { + sy3 = (this.y - oy) / (p3.y - oy); + } + // Bottom Left + var p4 = rect.bottomLeft(); + if (p4.x < ox) { + sx4 = (this.x - ox) / (p4.x - ox); + } + if (p4.y > oy) { + sy4 = (this.y + this.height - oy) / (p4.y - oy); + } + + return { + sx: min(sx1, sx2, sx3, sx4), + sy: min(sy1, sy2, sy3, sy4) + }; + }, + + maxRectUniformScaleToFit: function(rect, origin) { + + var scale = this.maxRectScaleToFit(rect, origin); + return min(scale.sx, scale.sy); + }, + + // Move and expand me. + // @param r {rectangle} representing deltas + moveAndExpand: function(r) { + + this.x += r.x || 0; + this.y += r.y || 0; + this.width += r.width || 0; + this.height += r.height || 0; + return this; + }, + + // Normalize the rectangle; i.e., make it so that it has a non-negative width and height. + // If width < 0 the function swaps the left and right corners, + // and it swaps the top and bottom corners if height < 0 + // like in http://qt-project.org/doc/qt-4.8/qrectf.html#normalized + normalize: function() { + + var newx = this.x; + var newy = this.y; + var newwidth = this.width; + var newheight = this.height; + if (this.width < 0) { + newx = this.x + this.width; + newwidth = -this.width; + } + if (this.height < 0) { + newy = this.y + this.height; + newheight = -this.height; + } + this.x = newx; + this.y = newy; + this.width = newwidth; + this.height = newheight; + return this; + }, + + // Offset me by the specified amount. + offset: function(dx, dy) { + + // pretend that this is a point and call offset() + // rewrites x and y according to dx and dy + return Point.prototype.offset.call(this, dx, dy); + }, + + origin: function() { + + return new Point(this.x, this.y); + }, + + // @return {point} a point on my boundary nearest to the given point. + // @see Squeak Smalltalk, Rectangle>>pointNearestTo: + pointNearestToPoint: function(point) { + + point = new Point(point); + if (this.containsPoint(point)) { + var side = this.sideNearestToPoint(point); + switch (side) { + case 'right': + return new Point(this.x + this.width, point.y); + case 'left': + return new Point(this.x, point.y); + case 'bottom': + return new Point(point.x, this.y + this.height); + case 'top': + return new Point(point.x, this.y); + } + } + return point.adhereToRect(this); + }, + + rightLine: function() { + + return new Line(this.topRight(), this.bottomRight()); + }, + + rightMiddle: function() { + + return new Point(this.x + this.width, this.y + this.height / 2); + }, + + round: function(precision) { + + let f = 1; // case 0 + if (precision) { + switch (precision) { + case 1: f = 10; break; + case 2: f = 100; break; + case 3: f = 1000; break; + default: f = pow(10, precision); break; + } + } + + this.x = round(this.x * f) / f; + this.y = round(this.y * f) / f; + this.width = round(this.width * f) / f; + this.height = round(this.height * f) / f; + return this; + }, + + // Scale rectangle with origin. + scale: function(sx, sy, origin) { + + origin = this.origin().scale(sx, sy, origin); + this.x = origin.x; + this.y = origin.y; + this.width *= sx; + this.height *= sy; + return this; + }, + + // @return {string} (left|right|top|bottom) side which is nearest to point + // @see Squeak Smalltalk, Rectangle>>sideNearestTo: + sideNearestToPoint: function(point) { + + point = new Point(point); + var distToLeft = point.x - this.x; + var distToRight = (this.x + this.width) - point.x; + var distToTop = point.y - this.y; + var distToBottom = (this.y + this.height) - point.y; + var closest = distToLeft; + var side = 'left'; + + if (distToRight < closest) { + closest = distToRight; + side = 'right'; + } + if (distToTop < closest) { + closest = distToTop; + side = 'top'; + } + if (distToBottom < closest) { + // closest = distToBottom; + side = 'bottom'; + } + return side; + }, + + snapToGrid: function(gx, gy) { + + var origin = this.origin().snapToGrid(gx, gy); + var corner = this.corner().snapToGrid(gx, gy); + this.x = origin.x; + this.y = origin.y; + this.width = corner.x - origin.x; + this.height = corner.y - origin.y; + return this; + }, + + toJSON: function() { + + return { x: this.x, y: this.y, width: this.width, height: this.height }; + }, + + topLine: function() { + + return new Line(this.topLeft(), this.topRight()); + }, + + topMiddle: function() { + + return new Point(this.x + this.width / 2, this.y); + }, + + topRight: function() { + + return new Point(this.x + this.width, this.y); + }, + + toString: function() { + + return this.origin().toString() + ' ' + this.corner().toString(); + }, + + // @return {rect} representing the union of both rectangles. + union: function(rect) { + + return Rect.fromRectUnion(this, rect); + }, + + update: function(x, y, w, h) { + + if ((Object(x) === x)) { + y = x.y; + w = x.width; + h = x.height; + x = x.x; + } + + this.x = x || 0; + this.y = y || 0; + this.width = w || 0; + this.height = h || 0; + return this; + } +}; + +Rect.prototype.bottomRight = Rect.prototype.corner; + +Rect.prototype.topLeft = Rect.prototype.origin; + +Rect.prototype.translate = Rect.prototype.offset; + +// For backwards compatibility: +export const rect = Rect;