Анимируем бабочек в Expression Blend

Накануне мартовского праздника (да и весна началась!) мы будем учиться анимировать бабочек.

Задача

В первом приближении задачу можно поставить следующим образом: созданить анимацию бабочки, машущей крыльями.

Препарирование бабочки

Для работы нам понадобится изображение бабочки, желательно, натуралистичное (за готовыми изображениями направляю в интернеты ;), в png и с прозрачным фоном, например, такое:

Morpho-Diana-icon

Чтобы бабочка “махала” крыльями, это изображение необходимо разрезать не две части и сохранить как отдельные картинки:

 Diana 1и  Diana 2

User Control Butterfly

В проекте Silverlight в Expression Blend импортируем два изображения (можно напрямую перетащить на рабочее пространство):

image

Выделив оба фрагмента, из контекстного меню выбираем “Make Into UserControl” (или просто F8):

image

У нас появился новый контрол Butterfly.

Анимация бабочки

У нас будет три анимации: взлет бабочки, приземление бабочки и взмах крыльями.

Предварительно в нашем контроле нужно сделать следующие изменения:

  1. Дать названия каждому из крыльев, например, “Left” и “Right” — так будет проще ориентироваться.
  2. Меняем положение центра. Для этого в панели “Transform” на вкладке “Translate” выбираем: центральную правую точку для левого крыла и центральную левую для правого:
    image
    (Аналогичное изменение модно сделать на вкладке “Center Point”: 1 и 0.5 для левого крыла, 0 и 0.5 — для правого. Числа эти относительные: {0, 0} — верхний левый угол объекта, {1, 1} — нижний правый.)
  3. Уменьшить масштаб. Для этого в панели “Transform” открываем вкладку “Scale” и для каждого из крыльев выставляем значения 0.75 для X и Y:
    image image
  4. Меняем положение центра проекции. Для этого в панели “Transform” в части “Projection” на вкладке “Center of Rotation” выставляем значения, аналогичные п.2: 1 и 0.5 для левого крыла, 0 и 0.5 — для правого
    image 

Предварительная настройка окончена.

Взлет и посадка

Анимации взлета и посадки будут довольно простыми: при взлете бабочка увеличивается в размере, поднимает и опускает крылья; при посадке — уменьшается, поднимает и опускает крылья.

Начнем со взлета. Переходим в панель “Objects and Timeline”, нажимаем на плюсик для создания новой анимации StoryBoard:

image

image

При этом рабочая область немного изменится — появится линейка времени для записи анимации:image

(В принципе, можно переключить рабочее пространство в режим анимации — F6 или Windows → Workspaces → Animation).

Для взлета достаточно ~0.5 секунды. Выбираем первое крыло, ставим текущее время на 0.5:

image

На вкладке “Scale” панели “Transform” выставляем значения X и Y в 1:

image

Аналогичную операцию проделываем для второго крыла.

При этом на линейке времени вы увидите отметки, что в некоторые моменты времени происходят изменения (если раскрыть объекты, можно увидеть, какие именно свойства изменены):

image

Если проиграть анимацию, бабочка увеличивается в размере.

Давайте теперь добавим взмахи крыльями. Выделим момент времени ~0.25 секунды, “Transform” → “Projection” → “Rotation”, ставим Y=-70 для левого крыла и Y=70 для правого:

image

При этом бабочка должна немного “сжаться”:

image

Можно запустить анимацию и посмотреть, что получилось. На самом деле бабочка поднимается, еще не успев оттолкнуться крыльями, поэтому изменение размера нужно сделать после подъема крыльев.

Самый простой способ внести изменение — выбрать нулевой момент времени и для каждого из крыльев добавить KeyFrame (овал с плюсом):

image 

(Для удобства внизу можно поменять масштаб временной линейки.)

Далее для RenderTransform каждого из крыльев перемещаем отметки на временной шкале в нужные моменты:

image

Теперь стало получше ;)

 

Продолжим посадкой. Все практически аналогично взлету, только наоборот. Для этого создадим новый StoryBoard “Down”.

Чтобы сильно не повторяться, сделаем дупликат (Duplicate) и переименуем в “Down”:

 image

Далее нужно поправить изменение масштабов (RenderTransform для каждого из крыльев): начать с 1 и закончить 0.75:

image

Полет нормальный

Делаем еще один дупликат, называем “Fly”. Удаляем RenderTransform для каждого из крыльев:

image

Сжимаем по времени:

image

На этом анимации закрываем.

Обидеть бабочку каждый может

Следующий шаг — заставить анимации работать последовательно, а бабочку летать.

(Тут нужно было бы воскликнуть: “Открываем Visual Studio и лезем в код”. Но мы воспользуемся редактором Expression Blend, благо много кода писать не придется.)

Начнем с того, что нужно повесить событие, которое будет запускать анимацию по наведению курсора мыши на бабочку:

 public Butterfly()
{
    // Required to initialize variables
 InitializeComponent();
    MouseEnter += ButterflyControl_MouseEnter;
}
       
