UNPKG

@js-draw/math

Version:
151 lines (150 loc) 5.84 kB
import { Vec2 } from '../Vec2.mjs'; import solveQuadratic from '../polynomial/solveQuadratic.mjs'; import BezierJSWrapper from './BezierJSWrapper.mjs'; import Rect2 from './Rect2.mjs'; /** * Represents a 2D [Bézier curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). * * Example: * ```ts,runnable,console * import { QuadraticBezier, Vec2 } from '@js-draw/math'; * * const startPoint = Vec2.of(4, 3); * const controlPoint = Vec2.of(1, 1); * const endPoint = Vec2.of(1, 3); * * const curve = new QuadraticBezier( * startPoint, * controlPoint, * endPoint, * ); * * console.log('Curve:', curve); * ``` * * **Note**: Some Bézier operations internally use the `bezier-js` library. */ export class QuadraticBezier extends BezierJSWrapper { constructor( // Start point p0, // Control point p1, // End point p2) { super(); this.p0 = p0; this.p1 = p1; this.p2 = p2; } /** * Returns a component of a quadratic Bézier curve at t, where p0,p1,p2 are either all x or * all y components of the target curve. */ static componentAt(t, p0, p1, p2) { return p0 + t * (-2 * p0 + 2 * p1) + t * t * (p0 - 2 * p1 + p2); } static derivativeComponentAt(t, p0, p1, p2) { return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2); } static secondDerivativeComponentAt(t, p0, p1, p2) { return 2 * (p0 - 2 * p1 + p2); } /** * @returns the curve evaluated at `t`. * * `t` should be a number in `[0, 1]`. */ at(t) { if (t === 0) return this.p0; if (t === 1) return this.p2; const p0 = this.p0; const p1 = this.p1; const p2 = this.p2; return Vec2.of(QuadraticBezier.componentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.componentAt(t, p0.y, p1.y, p2.y)); } derivativeAt(t) { const p0 = this.p0; const p1 = this.p1; const p2 = this.p2; return Vec2.of(QuadraticBezier.derivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.derivativeComponentAt(t, p0.y, p1.y, p2.y)); } secondDerivativeAt(t) { const p0 = this.p0; const p1 = this.p1; const p2 = this.p2; return Vec2.of(QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y)); } normal(t) { const tangent = this.derivativeAt(t); return tangent.orthog().normalized(); } /** @returns an overestimate of this shape's bounding box. */ getLooseBoundingBox() { return Rect2.bboxOf([this.p0, this.p1, this.p2]); } /** * @returns the *approximate* distance from `point` to this curve. */ approximateDistance(point) { // We want to minimize f(t) = |B(t) - p|². // Expanding, // f(t) = (Bₓ(t) - pₓ)² + (Bᵧ(t) - pᵧ)² // ⇒ f'(t) = Dₜ(Bₓ(t) - pₓ)² + Dₜ(Bᵧ(t) - pᵧ)² // // Considering just one component, // Dₜ(Bₓ(t) - pₓ)² = 2(Bₓ(t) - pₓ)(DₜBₓ(t)) // = 2(Bₓ(t)DₜBₓ(t) - pₓBₓ(t)) // = 2(p0ₓ + (t)(-2p0ₓ + 2p1ₓ) + (t²)(p0ₓ - 2p1ₓ + p2ₓ) - pₓ)((-2p0ₓ + 2p1ₓ) + 2(t)(p0ₓ - 2p1ₓ + p2ₓ)) // - (pₓ)((-2p0ₓ + 2p1ₓ) + (t)(p0ₓ - 2p1ₓ + p2ₓ)) const A = this.p0.x - point.x; const B = -2 * this.p0.x + 2 * this.p1.x; const C = this.p0.x - 2 * this.p1.x + this.p2.x; // Let A = p0ₓ - pₓ, B = -2p0ₓ + 2p1ₓ, C = p0ₓ - 2p1ₓ + p2ₓ. We then have, // Dₜ(Bₓ(t) - pₓ)² // = 2(A + tB + t²C)(B + 2tC) - (pₓ)(B + 2tC) // = 2(AB + tB² + t²BC + 2tCA + 2tCtB + 2tCt²C) - pₓB - pₓ2tC // = 2(AB + tB² + 2tCA + t²BC + 2t²CB + 2C²t³) - pₓB - pₓ2tC // = 2AB + 2t(B² + 2CA) + 2t²(BC + 2CB) + 4C²t³ - pₓB - pₓ2tC // = 2AB + 2t(B² + 2CA - pₓC) + 2t²(BC + 2CB) + 4C²t³ - pₓB // const D = this.p0.y - point.y; const E = -2 * this.p0.y + 2 * this.p1.y; const F = this.p0.y - 2 * this.p1.y + this.p2.y; // Using D = p0ᵧ - pᵧ, E = -2p0ᵧ + 2p1ᵧ, F = p0ᵧ - 2p1ᵧ + p2ᵧ, we thus have, // f'(t) = 2AB + 2t(B² + 2CA - pₓC) + 2t²(BC + 2CB) + 4C²t³ - pₓB // + 2DE + 2t(E² + 2FD - pᵧF) + 2t²(EF + 2FE) + 4F²t³ - pᵧE const a = 2 * A * B + 2 * D * E - point.x * B - point.y * E; const b = 2 * B * B + 2 * E * E + 2 * C * A + 2 * F * D - point.x * C - point.y * F; const c = 2 * E * F + 2 * B * C + 2 * C * B + 2 * F * E; //const d = 4 * C * C + 4 * F * F; // Thus, // f'(t) = a + bt + ct² + dt³ const fDerivAtZero = a; const f2ndDerivAtZero = b; const f3rdDerivAtZero = 2 * c; // Using the first few terms of a Maclaurin series to approximate f'(t), // f'(t) ≈ f'(0) + t f''(0) + t² f'''(0) / 2 let [min1, min2] = solveQuadratic(f3rdDerivAtZero / 2, f2ndDerivAtZero, fDerivAtZero); // If the quadratic has no solutions, approximate. if (isNaN(min1)) { min1 = 0.25; } if (isNaN(min2)) { min2 = 0.75; } const at1 = this.at(min1); const at2 = this.at(min2); const sqrDist1 = at1.squareDistanceTo(point); const sqrDist2 = at2.squareDistanceTo(point); const sqrDist3 = this.at(0).squareDistanceTo(point); const sqrDist4 = this.at(1).squareDistanceTo(point); return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4)); } getPoints() { return [this.p0, this.p1, this.p2]; } } export default QuadraticBezier;