Web Animations

Overview

This tutorial introduces two lovable zombies which we will learn to animate using the Dojo WebAnimation meta. We will introduce the API provided by Web Animations and show you how to use it with Dojo.

Zombies

Prerequisites

You can download the initial demo project and run npm install to get started.

The @dojo/cli command line tool should be installed globally. Refer to the Dojo local installation article for more information.

You also need to be familiar with TypeScript as Dojo uses it extensively.

What is the Web Animations API

Effect Timing
AnimationEffectTiming you control over the timing of the animations you create. It provides access to delay, fill, duration and more.

The Web Animations API provides programmatic control over web animations via the timing model and the animation model. This allows animations to be created and controlled via javascript with access to playbackrate, iterations, events and more. Previously this would have required the use of requestAnimationFrame or the less efficient setInterval.

Meta
Dojo meta provides a means to get and set properties against the generated HTML without exposing the domNode itself.

Dojo provides a meta that can apply Web Animations to the rendered Virtual DOM in the widgets you create. The WebAnimation meta allows properties such as play and duration to be reactive to state changes and fits in consistently within the Dojo ecosystem.

The Web Animations API is not currently available even in the latest browsers. To use the Web Animations API, Dojo provides a shim from @dojo/framework/shim/WebAnimations which is used by the WebAnimation meta.

Introducing the zombies

Looking at the Zombies.ts file within the initial project, there are a series of DIVs representing two zombies, each with a body and two legs. Via the magic of CSS these DIVs are converted into two zombies when you open your web browser. To demonstrate the use of Web Animations with Dojo, we will make the zombies walk towards each other when they are clicked.

Animating our zombies

To animate the zombies, we use the @dojo/framework/widget-core/meta/WebAnimation meta. This accepts key and AnimationProperties parameters.Let’s add animation properties to our first Zombie.

Import the WebAnimation meta from @dojo/framework/widget-core

  1. import WebAnimation, {
  2. AnimationProperties
  3. } from '@dojo/framework/widget-core/meta/WebAnimation';

Add the following to the render function in Zombies.ts

  1. // add to the top of the render function
  2. const zombieOneMoveAnimation: AnimationProperties = {
  3. id: 'zombieOneMove',
  4. effects: [
  5. { left: '0%' },
  6. { left: '35%' }
  7. ],
  8. timing: {
  9. duration: 2000,
  10. easing: 'ease-in',
  11. fill: 'forwards'
  12. },
  13. controls: {
  14. play: true
  15. }
  16. };
  17. this.meta(WebAnimation).animate('zombieOne', zombieOneMoveAnimation);

Reminder
If you cannot see the zombies, make sure you have run dojo build -m dev -w -s to build the application and start the local development server.

Refresh your web browser and you should now see the left zombie moving across the screen for 2 seconds.

Let’s explore the animation properties we have created:

  • A unique id for this animation
  • Effects array depicts the steps for the animation; here we are animating the left style from 0% to 35%
  • The timing object specifies the duration and easing effects for the animation; the fill parameter specifies that the animation should finish in its final position
  • The controls object in this case simply sets play to true
    Animating our zombie onclick

To make the zombie animate on click, we use our widget’s state to control the play property of the zombieOneMoveAnimation object. Create a _play: boolean property on our widget and toggle it in the zombie click function.

