Визуалы DirectComposition предоставляют куда больше простых свойств offset и content, о которых я так или иначе говорил в своих последних нескольких статьях. По-настоящему визуалы оживают, когда вы начинаете воздействовать на них с помощью преобразований и анимаций. В обоих случаях Windows-механизм композиции является своего рода процессором, и на вас возлагается ответственность за вычисление или конструирование матриц преобразований, а также анимационных кривых с кубическими функциями и синусоидальными волнами. К счастью, Windows API обеспечивает необходимую поддержку за счет пары взаимодополняющих API-функций. Direct2D имеет превосходную поддержку для определения матриц преобразования, упрощая описание вращения, масштабирования, создания перспективы, трансляции и многого другого. Аналогично Windows Animation Manager освобождает вас от необходимости быть экспертом в области математики, позволяя описывать анимации с помощью богатой библиотеки анимационных переходов (animation transitions), раскадровки (storyboard) с ключевыми кадрами и т. д. Комбинируя Windows Animation Manager, Direct2D и DirectComposition в одном приложении, вы по-настоящему прочувствуете мощь этих компонентов.
В прошлой статье (msdn.microsoft.com/magazine/dn759437) я показал, как DirectComposition API можно использовать наряду с Direct2D, чтобы получить лучшее из двух миров — графики режима сохранения (retained mode) и прямого режима (immediate mode). Проект-пример, включенный в ту статью, иллюстрировал эту концепцию на простом приложении, которое позволяет создавать окружности, двигать их по окну и довольно легко управлять их Z-порядком. В этой статье я хочу показать, насколько легко добавить некоторые впечатляющие эффекты с помощью преобразований и анимации. Прежде всего вы должны осознать, что DirectComposition предоставляет перегруженные версии многих скалярных свойств. Вероятно, вы заметили это, если следовали за мной в течение последних нескольких месяцев, когда я исследовал этот API. Например, смещение визуала можно задать с помощью методов SetOffsetX и SetOffsetY:
ComPtr<IDCompositionVisual2> visual = ...
HR(visual->SetOffsetX(10.0f));
HR(visual->SetOffsetY(20.0f));
Но интерфейс IDCompositionVisual, от которого наследует IDCompositionVisual2, также предоставляет перегрузки этих методов, которые принимают объект анимации, а не значение с плавающей точкой. Этот объект анимации материализуется как интерфейс IDCompositionAnimation. Например, я мог бы установить смещение визуала с помощью одного или двух объектов анимации в зависимости от того, нужна мне анимация по одной оси или по обеим:
ComPtr<IDCompositionAnimation> animateX = ...
ComPtr<IDCompositionAnimation> animateY = ...
HR(visual->SetOffsetX(animateX.Get()));
HR(visual->SetOffsetY(animateY.Get()));
Но механизм композиции обладает куда большими возможностями в анимации. Визуалы также поддерживают двух- и трехмерные преобразования. Метод SetTransform визуала можно использовать для применения двухмерного преобразования при заданном скалярном значении:
D2D1_MATRIX_3X2_F matrix = ...
HR(visual->SetTransform(matrix));
Здесь DirectComposition на самом деле опирается на матрицу 3×2, определенную в Direct2D API. С ее помощью вы можете выполнять разнообразные операции вроде вращения, трансляции, масштабирования и наклона визуала. Это влияет на координатное пространство, на которое проецируется контент визуала, но ограничено двухмерной графикой с осями X и Y.
Естественно, интерфейс IDCompositionVisual предоставляет перегруженную версию метода SetTransform, но она не принимает объект анимации напрямую. Видите ли, объект анимации отвечает за анимацию только одного значения в течение некоего времени. Матрица, по определению, состоит из набора значений. Возможно, вы захотите анимировать какое-то количество ее членов в зависимости от нужного вам эффекта. Поэтому перегруженная версия SetTransform принимает объект transform:
ComPtr<IDCompositionTransform> transform = ...
HR(visual->SetTransform(transform.Get()));
Именно объект transform, а точнее, различные интерфейсы, производные от IDCompositionTransform, предоставляет перегруженные методы, которые принимают либо скалярные значения, либо объекты анимации. Тем самым вы могли бы определить матрицу вращения с анимированным углом поворота, но с фиксированными центральной точкой и осями. Что конкретно вы анимируете, разумеется, дело ваше. Вот простой пример:
ComPtr<IDCompositionRotateTransform> transform = ...
HR(transform->SetCenterX(width / 2.0f));
HR(transform->SetCenterY(height / 2.0f));
HR(transform->SetAngle(animation.Get()));
HR(visual->SetTransform(transform.Get()));
Интерфейс IDCompositionRotateTransform наследует от IDCompositionTransform и представляет двухмерное преобразование, которое влияет на поворот визуала вокруг оси Z. Здесь я присваиваю центральной точке фиксированное значение, исходя из неких значений ширины и высоты, и использую объект анимации для управления углом.
Таков базовый шаблон. Я описал лишь двухмерные преобразования, но трехмерные преобразования во многом работают так же. Теперь позвольте мне проиллюстрировать более практический пример, показав, как применить в проекте из моей прошлой статьи преобразование и анимацию разнообразными способами с помощью Direct2D и Windows Animation Manager.
Иллюстрация преобразований и анимации в печатном издании невозможна без выхода за границы того, что можно легко ухватить, глядя на статическую страницу. Если вы хотите увидеть их в действии, просмотрите мой онлайновый учебный курс, где можно увидеть, как все это происходит (bit.ly/WhKQZT). Чтобы сделать концепции немного понятнее в печатном виде, я решил заменить окружности в своем проекте из прошлой статьи на квадраты. Это должно несколько прояснить различные преобразования на бумаге. Сначала я заменю переменную-член m_geometry в SampleWindow геометрией прямоугольника:
ComPtr<ID2D1RectangleGeometry> m_geometry;
Затем в методе SampleWindow CreateFactoryAndGeometry я буду получать Direct2D-фабрику, чтобы создавать геометрию прямоугольника вместо геометрии эллипса:
D2D1_RECT_F const rectangle =
RectF(0.0f, 0.0f, 100.0f, 100.0f);
HR(m_factory->CreateRectangleGeometry(
rectangle,
m_geometry.GetAddressOf()));
Вот и все. Остальное приложение просто использует абстракцию геометрии для рендеринга и проверки на попадания, как и раньше. Результат показан на рис. 1.
Рис. 1. Иллюстрация преобразований и анимации с помощью квадратов вместо окружностей
Далее я намерен добавить простой обработчик для реагирования на сообщение WM_KEYDOWN. В метод MessageHandler из SampleWindow я включаю для этого выражение if:
else if (WM_KEYDOWN == message)
{
KeyDownHandler(wparam);
}
Как обычно, в нем понадобится обработка, необходимая для восстановления после потери устройства. На рис. 2 представлен стандартный шаблон освобождения ресурсов устройства и объявления окна недействительным, чтобы обработчик сообщений WM_PAINT мог перестроить стек устройства. Кроме того, я ограничиваю обработчик клавишей Enter, чтобы избежать путаницы с клавишей Ctrl, используемой при добавлении фигур.
Рис. 2. Обвязка для восстановления после потери устройства
void KeyDownHandler(WPARAM const wparam)
{
try
{
if (wparam != VK_RETURN)
{
return;
}
// Выполняем работу!
}
catch (ComException const & e)
{
TRACE(L"KeyDownHandler failed 0x%X\n",
e.result);
ReleaseDeviceResources();
VERIFY(InvalidateRect(m_window,
nullptr,
false));
}
}
К этому моменту я готов к экспериментам с преобразованиями и анимациями. Давайте начнем с базового преобразования двухмерного поворота. Прежде всего я должен определить центральную точку, которая будет представлять Z-ось, или точку, вокруг которой будет осуществляться вращение. Поскольку DirectComposition ожидает передачи координат физических пикселей, я могу здесь просто вызвать функцию GetClientRect:
RECT bounds {};
VERIFY(GetClientRect(m_window, &bounds));
Затем выводим центральную точку клиентской области окна:
D2D1_POINT_2F center
{
bounds.right / 2.0f,
bounds.bottom / 2.0f
};
Кроме того, можно опираться на вспомогательные матричные функции Direct2D в конструировании матрицы, описывающей преобразование двухмерного поворота на 30 градусов:
D2D1_MATRIX_3X2_F const matrix =
Matrix3x2F::Rotation(30.0f, center);
А затем просто задаем свойство transform визуала и передаем изменение в дерево визуальных объектов. Для упрощения я применю это изменение к корневому визуалу:
HR(m_rootVisual->SetTransform(matrix));
HR(m_device->Commit());
Конечно, вы можете применять любое количество изменений к любому числу визуалов, и механизм композиции сам позаботится о координации этих изменений. Результаты этого простого двухмерного преобразования показаны на рис. 3. Вероятно, вы заметили некоторую ступенчатость (aliasing). Хотя Direct2D по умолчанию осуществляет сглаживание (anti-aliasing), он предполагает, что картинка появится в координатном пространстве, в котором выполнялся ее рендеринг. Механизм композиции ничего не знает о геометрии, с использованием которой был выполнен рендеринг поверхности композиции, поэтому у него нет возможности поправить это. В любом случае, как только в смесь добавляется анимация, ступенчатость будет видна на крайне малое время и, следовательно, почти незаметна.
Рис. 3. Простое двухмерное преобразование
Чтобы добавить анимацию к этому преобразованию, нужно исключить структуру матрицы для преобразования композиции. Я заменю структуры D2D1_POINT_2F и D2D1_MATRIX_3X2_F одним преобразованием поворота. Сначала нужно создать это преобразование, используя устройство композиции:
ComPtr<IDCompositionRotateTransform> transform;
HR(m_device->CreateRotateTransform(transform.GetAddressOf()));
(Учтите, что даже такие кажущиеся простыми объекты должны быть удалены, когда и если устройство теряется и создается заново.) Затем я задаю центральную точку и угол, используя методы интерфейса вместо матричной структуры Direct2D:
HR(transform->SetCenterX(bounds.right / 2.0f));
HR(transform->SetCenterY(bounds.bottom / 2.0f));
HR(transform->SetAngle(30.0f));
И компилятор выбирает подходящую перегруженную версию для обработки преобразования композиции:
HR(m_rootVisual->SetTransform(transform.Get()));
Вы полнение этого кода даст тот же эффект, что и на рис. 3, так как я еще не добавил анимацию. Создать объект animation достаточно просто:
ComPtr<IDCompositionAnimation> animation;
HR(m_device->CreateAnimation(animation.GetAddressOf()));
Затем я могу использовать этот объект animation вместо константного значения при задании угла:
HR(transform->SetAngle(animation.Get()));
Все становится чуточку интереснее, когда вы пытаетесь настроить анимацию. Простые анимации сравнительно прямолинейны. Как я уже говорил, анимации описываются кубическими функциями и синусоидальными волнами. Этот угол поворота можно анимировать с помощью линейного перехода, где значение увеличивается от 0 до 360 каждую секунду, добавив кубическую функцию:
float duration = 1.0f;
HR(animation->AddCubic(0.0,
0.0f,
360.0f / длительность,
0.0f,
0.0f));
Третий параметр метода AddCubic указывает коэффициент линейного увеличения, так что это имеет смысл. Если бы я оставил все в таком виде, визуал вечно вращался бы на 360 градусов. Анимацию можно остановить, как только угол станет равным 360 градусам:
HR(animation->End(duration, 360.0f));
Первый параметр метода End указывает смещение от начала анимации независимо от значения функции анимации. Второй параметр — это конечное значение анимации. Учитывайте это, так как анимация будет «перескакивать» к этому значению при неправильно выбранном шаге, что приведет к визуальному эффекту рывка.
В таких линейных анимациях достаточно легко разобраться, но более сложные анимации могут оказаться чрезмерно запутанными. Здесь и вступает в игру Windows Animation Manager. Вместо того, чтобы вызывать различные методы IDCompositionAnimation, чтобы добавить синусоидальные сегменты, кубический полином, повторяющиеся сегменты и т. д., можно сконструировать раскадровку анимации с помощью Windows Animation Manager и его богатой библиотеки переходов (transitions). После этого полученная переменная анимации используется для заполнения анимации композиции. Это требует написания несколько большего количества кода, но преимущество — гораздо большие возможности и контроль над анимацией в приложении. Для начала я должен создать сам диспетчер анимации:
ComPtr<IUIAnimationManager2> manager;
HR(CoCreateInstance(__uuidof(UIAnimationManager2),
nullptr, CLSCTX_INPROC, __uuidof(manager),
reinterpret_cast<void **>(manager.GetAddressOf())));
Да, Windows Animation Manager полагается на COM-активацию, поэтому не забудьте вызвать CoInitializeEx или RoInitialize для инициализации исполняющей среды. Кроме того, нужно создать библиотеку переходов:
ComPtr<IUIAnimationTransitionLibrary2> library;
HR(CoCreateInstance(__uuidof(UIAnimationTransitionLibrary2),
nullptr, CLSCTX_INPROC, __uuidof(library),
reinterpret_cast<void **>(library.GetAddressOf())));
Как правило, приложения будут сохранять эти два объекта в течение всего жизненного цикла, поскольку они нужны для непрерывной анимации и особенно для синхронизации скорости (velocity matching). Затем я должен создать раскадровку анимации (animation storyboard):
ComPtr<IUIAnimationStoryboard2> storyboard;
HR(manager->CreateStoryboard(storyboard.GetAddressOf()));
Раскадровка — то, что связывает переходы с переменными анимации и определяет их относительные расписания в течение некоего времени. Раскадровка способна агрегировать различные переходы, применяемые к разным переменным анимации; она гарантирует их синхронизацию и планируется как единое целое. (Конечно, вы можете создавать несколько раскадровок для планирования независимых анимаций.) Теперь мне нужно попросить диспетчер анимации создать переменную анимации:
ComPtr<IUIAnimationVariable2> variable;
HR(manager->CreateAnimationVariable(
0.0, // начальное значение
variable.GetAddressOf()));
Как только раскадровка запланирована, диспетчер анимации отвечает за поддержание актуальности переменной, чтобы приложение могло запрашивать действующее в данный момент значение. В этом случае я просто использую переменную анимации для заполнения анимации композиции сегментами, а затем удаляю их. Теперь можно задействовать могущественную библиотеку переходов для создания интересного эффекта перехода для переменной анимации:
ComPtr<IUIAnimationTransition2> transition;
HR(library->CreateAccelerateDecelerateTransition(
1.0, // длительность
360.0, // конечное значение
0.7, // коэффициент ускорения
0.3, // коэффициент замедления
transition.GetAddressOf()));
Переход вызовет ускорение переменной анимации, а потом замедление в течение заданного интервала, пока анимация не закончится со своим конечным значением. Применяемые коэффициенты влияют на то, насколько относительно быстро переменная будет ускоряться, а затем замедляться. Учтите, что комбинация коэффициентов не может превышать значения 1. Подготовив переменную анимации переход, я могу добавить их в раскадровку:
HR(storyboard->AddTransition(variable.Get(),
transition.Get()));
Теперь раскадровка готова к планированию:
HR(storyboard->Schedule(0.0));
Единственный параметр метода Schedule сообщает планировщику текущее время анимации. Это больше полезно для координации анимации и ее синхронизации с частотой обновления, используемой механизмом композиции, но пригодится и сейчас. К этому моменту переменная анимации «заряжена», и я могу попросить ее заполнить кривыми анимацию композиции:
HR(variable->GetCurve(animation.Get()));
В данном случае это равноценно тому, как если бы я вызвал такую анимацию композиции:
HR(animation->AddCubic(0.0, 0.0f, 0.0f, 514.2f, 0.0f));
HR(animation->AddCubic(0.7, 252.0f, 720.0f, -1200.0f, 0.0f));
HR(animation->End(1.0, 360.0f));
Здесь определенно требуется намного меньше кода, чем при работе с Windows Animation Manager, но разобраться во всей этой математике совсем не просто. Windows Animation Manager также позволяет координировать и плавно выполнять переходы между анимациями, что крайне трудно сделать вручную.