我遇到了同样的问题:那里的每个动画包似乎都使用贝塞尔曲线来控制一段时间内的值,但是没有关于如何将贝塞尔曲线实现为 y(x) 函数的信息。所以这就是我想出的。
二维空间中的标准三次贝塞尔曲线可以定义为四个点P0=(x0, y0) .. P3=(x3, y3).
P0 和 P3 是曲线的终点,而 P1 和 P2 是手柄影响其形状。使用参数 t ϵ [0, 1],然后可以使用方程确定沿曲线的任何给定点的 x 和 y 坐标
A) x = (1-t)3x0 + 3t(1-t)2x1 + 3t2(1-t)x2 + t3x3和
B) y = (1-t)3y0 + 3t(1-t)2y1 + 3t2(1-t)y2 + t3y3 .
我们想要的是一个函数 y(x),给定一个 x 坐标,它将返回曲线的相应 y 坐标。为此,曲线必须从左到右单调移动,这样它就不会在不同的 y 位置上多次占据相同的 x 坐标。确保这一点的最简单方法是限制输入点,使 x03 和 x1, x2 ε [x0, x3]。换句话说,P0 必须在 P3 的左侧,两个句柄之间。
为了计算给定 x 的 y,我们必须首先从 x 确定 t。从 t 得到 y 就是将 t 应用于方程 B 的简单问题。
我看到了两种方法来确定给定 y 的 t。
首先,您可以尝试对 t 进行二分搜索。从下限 0 和上限 1 开始,通过方程 A 计算 t 的这些值的 x。继续平分区间,直到获得合理接近的近似值。虽然这应该可以正常工作,但它既不会特别快也不会非常精确(至少不能同时使用两者)。
第二种方法是实际求解方程 A 的 t。这有点难以实现,因为方程是三次的。另一方面,计算变得非常快速并产生精确的结果。
方程 A 可以改写为
(-x0+3x1-3x2+x3)t3 + (3x0-6x1+3x2)t2 + (-3x0+3x1)t + (x0-x) = 0.
插入 x0..x3 的实际值,我们得到 at3 形式的三次方程+ bt2 + c*t + d = 0 我们知道在 [0, 1] 内只有一个解。我们现在可以使用this Stack Overflow answer 中发布的算法来求解这个方程。
以下是一个演示这种方法的小 C# 类。将其转换为您选择的语言应该足够简单。
using System;
public class Point {
public Point(double x, double y) {
X = x;
Y = y;
}
public double X { get; private set; }
public double Y { get; private set; }
}
public class BezierCurve {
public BezierCurve(Point p0, Point p1, Point p2, Point p3) {
P0 = p0;
P1 = p1;
P2 = p2;
P3 = p3;
}
public Point P0 { get; private set; }
public Point P1 { get; private set; }
public Point P2 { get; private set; }
public Point P3 { get; private set; }
public double? GetY(double x) {
// Determine t
double t;
if (x == P0.X) {
// Handle corner cases explicitly to prevent rounding errors
t = 0;
} else if (x == P3.X) {
t = 1;
} else {
// Calculate t
double a = -P0.X + 3 * P1.X - 3 * P2.X + P3.X;
double b = 3 * P0.X - 6 * P1.X + 3 * P2.X;
double c = -3 * P0.X + 3 * P1.X;
double d = P0.X - x;
double? tTemp = SolveCubic(a, b, c, d);
if (tTemp == null) return null;
t = tTemp.Value;
}
// Calculate y from t
return Cubed(1 - t) * P0.Y
+ 3 * t * Squared(1 - t) * P1.Y
+ 3 * Squared(t) * (1 - t) * P2.Y
+ Cubed(t) * P3.Y;
}
// Solves the equation ax³+bx²+cx+d = 0 for x ϵ ℝ
// and returns the first result in [0, 1] or null.
private static double? SolveCubic(double a, double b, double c, double d) {
if (a == 0) return SolveQuadratic(b, c, d);
if (d == 0) return 0;
b /= a;
c /= a;
d /= a;
double q = (3.0 * c - Squared(b)) / 9.0;
double r = (-27.0 * d + b * (9.0 * c - 2.0 * Squared(b))) / 54.0;
double disc = Cubed(q) + Squared(r);
double term1 = b / 3.0;
if (disc > 0) {
double s = r + Math.Sqrt(disc);
s = (s < 0) ? -CubicRoot(-s) : CubicRoot(s);
double t = r - Math.Sqrt(disc);
t = (t < 0) ? -CubicRoot(-t) : CubicRoot(t);
double result = -term1 + s + t;
if (result >= 0 && result <= 1) return result;
} else if (disc == 0) {
double r13 = (r < 0) ? -CubicRoot(-r) : CubicRoot(r);
double result = -term1 + 2.0 * r13;
if (result >= 0 && result <= 1) return result;
result = -(r13 + term1);
if (result >= 0 && result <= 1) return result;
} else {
q = -q;
double dum1 = q * q * q;
dum1 = Math.Acos(r / Math.Sqrt(dum1));
double r13 = 2.0 * Math.Sqrt(q);
double result = -term1 + r13 * Math.Cos(dum1 / 3.0);
if (result >= 0 && result <= 1) return result;
result = -term1 + r13 * Math.Cos((dum1 + 2.0 * Math.PI) / 3.0);
if (result >= 0 && result <= 1) return result;
result = -term1 + r13 * Math.Cos((dum1 + 4.0 * Math.PI) / 3.0);
if (result >= 0 && result <= 1) return result;
}
return null;
}
// Solves the equation ax² + bx + c = 0 for x ϵ ℝ
// and returns the first result in [0, 1] or null.
private static double? SolveQuadratic(double a, double b, double c) {
double result = (-b + Math.Sqrt(Squared(b) - 4 * a * c)) / (2 * a);
if (result >= 0 && result <= 1) return result;
result = (-b - Math.Sqrt(Squared(b) - 4 * a * c)) / (2 * a);
if (result >= 0 && result <= 1) return result;
return null;
}
private static double Squared(double f) { return f * f; }
private static double Cubed(double f) { return f * f * f; }
private static double CubicRoot(double f) { return Math.Pow(f, 1.0 / 3.0); }
}