Edit Zombies.ts

  1. export class Zombies extends WidgetBase {
  2. // add the _play boolean at the top of the class
  3. private _play = false;
  4. private _onZombieClick() {
  5. // add the toggle and an invalidate call to the zombie
  6. // click function
  7. this._play = !this._play;
  8. this.invalidate();
  9. }
  10. // now use this._play instead of a hardcoded `true` in
  11. // the `zombieOneMoveAnimation` object

Refresh the web browser and the zombie animation should now play / pause when clicked.

Animation events

The WebAnimation meta provides callback functions for onFinish and onCancel. To ensure that we can replay our animation we need to set this._play to false when the animation has finished by adding an onFinish callback.

Add onFinish callback to Zombies.ts

  1. // add the `onFinish` function
  2. private _onAnimationFinish() {
  3. this._play = false;
  4. this.invalidate();
  5. }
  6. // add to the top of the render function
  7. const zombieOneMoveAnimation: AnimationProperties = {
  8. id: 'zombieOneMove',
  9. effects: [
  10. { left: '0%' },
  11. { left: '35%' }
  12. ],
  13. timing: {
  14. duration: 8000,
  15. easing: 'ease-in',
  16. fill: 'forwards'
  17. },
  18. controls: {
  19. play: true,
  20. onFinish: this._onAnimationFinish // Add the callback here
  21. }
  22. };

We will use this onFinish callback later in the tutorial to trigger a second animation.

All zombies deserve to be able to move!

So far we have only animated one of our zombies, so we will abstract the animation to a private method that we can use for both of our zombies.

Add a private method that will return our animation properties

  1. private _getZombieAnimation(id: string, direction: string): AnimationProperties {
  2. return {
  3. id,
  4. effects: [
  5. { [direction]: '0%' },
  6. { [direction]: '35%' }
  7. ],
  8. timing: {
  9. duration: 8000,
  10. easing: 'ease-in',
  11. fill: 'forwards'
  12. },
  13. controls: {
  14. play: this._play,
  15. onFinish: this._onAnimationFinish
  16. }
  17. };
  18. }

Replace the current animation in the render function

  1. this.meta(WebAnimation).animate(
  2. 'zombieOne',
  3. this._getZombieAnimation('zombieOneShuffle', 'left')
  4. );
  5. this.meta(WebAnimation).animate(
  6. 'zombieTwo',
  7. this._getZombieAnimation('zombieTwoShuffle', 'right')
  8. );

Let’s add more animations

Not content with your zombie just sliding across the screen like that? Let’s add some Hollywood style movie effects by animating the legs and the body.

Add more animations to Zombies.ts

  1. private _getZombieBodyAnimation(id: string): AnimationProperties {
  2. return {
  3. id,
  4. effects: [
  5. { transform: 'rotate(0deg)' },
  6. { transform: 'rotate(-2deg)' },
  7. { transform: 'rotate(0deg)' },
  8. { transform: 'rotate(3deg)' },
  9. { transform: 'rotate(0deg)' }
  10. ],
  11. timing: {
  12. duration: 1000,
  13. iterations: Infinity
  14. },
  15. controls: {
  16. play: this._play
  17. }
  18. };
  19. };
  20. private _getZombieLegAnimation(id: string, front?: boolean): AnimationProperties {
  21. const effects = [
  22. { transform: 'rotate(0deg)' },
  23. { transform: 'rotate(-5deg)' },
  24. { transform: 'rotate(0deg)' },
  25. { transform: 'rotate(5deg)' },
  26. { transform: 'rotate(0deg)' }
  27. ];
  28. if (front) {
  29. effects.reverse();
  30. }
  31. return {
  32. id,
  33. effects,
  34. timing: {
  35. duration: 1000,
  36. iterations: Infinity
  37. },
  38. controls: {
  39. play: this._play
  40. }
  41. };
  42. }
  43. // add this to the render function with the other `animate` calls
  44. this.meta(WebAnimation).animate(
  45. 'zombieOneBody',
  46. this._getZombieBodyAnimation('zombieOneBody')
  47. );
  48. this.meta(WebAnimation).animate(
  49. 'zombieOneBackLeg',
  50. this._getZombieLegAnimation('zombieOneBackLeg')
  51. );
  52. this.meta(WebAnimation).animate(
  53. 'zombieOneFrontLeg',
  54. this._getZombieLegAnimation('zombieOneFrontLeg', true)
  55. );
  56. this.meta(WebAnimation).animate(
  57. 'zombieTwoBody',
  58. this._getZombieBodyAnimation('zombieTwoBody')
  59. );
  60. this.meta(WebAnimation).animate(
  61. 'zombieTwoBackLeg',
  62. this._getZombieLegAnimation('zombieTwoBackLeg')
  63. );
  64. this.meta(WebAnimation).animate(
  65. 'zombieTwoFrontLeg',
  66. this._getZombieLegAnimation('zombieTwoFrontLeg', true)
  67. );

Refresh your browser and you should now see two stumbling zombies moving towards one another. Feel free to experiment with the parameters for rotation and timings.

Zombie-Walk

Notice that these two new animations have iterations set to Infinity. This ensures that they will play indefinitely, or until the first animation completes and this._play is set to false.

Joining animations

Let’s finish by adding some hearts that appear once the Zombies have reached the middle of the screen. We want them to float up to the top of the screen whilst changing size. We can do this by returning an array of AnimationProperties rather than a single object.

Add hearts to Zombies.ts

Add two private variables that we will need

  1. private _playHearts = false;
  2. private _numHearts = 5;

Add a method to return the heart animation

  1. private _getHeartAnimation(
  2. id: string,
  3. sequence: number,
  4. play: boolean
  5. ): AnimationProperties[] {
  6. const delay = sequence * 500;
  7. const leftOffset = Math.floor(Math.random() * 400) - 200;
  8. return [
  9. {
  10. id: `${id}FloatAway`,
  11. effects: [
  12. {
  13. opacity: 0,
  14. marginTop: '0',
  15. marginLeft: '0px'
  16. },
  17. {
  18. opacity: 0.8,
  19. marginTop: '-300px',
  20. marginLeft: `${1- leftOffset}px`
  21. },
  22. {
  23. opacity: 0,
  24. marginTop: '-600px',
  25. marginLeft: `${leftOffset}px`
  26. }
  27. ],
  28. timing: {
  29. duration: 1500,
  30. delay,
  31. },
  32. controls: {
  33. play: this._playHearts,
  34. onFinish: sequence === this._numHearts -1 ?
  35. this._onHeartsFinish : undefined
  36. }
  37. },
  38. {
  39. id: `${id}Scale`,
  40. effects: [
  41. { transform: 'scale(1)' },
  42. { transform: 'scale(0.8)' },
  43. { transform: 'scale(1)' },
  44. { transform: 'scale(1.2)' },
  45. { transform: 'scale(1)' }
  46. ],
  47. timing: {
  48. duration: 500,
  49. iterations: Infinity,
  50. delay,
  51. easing: 'ease-in-out'
  52. },
  53. controls: {
  54. play: this._playHearts
  55. }
  56. }
  57. ];
  58. }

Update the animation onFinish callbacks to link the completion of the walking animation to start the hearts

  1. private _onAnimationFinish() {
  2. this._play = false;
  3. // add toggle for `_playHearts`
  4. this._playHearts = true;
  5. this.invalidate();
  6. }
  7. private _onHeartsFinish() {
  8. if (this._playHearts = true) {
  9. this._playHearts = false;
  10. this.invalidate();
  11. }
  12. }

Finally, lets

  1. private _getHearts() {
  2. const hearts = [];
  3. let play = false;
  4. let i;
  5. for (i = 0; i < this._numHearts; i++) {
  6. const key = `heart${i}`;
  7. hearts.push(v('div', { classes: css.heart, key }));
  8. this.meta(WebAnimation).animate(key, this._getHeartAnimation(key, i, play));
  9. }
  10. return hearts;
  11. }
  12. // add this as the last child of the `root` VNode in render
  13. v('div', { classes: css.heartsHolder }, this._getHearts())

Now you should see the hearts appearing and floating away when the zombies get to the middle of the screen.

Controlling our animations

Due to the reactive nature of the Dojo WebAnimation meta, we can control our animation by changing the properties passed into the get animation functions. For example we can change the playbackRate programmatically.

Let’s add slider widgets to our page to change the speed of the zombie shuffle and legs.

  1. // add the imports
  2. import Slider from '@dojo/widgets/slider';
  3. import { w } from '@dojo/framework/widget-core/d';
  4. // add the variable and then change event
  5. private _zombieLegsPlaybackRate = 1;
  6. private _onZombieLegsPlaybackRateChange(value: string) {
  7. this._zombieLegsPlaybackRate = parseFloat(value);
  8. this.invalidate();
  9. }
  10. // use the new this._zombieLegsPlaybackRate property in the animation
  11. private _getZombieLegAnimation(id: string, front?: boolean): AnimationProperties {
  12. const effects = [
  13. { transform: 'rotate(0deg)' },
  14. { transform: 'rotate(-5deg)' },
  15. { transform: 'rotate(0deg)' },
  16. { transform: 'rotate(5deg)' },
  17. { transform: 'rotate(0deg)' }
  18. ];
  19. if (front) {
  20. effects.reverse();
  21. }
  22. return {
  23. id,
  24. effects,
  25. timing: {
  26. duration: 1000,
  27. iterations: Infinity
  28. },
  29. controls: {
  30. play: this._play,
  31. playbackRate: this._zombieLegsPlaybackRate // add it here
  32. }
  33. };
  34. }
  35. // finally, add the control to the top of the render function
  36. v('div', { classes: css.controls }, [
  37. w(Slider, {
  38. min: 0.1,
  39. max: 10,
  40. step: 0.1,
  41. value: this._zombieLegsPlaybackRate,
  42. onInput: this._onZombieLegsPlaybackRateChange
  43. })
  44. ])

Open your browser and you should now be able to speed up and slow down the zombie leg animation as they move across the screen.

Summary

In this tutorial, we learned:

  • How to use a Dojo meta
  • How to pass AnimationProperties via the WebAnimation meta
  • How to stop and start an animation
  • How to control an animation
  • That zombies feel love ? ?
    You can download the completed demo application from this tutorial.