1

Мне необходимо анимировать движение чего-либо, для примера, Control'а, точнее его x координаты, так, чтобы x координата зависела от кривой безье EaseInOutQuint(x1=0.83, y1=0, x2=0.17, y2=1). (Прямо как в css!) Но я не могу придумать как это правильно сделать. Для того, чтобы понять как у меня вообще движется Control, я нарисовал два графика. Первый график - сама кривая. Сначала движение идёт медленно, потом проходит очень быстро, и опять идёт медленно.

какой график движения должен быть

И как получается на самом деле:

как есть

Получаю точки для графика так:

void GetBezierPoints()
{
   Dictionary<double, double> points = new Dictionary<double, double>() { };
   for (double i = 0; i < 1.04; i += 0.001)
   {
        i = Math.Round(i, 3);
        points.Add(GetCubicBezier(i, 0.83, 0.17), GetCubicBezier(i, 0, 1));
   }
}
double GetCubicBezier(double t, double p1, double p2)
{
    double[] p = new double[4] { 0, p1, p2, 1 };
    double result =
        Math.Pow(1 - t, 3) * p[0] +
        3 * t * Math.Pow(1 - t, 2) * p[1] +
        3 * Math.Pow(t, 2) * (1 - t) * p[2] +
        Math.Pow(t, 3) * p[3];
    return result;
}

И по идее здесь(где я не могу понять как) я получаю прогресс и двигаю Control в определённую позицию.

void MoveControl()
{
    double fraction = (DateTime.Now - startTime).TotalMilliseconds / animationDuration;
    double progress = GetCubicBezier(fraction, 0, 1); // что здесь должно быть я без понятия
    int nextControlPos = progress * endPos;
    control.Location.X = nextControlPos;
}

Вообще не понимаю как сделать правильно(или сильно туплю и не вижу очевидного :c )

Я использовал решение 3, как наиболее подходящее вопросу.

Cordis
  • 453
  • В анимации где-то должен быть цикл. – aepot Apr 02 '21 at 06:38
  • @aepot цикл - System.Timers.Timer с интервалом 1000/60, который вызывает метод MoveControl. Проблема в том, чтобы определить куда двигать в соответствии с кривой – Cordis Apr 02 '21 at 07:03

1 Answers1

3

Движок анимации

Для того, чтобы воспользоваться любой формулой нелинейной анимации, я написал класс, анимирующий свойство типа Point для любого объекта содержащего такое свойство.

public class PointAnimation
{
    private readonly int _fps;
    private readonly Func<double, double> _curve;
public PointAnimation() : this(60) { }

public PointAnimation(Func&lt;double, double&gt; curve) : this(curve, 60) { }

public PointAnimation(int fps) : this(x =&gt; x, fps) { }

public PointAnimation(Func&lt;double, double&gt; curve, int fps)
    =&gt; (_curve, _fps) = (curve, fps);

public async Task AnimateAsync&lt;T&gt;(T instance, Expression&lt;Func&lt;T, Point&gt;&gt; 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 &lt;= 1; progress += step)
    {
        ms += intDelay;
        int adjDelay = ms - (int)sw.ElapsedMilliseconds;
        if (adjDelay &lt;= 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 принимает аргументы

  1. Количество кадров в секунду
  2. Функция анимации
  3. Либо и то и другое сразу

Метод AnimateAsync принимает аргументы:

  1. Класс любого типа
  2. Свойство типа Point, которое содержит класс, в виде лямбда-выражения, начальная точка будет взята именно из него
  3. Конечная точка
  4. Продолжительность анимации в миллисекундах

С этим классом анимации вы можете использовать любую функцию, только стоит помнить, что входной аргумент изменяется от 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) &lt; _precision)
            return Evaluate(_y1, _y2, midpoint);
        if (estimate &lt; 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;
}
aepot
  • 49,560
  • А можно ли это всё-таки сделать, не избегая безье? Или слишком муторно получится – Cordis Apr 02 '21 at 13:41
  • @Cordis муторно, с ходу я не нашел формулу рисования кривой по 4 точкам, точнее она есть, та что у вас в вопросе очень на похожа на то что надо. Но я не нашел как переложить ее на формулу отношения tcurve = f(t). И примеров нигде нет. Понятно было только что в качетсве исходных данных должны идти 4 точки и t, а на выходе модифицированный t. Я полинтернета перерыл на самом деле, включая англоязычный и более вменяемого, чем в ответе способа сделать ease-in-out, я не нашел. – aepot Apr 02 '21 at 14:09
  • @Cordis второй момент, то что вам надо реализовывать здесь, например есть уже готовое реализованное в WPF - DoubleAnimation и куча анимаций для других типов свойств, готовое, из коробки. Так что если вам надо, чтобы в интерфейсе красиво ползало и скакало, переливаясь цветами и плавно исчезало, изучайте WPF. – aepot Apr 02 '21 at 14:13
  • @Cordis в любом случае, движок написан, можно издеваться над формулами сколько душе угодно. – aepot Apr 02 '21 at 14:20
  • 1
    Передо мной стояла задача именно реализовать движение в зависимости от функции. Control был тут лишь для примера. До асинхронности я уже дошёл, но вот анимации на ней делать не додумался. Походу пойду к своему преподу по математике и буду выпрашивать у него помощь) Спасибо! – Cordis Apr 02 '21 at 14:42
  • @Cordis проверьте, добавил в ответ Безье. Очевидно, X координата здесь не влияет на проекцию. – aepot Apr 02 '21 at 14:59
  • я нашёл реализацию через безье на github. Скоро дополню ответ – Cordis Apr 02 '21 at 15:19
  • @Cordis так я уже дополнил. Внизу ответа. Забирайте и пользуйтесь. :) – aepot Apr 02 '21 at 15:40
  • Так ведь эта реализация и была у меня. И именно она дала второй график, который работает не правильно. Я нашёл код, который используя время возвращает прогресс в таком виде, как на первом графике. По моим исследованиям, если не учитывать X при расчёте, он будет 0.5 – Cordis Apr 02 '21 at 15:44
  • @Cordis ваша ошибка была в том, что вы брали проекцию на ось X, а надо было на ось Y. – aepot Apr 02 '21 at 15:44
  • await Task.Delay((int)delay) не учитывает длительность вычислений. Stopwatch показывает время выполнения >1500мс, вместо заданных 1000мс. обидно, однако:( – Cordis Apr 02 '21 at 17:14
  • @Cordis исправлено в ответе. – aepot Apr 04 '21 at 08:30
  • А можно в виде nuget-пакета с лицензией MIT или Apache!? – Blackmeser May 22 '21 at 02:36
  • @Blackmeser чем вас сделай-сам пакет с лицензией Creative Commons не устраивает? – aepot May 22 '21 at 07:07