Движок анимации
Для того, чтобы воспользоваться любой формулой нелинейной анимации, я написал класс, анимирующий свойство типа Point для любого объекта содержащего такое свойство.
public class PointAnimation
{
private readonly int _fps;
private readonly Func<double, double> _curve;
public PointAnimation() : this(60) { }
public PointAnimation(Func<double, double> curve) : this(curve, 60) { }
public PointAnimation(int fps) : this(x => x, fps) { }
public PointAnimation(Func<double, double> curve, int fps)
=> (_curve, _fps) = (curve, fps);
public async Task AnimateAsync<T>(T instance, Expression<Func<T, Point>> exp, Point end, double duration) where T : class
{
string propertyName = ((MemberExpression)exp.Body).Member.Name;
PropertyInfo propertyInfo = typeof(T).GetProperty(propertyName);
Point start = (Point)propertyInfo.GetValue(instance);
Point distance = new Point(end.X - start.X, end.Y - start.Y);
double delay = 1000 / _fps;
double step = delay / duration;
int intDelay = (int)delay;
int ms = 0;
Stopwatch sw = new Stopwatch();
sw.Start();
for (double progress = 0; progress <= 1; progress += step)
{
ms += intDelay;
int adjDelay = ms - (int)sw.ElapsedMilliseconds;
if (adjDelay <= 0)
continue; // слишком много прошло времени, кадр опоздал, пропускаем
double curveProgress = _curve(progress);
double x = start.X + distance.X * curveProgress;
double y = start.Y + distance.Y * curveProgress;
propertyInfo.SetValue(instance, new Point((int)Math.Round(x), (int)Math.Round(y)));
await Task.Delay(adjDelay);
}
sw.Stop();
propertyInfo.SetValue(instance, end);
}
}
Никаких таймеров, всё по-взрослому, только асинхронное программирование. Здесь весь код, я ничего не спрятал.
Конструктор PointAnimation принимает аргументы
- Количество кадров в секунду
- Функция анимации
- Либо и то и другое сразу
Метод AnimateAsync принимает аргументы:
- Класс любого типа
- Свойство типа
Point, которое содержит класс, в виде лямбда-выражения, начальная точка будет взята именно из него
- Конечная точка
- Продолжительность анимации в миллисекундах
С этим классом анимации вы можете использовать любую функцию, только стоит помнить, что входной аргумент изменяется от 0 до 1 и выходной также должен изменяться от 0 до 1.
Формула кривой
Полная формула Безье хорошо подходит для построения графика, но она, как мне кажется, избыточна для проекции кривой Безье на время t. То есть для простого искажения значения времени, чтобы получить нелинейную анимацию.
Математики предлагают следующее: fa(x) = xa / (xa + (1 - x)a)
Что можно описать в виде простого метода
private double EaseInOutBlend(double t, double power)
{
double p = Math.Pow(t, power);
double np = Math.Pow(1 - t, power);
return p / (p + np);
}
Где t меняется от 0 до 1.
И здесь можно управлять кривой с помощью степени, в которую возводится x, кстати, эта степень - не обязательно целое число, можете например возвести в 1.5.
А пользоваться этим всем достаточно легко. Например, пусть будет у вас на форме Label и Button с именами по умолчанию label1 и button1 соответственно.
Тогда можно написать вот такой обработчик клика кнопки.
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
PointAnimation animation = new PointAnimation(p => EaseInOutBlend(p, 2));
Point end = new Point(label1.Location.X + 200, label1.Location.Y + 200);
await animation.AnimateAsync(label1, c => c.Location, end, 1000);
button1.Enabled = true;
}
Тригонометрия
Бонусом можете еще такую функцию попробовать
private double CosBlend(double t)
{
return (1 - Math.Cos(Math.PI * t)) / 2;
}
Кривая Безье
А вот и кривая Безье на основе аппроксимации X при поиске Y.
public class CubicBezier
{
private const double _precision = 0.0001;
private readonly double _x1, _x2, _y1, _y2;
public CubicBezier(double x1, double y1, double x2, double y2)
{
_x1 = x1;
_y1 = y1;
_x2 = x2;
_y2 = y2;
}
private double Evaluate(double a, double b, double m)
{
return 3 * a * (1 - m) * (1 - m) * m
+ 3 * b * (1 - m) * m * m
+ m * m * m;
}
public double Transform(double t)
{
double start = 0.0;
double end = 1.0;
while (true)
{
double midpoint = (start + end) / 2;
double estimate = Evaluate(_x1, _x2, midpoint);
if (Math.Abs(t - estimate) < _precision)
return Evaluate(_y1, _y2, midpoint);
if (estimate < t)
start = midpoint;
else
end = midpoint;
}
}
}
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
CubicBezier bezier = new CubicBezier(0.83, 0.0, 0.17, 1.0);
PointAnimation animation = new PointAnimation(p => bezier.Transform(p));
Point end = new Point(label1.Location.X + 200, label1.Location.Y + 200);
await animation.AnimateAsync(label1, c => c.Location, end, 500);
button1.Enabled = true;
}