Skip to main content

Authoring plugins

Plugins are a way to extend the functionality of Motion Canvas.

Motion Canvas consists of two separate parts: a Vite development server running on Node.js and an editor running in the browser (runtime). Plugins can extend both of these.

Writing a runtime plugin

Let's start by creating the entry point - a simple file that exports our plugin. For now, we'll put that file in the src directory of our Motion Canvas project. If you find yourself reusing the plugin across multiple projects, you can put it in a separate package.

To define the plugin, we pass a configuration object to the makePlugin helper function and export the result. name is the only required property. player is an optional hook that will receive the Player instance right after it's created. Take a look at the Plugin interface for the full list of available hooks.

Project structure
  project/
├── src/
│ ├── scenes/
│ │ └── example.tsx
+ │ ├── plugin.ts
│ └── project.ts
├── package.json
└── vite.config.ts
src/plugin.ts
import {makePlugin} from '@motion-canvas/core';

export default makePlugin({
name: 'motion-canvas-plugin-example',
player(player) {
player.onRecalculated.subscribe(() => {
player.requestReset();
player.togglePlayback(true);
});
},
});

In this example we subscribe to the onRecalculated event to play the animation from the beginning whenever this event occurs. Recalculation happens when we edit the animation either in the editor or in the code.

Now we can import the plugin in our project file. Note that makePlugin returns a function that we need to call to create an actual plugin object. We'll see why that's useful in a moment:

src/project.ts
import {makeProject} from '@motion-canvas/core';
import myPlugin from './plugin';
import example from './scenes/example?scene';

export default makeProject({
scenes: [example],
plugins: [myPlugin()],
// ^^ we need to call the function
});

That's it! Now we can run the project and see our plugin in action.

Writing a Node.js plugin

Motion Canvas builds on top of the Vite plugin system. To create a plugin that runs on Node.js, you just create a Vite plugin and import it in your vite.config.ts file. On top of that, @motion-canvas/vite-plugin provides a symbol that lets you define motion-canvas-specific options.

Let's start by creating a file for our plugin. This time we'll put it in the root directory of our project because Vite plugins are not part of the runtime source code.

We use the Vite configureServer hook to add a custom endpoint. Meanwhile, the entryPoint option lets us specify the path to our runtime plugin:

Project structure
  project/
├── src/
│ ├── scenes/
│ │ └── example.tsx
│ ├── plugin.ts
│ └── project.ts
├── package.json
+ ├── myVitePlugin.ts
└── vite.config.ts
myVitePlugin.ts
import {Plugin, PLUGIN_OPTIONS} from '@motion-canvas/vite-plugin';

export default function myVitePlugin(): Plugin {
return {
name: 'vite-plugin-motion-canvas-example',

// extend the dev server using Vite plugin hooks:
configureServer(server) {
server.middlewares.use('/my-plugin', (req, res) => {
res.end('Hello from my plugin!');
});
},

// extend Motion Canvas:
[PLUGIN_OPTIONS]: {
entryPoint: './plugin.ts',
},
};
}

Here's how we would import such plugin in our vite.config.ts file:

vite.config.ts
import {defineConfig} from 'vite';
import motionCanvas from '@motion-canvas/vite-plugin';
import myVitePlugin from './myVitePlugin';

export default defineConfig({
plugins: [
motionCanvas(),
myVitePlugin(),
],
});

Also, since we defined the entry point in the Node.js plugin, we no longer need to import the runtime plugin in our project file:

src/project.ts
  import {makeProject} from '@motion-canvas/core';
- import myPlugin from './plugin';
import example from './scenes/example?scene';

export default makeProject({
scenes: [example],
- plugins: [myPlugin()],
});
tip

Notice that we defined the entry point as './plugin.ts'. Because it's a relative path, it will be resolved relative to the project file. Once you turn your plugin into a separate package, you can use a package name instead.

You can verify that the plugin is working by running the project and visiting http://localhost:9000/my-plugin.

Passing options to Runtime

A Node.js plugin has the ability to pass options to the runtime plugin. We can do that using the runtimeConfig property:

myVitePlugin.ts
import {Plugin, PLUGIN_OPTIONS} from '@motion-canvas/vite-plugin';

export default function myVitePlugin(): Plugin {
return {
name: 'vite-plugin-motion-canvas-example',
// ...
[PLUGIN_OPTIONS]: {
entryPoint: './plugin.ts',
runtimeConfig: () => ({
foo: 'bar',
}),
},
};
}

We can then update the runtime plugin to receive these options:

src/plugin.ts
import {makePlugin} from '@motion-canvas/core';

interface MyPluginOptions {
foo: string;
}

export default makePlugin((options?: MyPluginOptions) => {
console.log(options?.foo); // 'bar'

return {
name: 'motion-canvas-plugin-example',
player(player) {
player.onRecalculated.subscribe(() => {
player.requestReset();
player.togglePlayback(true);
});
},
};
});

Plugin examples

You can take a look at our FFmpeg plugin for an example of a fully-fledged plugin. It's runtime portion provides the editor with a Video exporter that sends the rendered frames over to Node.js. The server portion then uses FFmpeg to convert the frames into a video.