| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type {Point} from './selection_utils.js'; |
| |
| const BEZIER_EPSILON = 1e-7; |
| const CUBIC_BEZIER_SPLINE_SAMPLES = 11; |
| const MAX_NEWTON_METHOD_ITERATIONS = 4; |
| |
| /** |
| * This class calculates a cubic bezier easing function with provided parameters |
| * to mimic the functionality of the CSS easing function. |
| * |
| * This implementation is mainly translated from: |
| * ui/gfx/geometry/cubic_bezier.h |
| */ |
| export class CubicBezier { |
| private p1: Point; |
| private p2: Point; |
| |
| // Polynomial coefficients in form (x,y) |
| private a: Point = {x: 0, y: 0}; |
| private b: Point = {x: 0, y: 0}; |
| private c: Point = {x: 0, y: 0}; |
| |
| // Samples for estimating the spline curve |
| private splineSamples: number[] = []; |
| |
| constructor(x1: number, y1: number, x2: number, y2: number) { |
| this.p1 = {x: x1, y: y1}; |
| this.p2 = {x: x2, y: y2}; |
| this.initCoefficients(this.p1, this.p2); |
| this.initSpline(); |
| } |
| |
| solveForY(x: number): number { |
| x = Math.max(0, Math.min(1, x)); |
| return this.sampleCurveY(this.solveCurveX(x)); |
| } |
| |
| solveCurveX(x: number): number { |
| let t0: number|undefined; |
| let t1: number|undefined; |
| let x2: number|undefined; |
| let d2: number; |
| |
| let t2 = x; |
| |
| // Linear interpolation of spline curve for initial guess. |
| const deltaT = 1 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); |
| for (let i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) { |
| if (x <= this.splineSamples[i]) { |
| t1 = deltaT * i; |
| t0 = t1 - deltaT; |
| t2 = t0 + |
| (t1 - t0) * (x - this.splineSamples[i - 1]) / |
| (this.splineSamples[i] - this.splineSamples[i - 1]); |
| break; |
| } |
| } |
| |
| // Perform a few iterations of Newton's method -- normally very fast. |
| // See https://en.wikipedia.org/wiki/Newton%27s_method. |
| for (let i = 0; i < MAX_NEWTON_METHOD_ITERATIONS; i++) { |
| x2 = this.sampleCurveX(t2) - x; |
| if (Math.abs(x2) < BEZIER_EPSILON) { |
| return t2; |
| } |
| d2 = this.sampleCurveDerivativeX(t2); |
| if (Math.abs(d2) < BEZIER_EPSILON) { |
| break; |
| } |
| t2 = t2 - x2 / d2; |
| } |
| if (x2 !== undefined && Math.abs(x2) < BEZIER_EPSILON) { |
| return t2; |
| } |
| |
| // Fall back to the bisection method for reliability. |
| if (t0 !== undefined && t1 !== undefined) { |
| while (t0 < t1) { |
| x2 = this.sampleCurveX(t2); |
| if (Math.abs(x2 - x) < BEZIER_EPSILON) { |
| return t2; |
| } |
| |
| if (x > x2) { |
| t0 = t2; |
| } else { |
| t1 = t2; |
| } |
| |
| t2 = (t1 + t0) * 0.5; |
| } |
| } |
| |
| // Failed to solve. |
| return t2; |
| } |
| |
| sampleCurveX(t: number): number { |
| // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. |
| // The x values are in the range [0, 1]. So it isn't needed toFinite |
| // clamping. |
| // https://drafts.csswg.org/css-easing-1/#funcdef-cubic-bezier-easing-function-cubic-bezier |
| return ((this.a.x * t + this.b.x) * t + this.c.x) * t; |
| } |
| |
| private sampleCurveDerivativeX(t: number) { |
| return (3 * this.a.x * t + 2 * this.b.x) * t + this.c.x; |
| } |
| |
| |
| private sampleCurveY(t: number): number { |
| return this.toFinite(((this.a.y * t + this.b.y) * t + this.c.y) * t); |
| } |
| |
| private initCoefficients(p1: Point, p2: Point) { |
| // Calculate the polynomial coefficients, implicit first and last control |
| // points are (0,0) and (1,1). First, for x. |
| this.c.x = 3 * p1.x; |
| this.b.x = 3 * (p2.x - p1.x) - this.c.x; |
| this.a.x = 1 - this.c.x - this.b.x; |
| |
| // Now for y. |
| this.c.y = this.toFinite(3 * p1.y); |
| this.b.y = this.toFinite(3 * (p2.y - p1.y) - this.c.y); |
| this.a.y = this.toFinite(1 - this.c.y - this.b.y); |
| } |
| |
| private initSpline() { |
| const deltaT = 1 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); |
| for (let i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) { |
| this.splineSamples[i] = this.sampleCurveX(i * deltaT); |
| } |
| } |
| |
| private toFinite(n: number): number { |
| if (Number.isFinite(n)) { |
| return n; |
| } |
| |
| return n > 0 ? Number.MAX_SAFE_INTEGER : Number.MIN_SAFE_INTEGER; |
| } |
| } |