Я столкнулся с той же проблемой: кажется, что каждый пакет анимации использует кривые Безье для управления значениями во времени, но нет никакой информации о том, как реализовать кривую Безье как функцию y (x). Итак, вот что я придумала.
Стандартная кубическая кривая Безье в 2D-пространстве может быть определена четырьмя точками P 0 = (x 0 , y 0 ). . P 3 = (x 3 , y 3 ) .
P 0 и P 3 являются конечными точками кривой, в то время как P 1 и P 2 являются ручками, влияющими на ее форму. Используя параметр t ϵ [0, 1], координаты x и y для любой заданной точки вдоль кривой затем можно определить с помощью уравнений
А) х = (1-т) 3 х 0 + 3т (1-т) 2 х 1 + 3т 2 (1-т) х 2 + т 3 х 3 и
B) y = (1-т) 3 y 0 + 3т (1-т) 2 y 1 + 3т 2 (1-т) у 2 + т 3 у 3 .
Нам нужна функция y (x), которая при заданной координате x возвращает соответствующую координату y кривой. Чтобы это работало, кривая должна монотонно двигаться слева направо, чтобы она не занимала одну и ту же координату x более одного раза в разных положениях y. Самый простой способ гарантировать это - ограничить входные точки так, чтобы x 0 3 и x 1 , x 2 ϵ [x 0 , x 3 ] . Другими словами, P 0 должен быть слева от P 3 с двумя ручками между ними.
Чтобы вычислить y для данного x, мы должны сначала определить t из x. Чтобы получить y из t, достаточно просто применить t к уравнению B.
Я вижу два способа определения t для данного y.
Во-первых, вы можете попробовать двоичный поиск для t. Начните с нижней границы 0 и верхней границы 1 и вычислите x для этих значений для t с помощью уравнения A. Продолжайте делить интервал до тех пор, пока не получите достаточно близкое приближение. Хотя это должно работать нормально, оно не будет ни особенно быстрым, ни очень точным (по крайней мере, не одновременно).
Второй подход состоит в том, чтобы фактически решить уравнение A для t. Это немного сложно реализовать, потому что уравнение кубическое. С другой стороны, вычисления становятся очень быстрыми и дают точные результаты.
Уравнение А можно переписать как
(- x 0 + 3x 1 -3x 2 + x 3 ) t 3 + (3x 0 -6x 1 + 3x 2 ) t 2 + (-3x 0 + 3x 1 ) t + (x 0 -x) = 0 .
Вставив ваши фактические значения для x 0 .. x 3 , мы получим кубическое уравнение вида a t 3 + b t 2 + c * t + d = 0 , для которого мы знаем, что в [0, 1] есть только одно решение. Теперь мы можем решить это уравнение, используя алгоритм, подобный тому, который был опубликован в этом ответе переполнения стека .
Ниже приведен небольшой класс 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); }
}