void ButterflyControl_MouseEnter(object sender, MouseEventArgs e)
{
 Start();
}

Далее (раскрываю карты) нам мы будем запускать бабочку в полет в случайном направлении на случайную дистанцию и приземлять.

Важный момент: при этом нужно блокировать (!) повторный запуск анимации. В данном случае мы обойдемся простым булевским флагом:

 bool isActive = false;
public bool IsActive
{
   get { return isActive; }
}
void ButterflyControl_MouseEnter(object sender, MouseEventArgs e)
{
   if (!isActive)
      Start();
}

Для управления полетом нужно будет добавить еще несколько параметров:

 double angle = 0;
double direction = 0;

double x = 0;

double y = 0;
        
int counter;

static Random RND = new Random();

Обратите внимание на то, что RND сделан статическим, чтобы для разных бабочек не генерировать одни и те же последовательности.

counter — счетчик, сколько раз нужно проиграть анимацию “Fly". На каждом таком шаге бабочка будет немного поворачиваться и пролетать некоторое расстояние в этом направлении.

Start(); и запуск полета
 public void Start()
{
   counter = Buttrefly.RND.Next(20) + 5;
   direction = 20 * Buttrefly.RND.NextDouble() - 10;
         
   Storyboard up = this.Resources["Up"] as Storyboard;
   up.Begin();
   Run();
   isActive = true;
}   

direction — на время полета постоянная случайная составляющая поворота бабочки, чтобы бабочка куда-то устремлялась, а не совсем хаотически летала.

Обратите внимание, что мы цепляем запуск одного события за другое.

Run(); и приземление

Тут нам придется делать несколько важных вещей, поэтому рассмотрим по порядку.

Во-первых, нам нужно будет применить три трансформации: поворот, перемещение по X и перемещение по Y, для каждой из них своя анимация:

 private void Run()
{
   if (counter > 0)
   {
       Storyboard sb = new Storyboard();
       DoubleAnimation rotateAnimation = new DoubleAnimation();
       DoubleAnimation moveXAnimation = new DoubleAnimation();
       DoubleAnimation moveYAnimation = new DoubleAnimation();

Во-вторых, нужно установить время анимации (у нас оно было 0.3c), цель, к которой применяются анимации, и соответствующие свойства цели:

       rotateAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.3));
      moveXAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.3));
      moveYAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.3));
      Storyboard.SetTarget(rotateAnimation, LayoutRoot);
      Storyboard.SetTarget(moveXAnimation, LayoutRoot);
      Storyboard.SetTarget(moveYAnimation, LayoutRoot);
      Storyboard.SetTargetProperty(rotateAnimation, new PropertyPath("(UIControl.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)"));
      Storyboard.SetTargetProperty(moveXAnimation, new PropertyPath("(UIControl.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"));
      Storyboard.SetTargetProperty(moveYAnimation, new PropertyPath("(UIControl.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)"));

В PropertyPath указывается магический путь до нужного свойства ;)

В-третьих, нужно вспомнить немного геометрии и прописать соответствующие трансформации, имея в виду, что угол задается в градусах и по часовой стрелке:

       rotateAnimation.From = angle;
      angle = angle + direction + (80 * Buttrefly.RND.NextDouble() - 40);
      rotateAnimation.To = angle;
                
      moveXAnimation.From = x;
      x = x + (40 * Math.Sin((angle) / 180 * Math.PI));
      moveXAnimation.To = x;
                
      moveYAnimation.From = y;
      y = y - (40 * Math.Cos((angle) / 180 * Math.PI));
      moveYAnimation.To = y;

В-четвертых, объединяем анимации в одну StoryBoard, прописываем событие на завершение, запускаем анимацию трансформации и анимацию “Fly” махания крыльями и уменьшаем счетчик:

        sb.Children.Add(rotateAnimation);
       sb.Children.Add(moveXAnimation);
       sb.Children.Add(moveYAnimation);       
       sb.Completed += new EventHandler(sb_Completed);
       sb.Begin();
       (this.Resources["Fly"] as Storyboard).Begin();
       counter--;
   }            
}

В-пятых, приземляемся.

 void sb_Completed(object sender, EventArgs e)
{
   if (counter > 0)
   {
       Run();
   }
   else
   {
       (this.Resources["Down"] as Storyboard).Begin();
       isActive = false;
   }
}

Готово.

Кстати, убедитесь, что у вас в XAML-коде заданы заглушки для трансформаций, иначе они динамически не найдутся:

 <Grid x:Name="LayoutRoot" RenderTransformOrigin="0.5,0.5">
   <Grid.RenderTransform>
      <TransformGroup>
          <ScaleTransform/>
          <SkewTransform/>
          <RotateTransform Angle="0"/>
          <TranslateTransform/>
      </TransformGroup>
   </Grid.RenderTransform>

Проверка

Разместите несколько Butterfly-контролов на главной странице и запускайте приложение. Теперь бабочки летают!

https://constantin.kichinsky.ru/projects/butterfly/

Продолжение следует! Исходники приложения в прикрепленном архиве.

Butterfly.zip