UNPKG

@mathigon/euclid

Version:

Euclidean geometry classes and tools for JavaScript

1,550 lines (1,530 loc) 52.4 kB
// src/angle.ts import { clamp as clamp4, nearlyEquals as nearlyEquals7 } from "@mathigon/fermat"; // src/arc.ts import { clamp as clamp3, nearlyEquals as nearlyEquals4 } from "@mathigon/fermat"; // src/circle.ts import { nearlyEquals as nearlyEquals3 } from "@mathigon/fermat"; // src/line.ts import { clamp as clamp2, isBetween, nearlyEquals as nearlyEquals2 } from "@mathigon/fermat"; // src/point.ts import { total } from "@mathigon/core"; import { clamp, lerp, nearlyEquals, Random, roundTo, square } from "@mathigon/fermat"; // src/utilities.ts import { mod } from "@mathigon/fermat"; var TWO_PI = 2 * Math.PI; function rad(p, c) { const a = Math.atan2(p.y - (c ? c.y : 0), p.x - (c ? c.x : 0)); return mod(a, TWO_PI); } function findClosest(p, items) { let q = void 0; let d = Infinity; let index = -1; for (const [i, e] of items.entries()) { const q1 = e.project(p); const d1 = Point.distance(p, q1); if (d1 < d) { q = q1; d = d1; index = i; } } return q ? [q, index] : void 0; } // src/point.ts var Point = class _Point { constructor(x = 0, y = 0) { this.x = x; this.y = y; this.type = "point"; } get unitVector() { if (nearlyEquals(this.length, 0)) return new _Point(1, 0); return this.scale(1 / this.length); } get length() { return Math.sqrt(this.x ** 2 + this.y ** 2); } get inverse() { return new _Point(-this.x, -this.y); } get flip() { return new _Point(this.y, this.x); } get perpendicular() { return new _Point(-this.y, this.x); } get array() { return [this.x, this.y]; } /** Finds the perpendicular distance between this point and a line. */ distanceFromLine(l) { return _Point.distance(this, l.project(this)); } /** Clamps this point to specific bounds. */ clamp(bounds, padding = 0) { const x = clamp(this.x, bounds.xMin + padding, bounds.xMax - padding); const y = clamp(this.y, bounds.yMin + padding, bounds.yMax - padding); return new _Point(x, y); } changeCoordinates(originCoords, targetCoords) { const x = targetCoords.xMin + (this.x - originCoords.xMin) / originCoords.dx * targetCoords.dx; const y = targetCoords.yMin + (this.y - originCoords.yMin) / originCoords.dy * targetCoords.dy; return new _Point(x, y); } add(p) { return _Point.sum(this, p); } subtract(p) { return _Point.difference(this, p); } round(inc = 1) { return new _Point(roundTo(this.x, inc), roundTo(this.y, inc)); } floor() { return new _Point(Math.floor(this.x), Math.floor(this.y)); } mod(x, y = x) { return new _Point(this.x % x, this.y % y); } angle(c = ORIGIN) { return rad(this, c); } // Snap to the x or y values of another point snap(p, tolerance = 5) { if (nearlyEquals(this.x, p.x, tolerance)) return new _Point(p.x, this.y); if (nearlyEquals(this.y, p.y, tolerance)) return new _Point(this.x, p.y); return this; } /** Calculates the average of multiple points. */ static average(...points) { const x = total(points.map((p) => p.x)) / points.length; const y = total(points.map((p) => p.y)) / points.length; return new _Point(x, y); } /** Calculates the dot product of two points p1 and p2. */ static dot(p1, p2) { return p1.x * p2.x + p1.y * p2.y; } static sum(p1, p2) { return new _Point(p1.x + p2.x, p1.y + p2.y); } static difference(p1, p2) { return new _Point(p1.x - p2.x, p1.y - p2.y); } /** Returns the Euclidean distance between two points p1 and p2. */ static distance(p1, p2) { return Math.sqrt(square(p1.x - p2.x) + square(p1.y - p2.y)); } /** Returns the Manhattan distance between two points p1 and p2. */ static manhattan(p1, p2) { return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y); } /** Interpolates two points p1 and p2 by a factor of t. */ static interpolate(p1, p2, t = 0.5) { return new _Point(lerp(p1.x, p2.x, t), lerp(p1.y, p2.y, t)); } /** Interpolates a list of multiple points. */ static interpolateList(points, t = 0.5) { const n = points.length - 1; const a = Math.floor(clamp(t, 0, 1) * n); return _Point.interpolate(points[a], points[a + 1], n * t - a); } /** Creates a point from polar coordinates. */ static fromPolar(angle, r = 1) { return new _Point(r * Math.cos(angle), r * Math.sin(angle)); } static random(b) { const x = Random.uniform(b.xMin, b.xMax); const y = Random.uniform(b.yMin, b.yMax); return new _Point(x, y); } static equals(p1, p2, precision) { return nearlyEquals(p1.x, p2.x, precision) && nearlyEquals(p1.y, p2.y, precision); } /** Check if p1, p2 and p3 lie on a straight line. */ static colinear(p1, p2, p3, tolerance) { const dx1 = p1.x - p2.x; const dy1 = p1.y - p2.y; const dx2 = p2.x - p3.x; const dy2 = p2.y - p3.y; return nearlyEquals(dx1 * dy2, dx2 * dy1, tolerance); } // --------------------------------------------------------------------------- /** Transforms this point using a 2x3 matrix m. */ transform(m) { const x = m[0][0] * this.x + m[0][1] * this.y + m[0][2]; const y = m[1][0] * this.x + m[1][1] * this.y + m[1][2]; return new _Point(x, y); } /** Rotates this point by a given angle (in radians) around point `c`. */ rotate(angle, c = ORIGIN) { if (nearlyEquals(angle, 0)) return this; const x0 = this.x - c.x; const y0 = this.y - c.y; const cos = Math.cos(angle); const sin = Math.sin(angle); const x = x0 * cos - y0 * sin + c.x; const y = x0 * sin + y0 * cos + c.y; return new _Point(x, y); } /** Reflects this point across a line l. */ reflect(l) { const v = l.p2.x - l.p1.x; const w = l.p2.y - l.p1.y; const x0 = this.x - l.p1.x; const y0 = this.y - l.p1.y; const mu = (v * y0 - w * x0) / (v * v + w * w); const x = this.x + 2 * mu * w; const y = this.y - 2 * mu * v; return new _Point(x, y); } scale(sx, sy = sx) { return new _Point(this.x * sx, this.y * sy); } shift(x, y = x) { return new _Point(this.x + x, this.y + y); } translate(p) { return this.shift(p.x, p.y); } equals(other, precision) { return _Point.equals(this, other, precision); } toString() { return `point(${this.x},${this.y})`; } }; var ORIGIN = new Point(0, 0); // src/types.ts function isPolygonLike(shape) { return ["polygon", "polyline", "rectangle", "triangle"].includes(shape.type); } function isPolygon(shape) { return ["polygon", "triangle"].includes(shape.type); } function isPolyline(shape) { return shape.type === "polyline"; } function isRectangle(shape) { return shape.type === "rectangle"; } function isLineLike(shape) { return ["line", "ray", "segment"].includes(shape.type); } function isLine(shape) { return shape.type === "line"; } function isRay(shape) { return shape.type === "ray"; } function isSegment(shape) { return shape.type === "segment"; } function isCircle(shape) { return shape.type === "circle"; } function isEllipse(shape) { return shape.type === "ellipse"; } function isArc(shape) { return shape.type === "arc"; } function isSector(shape) { return shape.type === "sector"; } function isAngle(shape) { return shape.type === "angle"; } function isPoint(shape) { return shape.type === "point"; } // src/line.ts var Line = class _Line { constructor(p1, p2) { this.p1 = p1; this.p2 = p2; this.type = "line"; } /* The distance between the two points defining this line. */ get length() { return Point.distance(this.p1, this.p2); } /* The squared distance between the two points defining this line. */ get lengthSquared() { return (this.p1.x - this.p2.x) ** 2 + (this.p1.y - this.p2.y) ** 2; } /** The midpoint of this line. */ get midpoint() { return Point.average(this.p1, this.p2); } /** The slope of this line. */ get slope() { return (this.p2.y - this.p1.y) / (this.p2.x - this.p1.x); } /** The y-axis intercept of this line. */ get intercept() { return this.p1.y - this.slope * this.p1.x; } /** The angle formed between this line and the x-axis. */ get angle() { return rad(this.p2, this.p1); } /** The point representing a unit vector along this line. */ get unitVector() { return this.p2.subtract(this.p1).unitVector; } /** The point representing the perpendicular vector of this line. */ get perpendicularVector() { return new Point(this.p2.y - this.p1.y, this.p1.x - this.p2.x).unitVector; } /** Finds the line parallel to this one, going through point p. */ parallel(p) { return new _Line(p, p.add(this.p2).subtract(this.p1)); } /** Finds the line perpendicular to this one, going through point p. */ perpendicular(p) { const q = this.line.project(p); if (Point.equals(p, q)) return new _Line(q, q.add(this.perpendicularVector.scale(this.length / 2))); return new _Line(q, p); } /** The perpendicular bisector of this line. */ get perpendicularBisector() { return this.perpendicular(this.midpoint); } /** Squared distance between a point and a line. */ distanceSquared(p) { const proj = this.project(p); return (p.x - proj.x) ** 2 + (p.y - proj.y) ** 2; } get line() { return this.type === "line" ? this : new _Line(this.p1, this.p2); } get ray() { return isRay(this) ? this : new Ray(this.p1, this.p2); } get segment() { return isSegment(this) ? this : new Segment(this.p1, this.p2); } // --------------------------------------------------------------------------- /** Signed distance along the line (opposite of .at()). */ offset(p) { const a = Point.difference(this.p2, this.p1); const b = Point.difference(p, this.p1); return Point.dot(a, b) / this.lengthSquared; } /** Projects a point `p` onto this line. */ project(p) { return this.at(this.offset(p)); } /** Returns which side of this line a point p is on (or 0 on the line). */ side(p, tolerance) { const a = Point.difference(this.p2, this.p1); const b = Point.difference(p, this.p1); const d = b.x * a.y - b.y * a.x; return nearlyEquals2(d, 0, tolerance) ? 0 : Math.sign(d); } /** Checks if a point p lies on this line. */ contains(p, tolerance) { return this.side(p, tolerance) === 0; } /** Gets the point at a specific offset along the line (opposite of .offset()). */ at(t) { return Point.interpolate(this.p1, this.p2, t); } // --------------------------------------------------------------------------- transform(m) { return new this.constructor(this.p1.transform(m), this.p2.transform(m)); } /** Rotates this line by a given angle (in radians), optionally around point `c`. */ rotate(a, c = ORIGIN) { if (nearlyEquals2(a, 0)) return this; return new this.constructor(this.p1.rotate(a, c), this.p2.rotate(a, c)); } reflect(l) { return new this.constructor(this.p1.reflect(l), this.p2.reflect(l)); } scale(sx, sy = sx) { return new this.constructor(this.p1.scale(sx, sy), this.p2.scale(sx, sy)); } shift(x, y = x) { return new this.constructor(this.p1.shift(x, y), this.p2.shift(x, y)); } translate(p) { return this.shift(p.x, p.y); } equals(other, tolerance) { return this.contains(other.p1, tolerance) && this.contains(other.p2, tolerance); } toString() { return `line(${this.p1},${this.p2})`; } }; var Ray = class extends Line { constructor() { super(...arguments); this.type = "ray"; } equals(other, tolerance) { if (other.type !== "ray") return false; if (!this.p1.equals(other.p1, tolerance)) return false; if (this.p2.equals(other.p2, tolerance)) return true; return other.contains(this.p2, tolerance) || this.contains(other.p2, tolerance); } contains(p, tolerance) { if (!super.contains(p, tolerance)) return false; const offset = this.offset(p); return nearlyEquals2(offset, 0, tolerance) || offset > 0; } toString() { return `ray(${this.p1},${this.p2})`; } }; var Segment = class _Segment extends Line { constructor() { super(...arguments); this.type = "segment"; } contains(p, tolerance) { if (!super.contains(p, tolerance)) return false; if (this.p1.equals(p, tolerance) || this.p2.equals(p, tolerance)) return true; if (nearlyEquals2(this.p1.x, this.p2.x, tolerance)) { return isBetween(p.y, this.p1.y, this.p2.y); } else { return isBetween(p.x, this.p1.x, this.p2.x); } } project(p) { const a = Point.difference(this.p2, this.p1); const b = Point.difference(p, this.p1); const q = clamp2(Point.dot(a, b) / this.lengthSquared, 0, 1); return this.p1.add(a.scale(q)); } /** Contracts (or expands) a line by a specific ratio. */ contract(x) { return new _Segment(this.at(x), this.at(1 - x)); } equals(other, tolerance, oriented = false) { if (other.type !== "segment") return false; return this.p1.equals(other.p1, tolerance) && this.p2.equals(other.p2, tolerance) || !oriented && this.p1.equals(other.p2, tolerance) && this.p2.equals(other.p1, tolerance); } toString() { return `segment(${this.p1},${this.p2})`; } }; // src/circle.ts var Circle = class _Circle { constructor(c = ORIGIN, r = 1) { this.c = c; this.r = r; this.type = "circle"; } /** The length of the circumference of this circle. */ get circumference() { return TWO_PI * this.r; } /** The area of this circle. */ get area() { return Math.PI * this.r ** 2; } get arc() { const start = this.c.shift(this.r, 0); return new Arc(this.c, start, TWO_PI); } tangentAt(t) { const p1 = this.at(t); const p2 = this.c.rotate(Math.PI / 2, p1); return new Line(p1, p2); } collision(r) { const tX = this.c.x < r.p.x ? r.p.x : this.c.x > r.p.x + r.w ? r.p.x + r.w : this.c.x; const tY = this.c.y < r.p.y ? r.p.y : this.c.y > r.p.y + r.h ? r.p.y + r.h : this.c.y; const d = Point.distance(this.c, new Point(tX, tY)); return d <= this.r; } // --------------------------------------------------------------------------- project(p) { const proj = p.subtract(this.c).unitVector.scale(this.r); return Point.sum(this.c, proj); } at(t) { const a = TWO_PI * t; return this.c.shift(this.r * Math.cos(a), this.r * Math.sin(a)); } offset(p) { return rad(p, this.c) / TWO_PI; } contains(p) { return Point.distance(p, this.c) <= this.r; } // --------------------------------------------------------------------------- transform(m) { const scale = Math.abs(m[0][0]) + Math.abs(m[1][1]); return new _Circle(this.c.transform(m), this.r * scale / 2); } rotate(a, c = ORIGIN) { if (nearlyEquals3(a, 0)) return this; return new _Circle(this.c.rotate(a, c), this.r); } reflect(l) { return new _Circle(this.c.reflect(l), this.r); } scale(sx, sy = sx) { return new _Circle(this.c.scale(sx, sy), this.r * (sx + sy) / 2); } shift(x, y = x) { return new _Circle(this.c.shift(x, y), this.r); } translate(p) { return this.shift(p.x, p.y); } equals(other, tolerance) { return nearlyEquals3(this.r, other.r, tolerance) && this.c.equals(other.c, tolerance); } toString() { return `circle(${this.c},${this.r})`; } }; // src/arc.ts var Arc = class { constructor(c, start, angle) { this.c = c; this.start = start; this.angle = angle; this.type = "arc"; } get circle() { return new Circle(this.c, this.radius); } get radius() { return Point.distance(this.c, this.start); } get end() { return this.start.rotate(this.angle, this.c); } get startAngle() { return rad(this.start, this.c); } contract(p) { return new this.constructor(this.c, this.at(p / 2), this.angle * (1 - p)); } get minor() { if (this.angle <= Math.PI) return this; return new this.constructor(this.c, this.end, TWO_PI - this.angle); } get major() { if (this.angle >= Math.PI) return this; return new this.constructor(this.c, this.end, TWO_PI - this.angle); } get center() { return this.at(0.5); } // --------------------------------------------------------------------------- project(p) { const start = this.startAngle; const end = start + this.angle; let angle = rad(p, this.c); if (end > TWO_PI && angle < end - TWO_PI) angle += TWO_PI; angle = clamp3(angle, start, end); return this.c.shift(this.radius, 0).rotate(angle, this.c); } at(t) { return this.start.rotate(this.angle * t, this.c); } offset(p) { return new Angle(this.start, this.c, p).rad / this.angle; } contains(p) { return p.equals(this.project(p)); } // --------------------------------------------------------------------------- transform(m) { return new this.constructor( this.c.transform(m), this.start.transform(m), this.angle ); } /** Rotates this arc by a given angle (in radians), optionally around point `c`. */ rotate(a, c = ORIGIN) { if (nearlyEquals4(a, 0)) return this; return new this.constructor( this.c.rotate(a, c), this.start.rotate(a, c), this.angle ); } reflect(l) { return new this.constructor( this.c.reflect(l), this.start.reflect(l), this.angle ); } scale(sx, sy = sx) { return new this.constructor( this.c.scale(sx, sy), this.start.scale(sx, sy), this.angle ); } shift(x, y = x) { return new this.constructor( this.c.shift(x, y), this.start.shift(x, y), this.angle ); } translate(p) { return this.shift(p.x, p.y); } equals() { return false; } toString() { return `arc(${this.c},${this.start},${this.angle})`; } }; var Sector = class extends Arc { constructor() { super(...arguments); this.type = "sector"; } contains(p) { return Point.distance(p, this.c) <= this.radius && new Angle(this.start, this.c, p).rad <= this.angle; } toString() { return `sector(${this.c},${this.start},${this.angle})`; } }; // src/polygon.ts import { last, tabulate } from "@mathigon/core"; import { nearlyEquals as nearlyEquals6 } from "@mathigon/fermat"; // src/intersection.ts import { flatten } from "@mathigon/core"; import { isBetween as isBetween2, nearlyEquals as nearlyEquals5, square as square2, subsets } from "@mathigon/fermat"; function liesOnSegment(s, p) { if (nearlyEquals5(s.p1.x, s.p2.x)) return isBetween2(p.y, s.p1.y, s.p2.y); return isBetween2(p.x, s.p1.x, s.p2.x); } function liesOnRay(r, p) { if (nearlyEquals5(r.p1.x, r.p2.x)) return (p.y - r.p1.y) / (r.p2.y - r.p1.y) > 0; return (p.x - r.p1.x) / (r.p2.x - r.p1.x) > 0; } function liesOnArc(a, p) { return isBetween2(a.offset(p), 0, 1); } function lineLineIntersection(l1, l2) { const d1x = l1.p1.x - l1.p2.x; const d1y = l1.p1.y - l1.p2.y; const d2x = l2.p1.x - l2.p2.x; const d2y = l2.p1.y - l2.p2.y; const d = d1x * d2y - d1y * d2x; if (nearlyEquals5(d, 0)) return []; const q1 = l1.p1.x * l1.p2.y - l1.p1.y * l1.p2.x; const q2 = l2.p1.x * l2.p2.y - l2.p1.y * l2.p2.x; const x = q1 * d2x - d1x * q2; const y = q1 * d2y - d1y * q2; return [new Point(x / d, y / d)]; } function circleCircleIntersection(c1, c2) { const d = Point.distance(c1.c, c2.c); if (d > c1.r + c2.r) return []; if (d < Math.abs(c1.r - c2.r)) return []; if (nearlyEquals5(d, 0) && nearlyEquals5(c1.r, c2.r)) return []; if (nearlyEquals5(d, c1.r + c2.r)) return [new Line(c1.c, c2.c).midpoint]; const a = (square2(c1.r) - square2(c2.r) + square2(d)) / (2 * d); const b = Math.sqrt(square2(c1.r) - square2(a)); const px = (c2.c.x - c1.c.x) * a / d + (c2.c.y - c1.c.y) * b / d + c1.c.x; const py = (c2.c.y - c1.c.y) * a / d - (c2.c.x - c1.c.x) * b / d + c1.c.y; const qx = (c2.c.x - c1.c.x) * a / d - (c2.c.y - c1.c.y) * b / d + c1.c.x; const qy = (c2.c.y - c1.c.y) * a / d + (c2.c.x - c1.c.x) * b / d + c1.c.y; return [new Point(px, py), new Point(qx, qy)]; } function lineCircleIntersection(l, c) { const dx = l.p2.x - l.p1.x; const dy = l.p2.y - l.p1.y; const dr2 = square2(dx) + square2(dy); const cx = c.c.x; const cy = c.c.y; const D = (l.p1.x - cx) * (l.p2.y - cy) - (l.p2.x - cx) * (l.p1.y - cy); const disc = square2(c.r) * dr2 - square2(D); if (disc < 0) return []; const xa = D * dy / dr2; const ya = -D * dx / dr2; if (nearlyEquals5(disc, 0)) return [c.c.shift(xa, ya)]; const xb = dx * (dy < 0 ? -1 : 1) * Math.sqrt(disc) / dr2; const yb = Math.abs(dy) * Math.sqrt(disc) / dr2; return [c.c.shift(xa + xb, ya + yb), c.c.shift(xa - xb, ya - yb)]; } function simpleIntersection(a, b) { let results = []; const a1 = isArc(a) ? a.circle : a; const b1 = isArc(b) ? b.circle : b; if (isLineLike(a) && isLineLike(b)) { results = lineLineIntersection(a, b); } else if (isLineLike(a1) && isCircle(b1)) { results = lineCircleIntersection(a1, b1); } else if (isCircle(a1) && isLineLike(b1)) { results = lineCircleIntersection(b1, a1); } else if (isCircle(a1) && isCircle(b1)) { results = circleCircleIntersection(a1, b1); } for (const x of [a, b]) { if (isSegment(x)) results = results.filter((i) => liesOnSegment(x, i)); if (isRay(x)) results = results.filter((i) => liesOnRay(x, i)); if (isArc(x)) results = results.filter((i) => liesOnArc(x, i)); } return results; } function intersections(...elements) { if (elements.length < 2) return []; if (elements.length > 2) { return flatten(subsets(elements, 2).map((e) => intersections(...e))); } let [a, b] = elements; if (isAngle(a)) a = a.shape(true); if (isAngle(b)) b = b.shape(true); if (isPolygonLike(b)) [a, b] = [b, a]; if (isPolygonLike(a)) { const results = isLineLike(b) ? a.points.filter((p) => b.contains(p)) : []; for (const e of a.edges) results.push(...intersections(e, b)); return results; } return simpleIntersection(a, b); } // src/polygon.ts var Polygon = class _Polygon { constructor(...points) { this.type = "polygon"; this.points = points; } get circumference() { if (this.points.length <= 1) return 0; let length = Point.distance(this.points[0], last(this.points)); for (let i = 1; i < this.points.length; ++i) { length += Point.distance(this.points[i - 1], this.points[i]); } return length; } /** * The (signed) area of this polygon. The result is positive if the vertices * are ordered clockwise, and negative otherwise. */ get signedArea() { const p = this.points; const n = p.length; let A = p[n - 1].x * p[0].y - p[0].x * p[n - 1].y; for (let i = 1; i < n; ++i) { A += p[i - 1].x * p[i].y - p[i].x * p[i - 1].y; } return A / 2; } get area() { return Math.abs(this.signedArea); } get centroid() { const p = this.points; const n = p.length; let Cx = 0; for (let i = 0; i < n; ++i) Cx += p[i].x; let Cy = 0; for (let i = 0; i < n; ++i) Cy += p[i].y; return new Point(Cx / n, Cy / n); } get edges() { const n = this.points.length; const edges = []; for (let i = 0; i < n; ++i) { edges.push(new Segment(this.points[i], this.points[(i + 1) % n])); } return edges; } get radius() { const c = this.centroid; const radii = this.points.map((p) => Point.distance(p, c)); return Math.max(...radii); } /** The oriented version of this polygon (vertices in clockwise order). */ get oriented() { if (this.signedArea >= 0) return this; const points = [...this.points].reverse(); return new this.constructor(...points); } /** Checks if two polygons p1 and p2 collide. */ static collision(p1, p2, tolerance) { if (p1.points.some((q) => p2.contains(q))) return true; if (p2.points.some((q) => p1.contains(q))) return true; for (const e1 of p1.edges) { for (const e2 of p2.edges) { if (intersections(e1, e2)[0]) return true; } } return p1.equals(p2, tolerance); } /** Creates a regular polygon. */ static regular(n, radius = 1) { const da = TWO_PI / n; const a0 = Math.PI / 2 - da / 2; const points = tabulate((i) => Point.fromPolar(a0 + da * i, radius), n); return new _Polygon(...points); } /** Interpolates the points of two polygons */ static interpolate(p1, p2, t = 0.5) { const points = p1.points.map( (p, i) => Point.interpolate(p, p2.points[i], t) ); return new _Polygon(...points); } static convexHull(...points) { if (points.length <= 3) return new _Polygon(...points); const sorted = points.sort((a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y); const sortedReverse = sorted.slice(0).reverse(); const upper = []; const lower = []; for (const [source, target] of [[sorted, upper], [sortedReverse, lower]]) { for (const p of source) { while (target.length >= 2) { const p1 = target[target.length - 1]; const p2 = target[target.length - 2]; if ((p1.x - p2.x) * (p.y - p2.y) >= (p.x - p2.x) * (p1.y - p2.y)) { target.pop(); } else { break; } } target.push(p); } target.pop(); } return new _Polygon(...upper.concat(lower)); } // --------------------------------------------------------------------------- /** * Checks if a point p lies inside this polygon, by using a ray-casting * algorithm and calculating the number of intersections. */ contains(p) { let inside = false; for (const e of this.edges) { if (e.p1.equals(p) || e.contains(p)) return false; if (e.p1.y > p.y === e.p2.y > p.y) continue; const det = (e.p2.x - e.p1.x) / (e.p2.y - e.p1.y); if (p.x < det * (p.y - e.p1.y) + e.p1.x) inside = !inside; } return inside; } at(t) { if (t < 0) t += Math.floor(t); const offset = t * this.circumference; let cum = 0; for (const e of this.edges) { const l = e.length; if (cum + l > offset) return e.at((offset - cum) / l); cum += l; } return this.points[0]; } offset(p) { const edges = this.edges; const proj = findClosest(p, this.edges) || [this.points[0], 0]; let offset = 0; for (let i = 0; i < proj[1]; ++i) offset += edges[i].length; offset += edges[proj[1]].offset(p) * edges[proj[1]].length; return offset / this.circumference; } project(p) { const proj = findClosest(p, this.edges); return proj ? proj[0] : this.points[0]; } /** Center this polygon on a given point or the origin */ centerAt(on = ORIGIN) { return this.translate(on.subtract(this.centroid)); } // --------------------------------------------------------------------------- transform(m) { return new this.constructor(...this.points.map((p) => p.transform(m))); } /** Rotates this polygon by a given angle (in radians), optionally around point `center`. */ rotate(a, center = ORIGIN) { if (nearlyEquals6(a, 0)) return this; const points = this.points.map((p) => p.rotate(a, center)); return new this.constructor(...points); } reflect(line) { const points = this.points.map((p) => p.reflect(line)); return new this.constructor(...points); } scale(sx, sy = sx) { const points = this.points.map((p) => p.scale(sx, sy)); return new this.constructor(...points); } shift(x, y = x) { const points = this.points.map((p) => p.shift(x, y)); return new this.constructor(...points); } translate(p) { return this.shift(p.x, p.y); } equals(other, tolerance, oriented) { const n = this.points.length; if (n !== other.points.length) return false; const p1 = oriented ? this : this.oriented; const p2 = oriented ? other : other.oriented; for (let offset = 0; offset < n; ++offset) { if (p1.points.every((p, i) => p.equals(p2.points[(i + offset) % n], tolerance))) { return true; } } return false; } toString() { return `polygon(${this.points.join(",")})`; } }; var Polyline = class extends Polygon { constructor() { super(...arguments); this.type = "polyline"; } get circumference() { return this.length; } get length() { let length = 0; for (let i = 1; i < this.points.length; ++i) { length += Point.distance(this.points[i - 1], this.points[i]); } return length; } /** @returns {Segment[]} */ get edges() { const edges = []; for (let i = 0; i < this.points.length - 1; ++i) { edges.push(new Segment(this.points[i], this.points[i + 1])); } return edges; } toString() { return `polyline(${this.points.join(",")})`; } }; var Triangle = class extends Polygon { constructor() { super(...arguments); this.type = "triangle"; } get circumcircle() { const [a, b, c] = this.points; const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)); const ux = (a.x ** 2 + a.y ** 2) * (b.y - c.y) + (b.x ** 2 + b.y ** 2) * (c.y - a.y) + (c.x ** 2 + c.y ** 2) * (a.y - b.y); const uy = (a.x ** 2 + a.y ** 2) * (c.x - b.x) + (b.x ** 2 + b.y ** 2) * (a.x - c.x) + (c.x ** 2 + c.y ** 2) * (b.x - a.x); const center = new Point(ux / d, uy / d); const radius = Point.distance(center, this.points[0]); if (isNaN(radius) || radius > Number.MAX_SAFE_INTEGER) return; return new Circle(center, radius); } get incircle() { const edges = this.edges; const sides = edges.map((e) => e.length); const total2 = sides[0] + sides[1] + sides[2]; const [a, b, c] = this.points; const ux = sides[1] * a.x + sides[2] * b.x + sides[0] * c.x; const uy = sides[1] * a.y + sides[2] * b.y + sides[0] * c.y; const center = new Point(ux / total2, uy / total2); const radius = center.distanceFromLine(edges[0]); return isNaN(radius) ? void 0 : new Circle(center, radius); } get orthocenter() { const [a, b, c] = this.points; const h1 = new Line(a, b).perpendicular(c); const h2 = new Line(a, c).perpendicular(b); return intersections(h1, h2)[0]; } }; // src/angle.ts var RAD_TO_DEG = 180 / Math.PI; var DEG_TO_RAD = Math.PI / 180; function toDeg(n) { return n * RAD_TO_DEG; } function toRad(n) { return n * DEG_TO_RAD; } var Angle = class _Angle { constructor(a, b, c) { this.a = a; this.b = b; this.c = c; this.type = "angle"; } static fromDegrees(val) { return _Angle.fromRadians(val * (Math.PI / 180)); } static fromRadians(val) { const p1 = new Point(1, 0); const p2 = p1.rotate(val); return new _Angle(p1, ORIGIN, p2); } /** Checks if `a` and `b` are roughly equivalent (by default, within one degree of eachother) */ static equals(a, b, precision = Math.PI / 360) { return nearlyEquals7(a.rad, b.rad, precision); } /** The size, in radians, of this angle. */ get rad() { const phiA = Math.atan2(this.a.y - this.b.y, this.a.x - this.b.x); const phiC = Math.atan2(this.c.y - this.b.y, this.c.x - this.b.x); let phi = phiC - phiA; if (phi < 0) phi += TWO_PI; return phi; } /** The size, in degrees, of this angle. */ get deg() { return this.rad * 180 / Math.PI; } /** Checks if this angle is right-angled. */ get isRight() { return nearlyEquals7(this.rad, Math.PI / 2, Math.PI / 360); } /** The bisector of this angle. */ get bisector() { if (this.b.equals(this.a)) return void 0; if (this.b.equals(this.c)) return void 0; const phiA = Math.atan2(this.a.y - this.b.y, this.a.x - this.b.x); const phiC = Math.atan2(this.c.y - this.b.y, this.c.x - this.b.x); let phi = (phiA + phiC) / 2; if (phiA > phiC) phi += Math.PI; const x = Math.cos(phi) + this.b.x; const y = Math.sin(phi) + this.b.y; return new Line(this.b, new Point(x, y)); } /** Returns the smaller one of this and its supplementary angle. */ get sup() { return this.rad < Math.PI ? this : new _Angle(this.c, this.b, this.a); } /** Returns the Arc element corresponding to this angle. */ get arc() { return new Arc(this.b, this.a, this.rad); } // --------------------------------------------------------------------------- /** Radius of the arc or sector representing this angle. */ get radius() { return 24 + 20 * (1 - clamp4(this.rad, 0, Math.PI) / Math.PI); } /** Shape object that can be used to draw this angle. */ shape(filled = true, radius, round) { if (this.a.equals(this.b) || this.c.equals(this.b)) return new Polygon(ORIGIN); const angled = this.isRight && !round; if (!radius) radius = angled ? 20 : this.radius; const ba = new Segment(this.b, this.a); const a = ba.at(radius / ba.length); if (angled) { const bc = Point.difference(this.c, this.b).unitVector.scale(radius); if (filled) return new Polygon(this.b, a, a.add(bc), this.b.add(bc)); return new Polyline(a, a.add(bc), this.b.add(bc)); } if (filled) return new Sector(this.b, a, this.rad); return new Arc(this.b, a, this.rad); } // --------------------------------------------------------------------------- // These functions are just included for compatibility with GeoPath project(p) { return this.contains(p) ? p : this.shape(true).project(p); } at() { return this.c; } offset() { return 0; } contains(p) { return this.shape(true).contains(p); } // --------------------------------------------------------------------------- transform(m) { return new _Angle(this.a.transform(m), this.b.transform(m), this.c.transform(m)); } rotate(a, c) { if (nearlyEquals7(a, 0)) return this; return new _Angle(this.a.rotate(a, c), this.b.rotate(a, c), this.c.rotate(a, c)); } reflect(l) { return new _Angle(this.a.reflect(l), this.b.reflect(l), this.c.reflect(l)); } scale(sx, sy = sx) { return new _Angle(this.a.scale(sx, sy), this.b.scale(sx, sy), this.c.scale(sx, sy)); } shift(x, y = x) { return new _Angle(this.a.shift(x, y), this.b.shift(x, y), this.c.shift(x, y)); } translate(p) { return new _Angle(this.a.translate(p), this.b.translate(p), this.c.translate(p)); } equals(a, precision) { return _Angle.equals(a, this, precision); } toString() { return `angle(${this.a},${this.b},${this.c})`; } }; // src/bounds.ts import { isBetween as isBetween4 } from "@mathigon/fermat"; // src/rectangle.ts import { isBetween as isBetween3, nearlyEquals as nearlyEquals8 } from "@mathigon/fermat"; var Rectangle = class _Rectangle { constructor(p, w = 1, h = w) { this.p = p; this.w = w; this.h = h; this.type = "rectangle"; } /** Creates the smallest rectangle containing all given points. */ static aroundPoints(points) { let xMin = Infinity; let xMax = -Infinity; let yMin = Infinity; let yMax = -Infinity; for (const p of points) { xMin = xMin < p.x ? xMin : p.x; xMax = xMax > p.x ? xMax : p.x; yMin = yMin < p.y ? yMin : p.y; yMax = yMax > p.y ? yMax : p.y; } return new _Rectangle(new Point(xMin, yMin), xMax - xMin, yMax - yMin); } get center() { return new Point(this.p.x + this.w / 2, this.p.y + this.h / 2); } get centroid() { return this.center; } get circumference() { return 2 * Math.abs(this.w) + 2 * Math.abs(this.h); } get area() { return Math.abs(this.signedArea); } get signedArea() { return this.w * this.h; } /** @returns {Segment[]} */ get edges() { return this.polygon.edges; } /** @returns {Point[]} */ get points() { return this.polygon.points; } /** A polygon class representing this rectangle. */ get polygon() { const b = new Point(this.p.x + this.w, this.p.y); const c = new Point(this.p.x + this.w, this.p.y + this.h); const d = new Point(this.p.x, this.p.y + this.h); return new Polygon(this.p, b, c, d); } get bounds() { return new Bounds(this.p.x, this.p.x + this.w, this.p.y, this.p.y + this.h); } collision(r) { return this.p.x < r.p.x + r.w && this.p.x + this.w > r.p.x && this.p.y < r.p.y + r.h && this.p.y + this.h > r.p.y || this.equals(r.polygon); } padding(top, right, bottom, left) { return new _Rectangle(this.p.shift(-left, -top), this.w + left + right, this.h + top + bottom); } get unsigned() { if (this.w > 0 && this.h > 0) return this; const p = this.p.shift(this.w < 0 ? this.w : 0, this.h < 0 ? this.h : 0); return new _Rectangle(p, Math.abs(this.w), Math.abs(this.h)); } // --------------------------------------------------------------------------- contains(p, tolerance) { return isBetween3(p.x, this.p.x, this.p.x + this.w, tolerance) && isBetween3(p.y, this.p.y, this.p.y + this.h, tolerance); } project(p) { let q = void 0; for (const e of this.edges) { const q1 = e.project(p); if (!q || Point.distance(p, q1) < Point.distance(p, q)) q = q1; } return q; } at(t) { return this.polygon.at(t); } offset(p) { return this.polygon.offset(p); } get oriented() { return this.polygon.oriented; } // --------------------------------------------------------------------------- transform(m) { return this.polygon.transform(m); } /** Rotates this rectangle by a given angle (in radians), optionally around point `c`. */ rotate(a, c = ORIGIN) { if (nearlyEquals8(a, 0)) return this; return this.polygon.rotate(a, c); } reflect(l) { return this.polygon.reflect(l); } scale(sx, sy = sx) { return new _Rectangle(this.p.scale(sx, sy), this.w * sx, this.h * sy); } shift(x, y = x) { return new _Rectangle(this.p.shift(x, y), this.w, this.h); } translate(p) { return this.shift(p.x, p.y); } equals(other) { return this.polygon.equals(other); } toString() { return `rectangle(${this.p},${this.w},${this.h})`; } }; // src/bounds.ts var Bounds = class _Bounds { /** * Use the `errorHandling` option to decide how to deal with cases where the * min and max values are in the wrong order. */ constructor(xMin, xMax, yMin, yMax, errorHandling) { this.xMin = xMin; this.xMax = xMax; this.yMin = yMin; this.yMax = yMax; if (errorHandling === "swap") { if (this.dx < 0) [this.xMin, this.xMax] = [xMax, xMin]; if (this.dy < 0) [this.yMin, this.yMax] = [yMax, yMin]; } else if (errorHandling === "center") { if (this.dx < 0) this.xMin = this.xMax = (xMin + xMax) / 2; if (this.dy < 0) this.yMin = this.yMax = (yMin + yMax) / 2; } } contains(p) { return this.containsX(p) && this.containsY(p); } containsX(p) { return isBetween4(p.x, this.xMin, this.xMax); } containsY(p) { return isBetween4(p.y, this.yMin, this.yMax); } resize(dx, dy) { return new _Bounds(this.xMin, this.xMax + dx, this.yMin, this.yMax + dy); } get dx() { return this.xMax - this.xMin; } get dy() { return this.yMax - this.yMin; } get xRange() { return [this.xMin, this.xMax]; } get yRange() { return [this.yMin, this.yMax]; } extend(top, right = top, bottom = top, left = right) { return new _Bounds(this.xMin - left, this.xMax + right, this.yMin - top, this.yMax + bottom); } get rect() { return new Rectangle(new Point(this.xMin, this.yMin), this.dx, this.dy); } get center() { return new Point(this.xMin + this.dx / 2, this.yMin + this.dy / 2); } get flip() { return new _Bounds(this.yMin, this.yMax, this.xMin, this.xMax); } }; // src/draw-canvas.ts function drawCanvas(ctx, obj, options = {}) { if (isAngle(obj)) return drawCanvas(ctx, obj.shape(!!options.fill), options); if (options.fill) ctx.fillStyle = options.fill; if (options.opacity) ctx.globalAlpha = options.opacity; if (options.stroke) { ctx.strokeStyle = options.stroke; ctx.lineWidth = options.strokeWidth || 1; if (options.lineCap) ctx.lineCap = options.lineCap; if (options.lineJoin) ctx.lineJoin = options.lineJoin; } ctx.beginPath(); if (isSegment(obj)) { ctx.moveTo(obj.p1.x, obj.p1.y); ctx.lineTo(obj.p2.x, obj.p2.y); } else if (isLineLike(obj)) { if (!options.box) return; let [start, end] = intersections(obj, options.box); if (isRay(obj)) end = obj.p1; if (!start || !end) return; ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); } else if (isCircle(obj)) { ctx.arc(obj.c.x, obj.c.y, obj.r, 0, TWO_PI); } else if (isPolygonLike(obj)) { const points = obj.points; ctx.moveTo(points[0].x, points[0].y); for (const p of points.slice(1)) ctx.lineTo(p.x, p.y); ctx.closePath(); } else if (isPolyline(obj)) { ctx.moveTo(obj.points[0].x, obj.points[0].y); for (const p of obj.points.slice(1)) ctx.lineTo(p.x, p.y); } else if (isEllipse(obj)) { ctx.ellipse(obj.c.x, obj.c.y, obj.a, obj.b, obj.angle, 0, TWO_PI); } if (options.fill) ctx.fill(); if (options.stroke) ctx.stroke(); } // src/draw-svg.ts import { isOneOf } from "@mathigon/core"; import { clamp as clamp5 } from "@mathigon/fermat"; var CIRCLE_MAGIC = 4 * (Math.sqrt(2) - 1) / 3; function drawArc(a, b, c) { const orient = b.x * (c.y - a.y) + a.x * (b.y - c.y) + c.x * (a.y - b.y); const sweep = orient > 0 ? 1 : 0; const size = Point.distance(b, a); return [a.x, `${a.y}A${size}`, size, 0, sweep, 1, c.x, c.y].join(","); } function drawPath(...points) { return `M${points.map((p) => `${p.x},${p.y}`).join("L")}`; } function drawLineMark(x, type) { const p = x.perpendicularVector.scale(6); const n = x.unitVector.scale(3); const m = x.midpoint; switch (type) { case "bar": return drawPath(m.add(p), m.add(p.inverse)); case "bar2": return drawPath(m.add(n).add(p), m.add(n).add(p.inverse)) + drawPath(m.add(n.inverse).add(p), m.add(n.inverse).add(p.inverse)); case "arrow": return drawPath( m.add(n.inverse).add(p), m.add(n), m.add(n.inverse).add(p.inverse) ); case "arrow2": return drawPath( m.add(n.scale(-2)).add(p), m, m.add(n.scale(-2)).add(p.inverse) ) + drawPath(m.add(p), m.add(n.scale(2)), m.add(p.inverse)); default: return ""; } } function arrowPath(start, normal) { if (!start || !normal) return ""; const perp = normal.perpendicular; const a = start.add(normal.scale(9)).add(perp.scale(9)); const b = start.add(normal.scale(9)).add(perp.scale(-9)); return drawPath(a, start, b); } function drawLineArrows(x, type) { let path = ""; if (isOneOf(type, "start", "both")) { path += arrowPath(x.p1, x.unitVector); } if (isOneOf(type, "end", "both")) { path += arrowPath(x.p2, x.unitVector.inverse); } return path; } function drawArcArrows(x, type) { let path = ""; if (isOneOf(type, "start", "both")) { const normal = new Line(x.c, x.start).perpendicularVector.inverse; path += arrowPath(x.start, normal); } if (isOneOf(type, "end", "both")) { const normal = new Line(x.c, x.end).perpendicularVector; path += arrowPath(x.end, normal); } return path; } function getBezierPoints(points, radius) { const length0 = Point.distance(points[0], points[1]); const length1 = Point.distance(points[1], points[2]); const r1 = Math.max(0.1, length0 / 2); const r2 = Math.max(0.1, length1 / 2); const rad2 = Math.min(radius, r1, r2); const d1 = rad2 / length0; const d2 = rad2 / length1; const shift = 1 - CIRCLE_MAGIC; const p1 = Point.interpolate(points[0], points[1], clamp5(1 - d1, 0, 1)); const p2 = Point.interpolate(points[0], points[1], clamp5(1 - d1 * shift, 0, 1)); const p3 = Point.interpolate(points[1], points[2], clamp5(d2 * shift, 0, 1)); const p4 = Point.interpolate(points[1], points[2], clamp5(d2, 0, 1)); return [p1, p2, p3, p4]; } function drawRoundedPath(points, radius, close = false) { if (radius < 0) radius = 0; let path = "M"; if (!close) { path += `${points[0].x} ${points[0].y}`; } else { const p1 = points[points.length - 1]; const p2 = points[0]; const p3 = points[1]; const offsets = getBezierPoints([p1, p2, p3], radius); path += `${offsets[3].x} ${offsets[3].y}`; } for (let index = 0; index < points.length; index++) { if (index < points.length - 2 || close) { const p1 = points[index]; const p2 = points[(index + 1) % points.length]; const p3 = points[(index + 2) % points.length]; const offsets = getBezierPoints([p1, p2, p3], radius).map((p) => `${p.x} ${p.y}`); path += `L${offsets[0]}C${offsets[1]} ${offsets[2]} ${offsets[3]}`; } else if (index === points.length - 2 && !close) { path += `L${points[index + 1].x} ${points[index + 1].y}`; } } return path; } function drawRoundedRect(rect, tl, tr = tl, br = tl, bl = tr) { const { p, w, h } = rect; return `M${p.x} ${p.y + tl}a${tl} ${tl} 0 0 1 ${tl} ${-tl}h${w - tl - tr}a${tr} ${tr} 0 0 1 ${tr} ${tr}v${h - tr - br}a${br} ${br} 0 0 1 ${-br} ${br}h${-w + bl + br}a${bl} ${bl} 0 0 1 ${-bl} ${-bl}Z`; } function drawSVG(obj, options = {}) { if (isAngle(obj)) { const shape = obj.shape(!!options.fill, options.size, options.round); return drawSVG(shape, options); } if (isSegment(obj)) { if (obj.p1.equals(obj.p2)) return ""; let line = drawPath(obj.p1, obj.p2); if (options.mark) line += drawLineMark(obj, options.mark); if (options.arrows) line += drawLineArrows(obj, options.arrows); return line; } if (isRay(obj)) { if (!options.box) return ""; const end = intersections(obj, options.box)[0]; if (!end) return ""; let line = drawPath(obj.p1, end); if (options.mark) line += drawLineMark(obj, options.mark); return line; } if (isLine(obj)) { if (!options.box) return ""; const points = intersections(obj, options.box); if (points.length < 2) return ""; let line = drawPath(points[0], points[1]); if (options.mark) line += drawLineMark(obj, options.mark); return line; } if (isCircle(obj)) { return `M${obj.c.x - obj.r} ${obj.c.y}a${obj.r},${obj.r} 0 1 0 ${2 * obj.r} 0a${obj.r} ${obj.r} 0 1 0 ${-2 * obj.r} 0Z`; } if (isEllipse(obj)) { const [u, v] = obj.majorVertices; const rot = toDeg(obj.angle); return `M${u.x} ${u.y}A${obj.a} ${obj.b} ${rot} 0 0 ${v.x} ${v.y}A${obj.a} ${obj.b} ${rot} 0 0 ${u.x} ${u.y}Z`; } if (isArc(obj)) { let path = `M${drawArc(obj.start, obj.c, obj.end)}`; if (options.arrows) path += drawArcArrows(obj, options.arrows); return path; } if (isSector(obj)) { return `M${obj.c.x} ${obj.c.y} L ${drawArc(obj.start, obj.c, obj.end)}Z`; } if (isPolyline(obj)) { if (options.cornerRadius) return drawRoundedPath(obj.points, options.cornerRadius, false); return drawPath(...obj.points); } if (isPolygon(obj) || isRectangle(obj) && options.cornerRadius) { if (options.cornerRadius) { return drawRoundedPath(obj.points, options.cornerRadius, true); } return `${drawPath(...obj.points)}Z`; } if (isRectangle(obj)) { return `${drawPath(...obj.polygon.points)}Z`; } return ""; } // src/ellipse.ts import { nearlyEquals as nearlyEquals9, quadratic } from "@mathigon/fermat"; var Ellipse = class _Ellipse { /** * @param c Center of the ellipse * @param a Major axis * @param b Minor axis * @param angle The rotation of the major axis of the ellipse. */ constructor(c, a, b, angle = 0) { this.c = c; this.type = "ellipse"; if (a < b) { [a, b] = [b, a]; angle += Math.PI / 2; } this.a = a; this.b = b; this.angle = angle; const f = Math.sqrt(a ** 2 - b ** 2); this.f1 = this.c.add(new Point(-f, 0).rotate(angle)); this.f2 = this.c.add(new Point(f, 0).rotate(angle)); } get rx() { return nearlyEquals9(this.angle, 0) ? this.a : nearlyEquals9(this.angle, Math.PI / 2) ? this.b : void 0; } get ry() { return nearlyEquals9(this.angle, 0) ? this.b : nearlyEquals9(this.angle, Math.PI / 2) ? this.a : void 0; } normalAt(p) { return new Angle(this.f1, p, this.f2).bisector; } /** Intersection between an ellipse and a line. */ intersect(line) { line = line.rotate(-this.angle, this.c); const dx = line.p1.x - line.p2.x; const dy = line.p1.y - line.p2.y; const px = this.c.x - line.p1.x; const py = this.c.y - line.p1.y; const A = (dx / this.a) ** 2 + (dy / this.b) ** 2; const B = 2 * px * dx / this.a ** 2 + 2 * py * dy / this.b ** 2; const C = (px / this.a) ** 2 + (py / this.b) ** 2 - 1; const points = quadratic(A, B, C); return points.map((t) => line.at(t).rotate(this.angle, this.c)); } /** * Creates a new Ellipse. StringLength is the length of string from one foci * to a point on the circumference, to the other foci. */ static fromFoci(f1, f2, stringLength) { const c = Point.distance(f1, f2) / 2; const a = stringLength / 2; const b = Math.sqrt(a ** 2 - c ** 2); const angle = new Line(f1, f2).angle; return new _Ellipse(Point.interpolate(f1, f2), a, b, angle); } // --------------------------------------------------------------------------- get majorVertices() { return [ this.c.add(new Point(-this.a, 0).rotate(this.angle)), this.c.add(new Point(this.a, 0).rotate(this.angle)) ]; } get minorVertices() { return [ this.c.add(new Point(0, -this.b).rotate(this.angle)), this.c.add(new Point(0, this.b).rotate(this.angle)) ]; } get extremes() { const { a, b, angle } = this; const cos = Math.cos(angle); const sin = Math.sin(angle); const sqSum = a ** 2 + b ** 2; const sqDiff = (a ** 2 - b ** 2) * Math.cos(2 * angle); const yMax = Math.sqrt((sqSum - sqDiff) / 2); const xAtYMax = yMax * sqSum * sin * cos / (a ** 2 * sin ** 2 + b ** 2 * cos ** 2); const xMax = Math.sqrt((sqSum + sqDiff) / 2); const yAtXMax = xMax * sqSum * si