Skip to main content

Quickstart

In this guide, we'll create a simple animation using Motion Canvas.

Prerequisites

Make sure that Node.js version 16 or greater is installed on your machine.

tip

You can run the following command to check if Node.js is already installed:

node -v

Creating a new project

Run the following command in order to scaffold a new Motion Canvas project (If the command fails check out the troubleshooting section below):

npm init @motion-canvas@latest

Answer the prompts to name your project and select which language you would like to use; either TypeScript or plain JavaScript. We recommend using TypeScript in your first project, since that's the language we're using throughout this documentation.

You'll also be asked to choose how you'd like to export your animations. Motion Canvas comes with a built-in Image sequence exporter - perfect if you want to import your animations into a video editor. However, if you want to directly produce a finished video, you can choose to install the Video (FFmpeg) exporter. Don't worry, you can select multiple exporters, and you can always add more later.

To complete the scaffolding process, you'll need to run the following commands:

  1. Change Directory (cd) to the project root (You should replace <project-path> with the path you picked during scaffolding):
    cd <project-path>
  2. Install necessary dependencies:
    npm install
  3. Start the editor:
    npm start
    The editor can be accessed by visiting http://localhost:9000/. We'll use it to preview our animation, although for now there's not much to see.

Programming an animation

The scaffolding command will create several files for you, but for now we're going to focus on src/scenes/example.tsx, which is where we can add our animations. Open example.tsx in a text editor, and replace all code in the file with the following snippet.

Press play to preview the animation
import ...

export default makeScene2D(function* (view) {
const myCircle = createRef<Circle>();

view.add(
<Circle
ref={myCircle}
// try changing these properties:
x={-300}
width={140}
height={140}
fill="#e13238"
/>,
);

yield* all(
myCircle().position.x(300, 1).to(-300, 1),
myCircle().fill('#e6a700', 1).to('#e13238', 1),
);
});

Now save the file. Any changes you make are automatically picked up and reflected in the preview.

You should see a red circle in the preview pane at the top right of the web application. Press the play button to see the circle animate across the screen.

Explanation

Each video in Motion Canvas is represented by a project configuration object. In our example, this configuration is declared in src/project.ts:

src/project.ts
import {makeProject} from '@motion-canvas/core';

import example from './scenes/example?scene';

export default makeProject({
scenes: [example],
});

When creating a project, we need to provide it with an array of scenes to display. In this case, we use only one scene imported from src/scenes/example.tsx.

A scene is a set of elements displayed on the screen and an animation that governs them. The most basic scene looks as follows:

import {makeScene2D} from '@motion-canvas/2d';

export default makeScene2D(function* (view) {
// animation
});

makeScene2D() takes a function generator and turns it into a scene which we then import in our project file. The function generator describes the flow of the animation, while the provided view argument is used to add elements to the scene.

You can learn more about scenes, nodes, and this XML-like syntax in the scene hierarchy section. For now, what's important is that, in our example, we add an individual <Circle/> node to our scene. We make it red, set its width and height as 140 pixels and position it 300 pixels left from the center:

view.add(
<Circle
ref={myCircle}
x={-300}
width={140}
height={140}
fill="#e13238"
/>,
);

To animate our circle we first need to grab a reference to it. That's the purpose of the createRef function. We use it to create a reference and pass it to our circle using the ref attribute:

const myCircle = createRef<Circle>();

view.add(
<Circle
ref={myCircle}
x={-300}
width={140}
height={140}
fill="#e13238"
/>,
);

We then access the circle through myCircle() and animate its properties:

yield *
all(
myCircle().fill('#e6a700', 1).to('#e13238', 1),
myCircle().position.x(300, 1).to(-300, 1),
);

This snippet may seem a bit confusing so let's break it down.

Each property of a node can be read and updated throughout the animation. For example, in the circle above we defined its fill property as '#e13238':

<Circle
ref={myCircle}
x={-300}
width={140}
height={140}
fill="#e13238"
/>

Using our reference we can now retrieve this property's value:

const fill = myCircle().fill(); // '#e13238'

We can also update it by passing the new value as the first argument:

myCircle().fill('#e6a700');

This will immediately update the color of our circle. If we want to transition to a new value over some time, we can pass the transition duration (in seconds) as the second argument:

myCircle().fill('#e6a700', 1);

This creates a tween animation that smoothly changes the fill color over one second.

Animations in Motion Canvas don't play on their own, we need to explicitly tell them to. This is why scenes are declared using generator functions - they serve as a description of how the animation should play out. By yielding different instructions we can tell the scene animation to do different things.

For example, to play the tween we created, we can do:

yield * myCircle().fill('#e6a700', 1);

This will pause the generator, play out the animation we yielded, and then continue.

To play another animation, right after the first one, we can simply write another yield* statement:

yield * myCircle().fill('#e6a700', 1);
yield * myCircle().fill('#e13238', 1);

But since we're animating the same property, we can write it in a more compact way:

yield * myCircle().fill('#e6a700', 1).to('#e13238', 1);

In our example, aside from changing the color, we also move our circle around. We can try doing it the same way we animated the color:

yield * myCircle().fill('#e6a700', 1).to('#e13238', 1);
yield * myCircle().position.x(300, 1).to(-300, 1);

This works, but the position will start animating after the fill color. To make them happen at the same time, we use the all function:

yield *
all(
myCircle().fill('#e6a700', 1).to('#e13238', 1),
myCircle().position.x(300, 1).to(-300, 1),
);

all takes one or more animations and merges them together. Now they'll happen at the same time. The animation flow section goes into more depth about generators and flow functions such as all.

This brings us back to our initial example:

src/scenes/example.tsx
import {Circle, makeScene2D} from '@motion-canvas/2d';
import {all, createRef} from '@motion-canvas/core';

export default makeScene2D(function* (view) {
const myCircle = createRef<Circle>();

view.add(
<Circle
ref={myCircle}
x={-300}
width={140}
height={140}
fill="#e13238"
/>,
);

yield* all(
myCircle().position.x(300, 1).to(-300, 1),
myCircle().fill('#e6a700', 1).to('#e13238', 1),
);
});

Troubleshooting

npm init @motion-canvas@latest fails to execute.

There was a bug in npm causing the above command to fail. It got fixed in version 8.15.1. You can follow this guide to update your npm. Alternatively, you can use the following command instead:

npm exec @motion-canvas/create@latest
npm install fails with code ENOENT

If npm install fails with the following error:

npm ERR! code ENOENT
npm ERR! syscall open
npm ERR! path [path]\package.json
npm ERR! errno -4058
npm ERR! enoent ENOENT: no such file or directory, open '[path]\package.json'
npm ERR! enoent This is related to npm not being able to find a file.
npm ERR! enoent

Make sure that you're executing the command in the correct directory. When you finish bootstrapping the project with npm init, it will display three commands:

cd [path]
npm install
npm start

Did you run the cd command to switch to the directory containing your project?

I moved the camera too far away and can't find the preview (The preview is is black)

You can press 0 to refocus the camera on the preview.

The animation ends abruptly or does not start at the beginning.

Make sure the playback range selector in the timeline starts and ends where you expect it to, e.g., at the beginning and end of your animation. The range selector is a gray bar in the time axis of your timeline. When you move your mouse over it, six dots will appear allowing you to manipulate it.

File watching does not work on Windows Subsystem for Linux (WSL) 2

When running Vite on WSL2, file system watching does not work if a file is edited by Windows applications.

To fix this, move the project folder into the WSL2 file system and use WSL2 applications to edit files. Accessing the Windows file system from WSL2 is slow, so this will improve performance.

For more information view the Vite Docs.