feat: animation player (#92)

- New package has been added.
It contains a custom html element for playing animations.
- Docs have been updated.
This commit is contained in:
Jacob
2022-12-05 06:16:50 +01:00
committed by GitHub
parent d85f2f8a54
commit 8155118eb1
33 changed files with 813 additions and 317 deletions

View File

@@ -5,7 +5,17 @@ module.exports = {
'scope-enum': [
2,
'always',
['2d', 'core', 'create', 'docs', 'legacy', 'ui', 'vite-plugin'],
[
'2d',
'core',
'create',
'docs',
'examples',
'legacy',
'player',
'ui',
'vite-plugin',
],
],
},
};

79
package-lock.json generated
View File

@@ -4574,6 +4574,10 @@
"resolved": "packages/examples",
"link": true
},
"node_modules/@motion-canvas/player": {
"resolved": "packages/player",
"link": true
},
"node_modules/@motion-canvas/template": {
"resolved": "packages/template",
"link": true
@@ -13202,11 +13206,6 @@
"node": ">=8"
}
},
"node_modules/load-script": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA=="
},
"node_modules/loader-runner": {
"version": "4.3.0",
"license": "MIT",
@@ -13533,11 +13532,6 @@
"node": ">= 4.0.0"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"node_modules/meow": {
"version": "8.1.2",
"dev": true,
@@ -16379,21 +16373,6 @@
"webpack": ">=4.41.1 || 5.x"
}
},
"node_modules/react-player": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.11.0.tgz",
"integrity": "sha512-fIrwpuXOBXdEg1FiyV9isKevZOaaIsAAtZy5fcjkQK9Nhmk1I2NXzY/hkPos8V0zb/ZX416LFy8gv7l/1k3a5w==",
"dependencies": {
"deepmerge": "^4.0.0",
"load-script": "^1.0.0",
"memoize-one": "^5.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.0.1"
},
"peerDependencies": {
"react": ">=16.6.0"
}
},
"node_modules/react-router": {
"version": "5.3.3",
"license": "MIT",
@@ -18345,8 +18324,9 @@
}
},
"node_modules/terser": {
"version": "5.14.2",
"license": "BSD-2-Clause",
"version": "5.16.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.16.1.tgz",
"integrity": "sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==",
"dependencies": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
@@ -20300,11 +20280,12 @@
"@docusaurus/core": "^2.0.1",
"@docusaurus/preset-classic": "^2.0.1",
"@mdx-js/react": "^1.6.22",
"@motion-canvas/examples": "*",
"@motion-canvas/player": "*",
"clsx": "^1.2.0",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-player": "^2.10.1"
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^2.0.0",
@@ -20334,10 +20315,10 @@
"packages/player": {
"name": "@motion-canvas/player",
"version": "0.0.0",
"extraneous": true,
"license": "MIT",
"devDependencies": {
"@motion-canvas/core": "^12.0.0-alpha.2",
"terser": "^5.16.1",
"typescript": "^4.6.4",
"vite": "^3.0.4"
},
@@ -23493,13 +23474,14 @@
"@docusaurus/module-type-aliases": "^2.0.0",
"@docusaurus/preset-classic": "^2.0.1",
"@mdx-js/react": "^1.6.22",
"@motion-canvas/examples": "*",
"@motion-canvas/player": "*",
"@tsconfig/docusaurus": "^1.0.5",
"clsx": "^1.2.0",
"docusaurus-plugin-typedoc": "^0.17.5",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-player": "^2.10.1",
"typedoc": "^0.23.7",
"typedoc-plugin-markdown": "^3.13.3",
"typescript": "^4.7.4"
@@ -23515,6 +23497,15 @@
"vite": "^3.0.5"
}
},
"@motion-canvas/player": {
"version": "file:packages/player",
"requires": {
"@motion-canvas/core": "^12.0.0-alpha.2",
"terser": "*",
"typescript": "^4.6.4",
"vite": "^3.0.4"
}
},
"@motion-canvas/template": {
"version": "file:packages/template",
"requires": {
@@ -28987,11 +28978,6 @@
}
}
},
"load-script": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA=="
},
"loader-runner": {
"version": "4.3.0"
},
@@ -29194,11 +29180,6 @@
"fs-monkey": "^1.0.3"
}
},
"memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"meow": {
"version": "8.1.2",
"dev": true,
@@ -30899,18 +30880,6 @@
"@babel/runtime": "^7.10.3"
}
},
"react-player": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.11.0.tgz",
"integrity": "sha512-fIrwpuXOBXdEg1FiyV9isKevZOaaIsAAtZy5fcjkQK9Nhmk1I2NXzY/hkPos8V0zb/ZX416LFy8gv7l/1k3a5w==",
"requires": {
"deepmerge": "^4.0.0",
"load-script": "^1.0.0",
"memoize-one": "^5.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.0.1"
}
},
"react-router": {
"version": "5.3.3",
"requires": {
@@ -32180,7 +32149,9 @@
}
},
"terser": {
"version": "5.14.2",
"version": "5.16.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.16.1.tgz",
"integrity": "sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==",
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",

View File

@@ -15,6 +15,10 @@
"ui:dev": "npm run dev -w packages/ui",
"template:serve": "npm run serve -w packages/template",
"template:build": "npm run build -w packages/template",
"examples:serve": "npm run serve -w packages/examples",
"examples:build": "npm run build -w packages/examples",
"player:serve": "npm run serve -w packages/player",
"player:build": "npm run build -w packages/player",
"docs:start": "npm run start -w packages/docs",
"docs:build": "npm run build -w packages/docs",
"vite-plugin:build": "npm run build -w packages/vite-plugin",

View File

@@ -8,7 +8,8 @@
.docusaurus
.cache-loader
docs/core-api
docs/legacy-api
docs/2d-api
static/examples
# Misc
.DS_Store

View File

@@ -2,18 +2,26 @@
sidebar_position: 1
---
import ReactPlayer from 'react-player'
import '@motion-canvas/player';
# Creating an Animation
# Quickstart
In this guide, we will start a new Motion Canvas project and create an animation
in it.
In this guide, we'll create as simple animation using Motion Canvas.
### Prerequisites
Make sure that [Node.js](https://nodejs.org/) version 16 or greater is
installed on your machine.
:::tip
You can run the following command to check if Node.js is already installed:
```bash
node -v
```
:::
If you're using Motion Canvas as part of the Patreon early access, make sure to
[authenticate to GitHub Packages][autheticate] first.
@@ -25,17 +33,17 @@ Run the following command in order to scaffold a new Motion Canvas project:
npm init @motion-canvas
```
Answer the prompts to name your project and select which language you would like
to use, either TypeScript or plain JavaScript.
Answer the prompts to name your project and select which language you would
like to use, either TypeScript or plain JavaScript.
:::caution
:::tip
We recommend using TypeScript in your first project, as our Guides use
TypeScript.
We recommend using TypeScript in your first project, since that's the language
we're using throughout this documentation.
:::
### Starting the Motion Canvas App
### Starting the editor
From your new Motion Canvas project directory, run
@@ -43,10 +51,9 @@ From your new Motion Canvas project directory, run
npm run serve
```
This will start the Motion Canvas editor, which you may open by visiting
[http://localhost:9000/](http://localhost:9000/). We will use the editor to
preview our animation, but for now there is nothing to see. To do that, we
must add an element to our scene.
This will start the Motion Canvas editor, which you can open by visiting
[http://localhost:9000/](http://localhost:9000/). We'll use the editor to
preview our animation, but for now there's not much to see.
### Programming an animation
@@ -56,24 +63,28 @@ animations. Open `example.tsx` in a text editor, and replace all code in
the file with the following snippet.
```tsx title="src/scenes/example.tsx"
import {makeKonvaScene} from '@motion-canvas/legacy/lib/scenes';
import {Circle} from 'konva/lib/shapes/Circle';
import {makeScene2D} from '@motion-canvas/2d/lib/scenes';
import {Circle} from '@motion-canvas/2d/lib/components';
import {useRef} from '@motion-canvas/core/lib/utils';
export default makeKonvaScene(function* (view) {
const myCircle = useRef();
export default makeScene2D(function* (view) {
const myCircle = useRef<Circle>();
view.add(
<Circle
position={{x: -300, y: 0}}
// highlight-start
ref={myCircle}
x={-300}
width={240}
height={240}
fill="#ccc"
/>
fill="#e13238"
/>,
);
yield* myCircle.value.position({x: 300, y: 0}, 1);
yield* all(
myCircle.value.fill('#e6a700', 1).to('#e13238', 1),
myCircle.value.position.x(300, 1).to(-300, 1),
);
});
```
@@ -82,156 +93,231 @@ reflected in the preview. You should see a gray 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.
<ReactPlayer loop controls url='/video/animation.mp4' />
<motion-canvas-player
style={{maxWidth: 480}}
quality={0.5}
src="/examples/quickstart.js"
/>
Depending on the size of your browser, you may want to zoom the preview out by
scrolling down on your mouse wheel while hovering over the preview panel.
### Explanation
Motion Canvas is designed to work with different HTML canvas libraries, though
it currently only ships with [Konva](https://konvajs.org/) integration. To
create a new konva scene, we need to call `makeKonvaScene` with a generator
function.
Each video in Motion Canvas is represented by an instance of the `Project`
class. In our example, the project is declared in `src/project.ts`:
```tsx
// highlight-next-line
import {makeKonvaScene} from '@motion-canvas/legacy/lib/scenes';
```ts title="src/project.ts"
import {Project} from '@motion-canvas/core/lib';
// highlight-next-line
export default makeKonvaScene(function* (view) {
// animation code
import example from './scenes/example?scene';
export default new Project({
scenes: [example],
});
```
The generator function is passed a `view` argument, which we will use to add
components to the scene.
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`.
:::tip
If you aren't familiar with generator functions from JavaScript, declared with
`function*`, you can read the MDN documentation on them [here][generators],
though a thorough understanding of them is not necessary to start using Motion
Canvas.
:::
In order to create an animation, we needed something to animate. For that, we
used the Konva Circle component.
A scene is a set of elements displayed on the screen and an animation that
governs them. The most basic scene looks as follows:
```tsx
import {makeKonvaScene} from '@motion-canvas/legacy/lib/scenes';
// highlight-next-line
import {Circle} from 'konva/lib/shapes/Circle';
import {makeScene2D} from '@motion-canvas/2d/lib/scenes';
export default makeKonvaScene(function* (view) {
// highlight-start
view.add(
<Circle
width={240}
height={240}
fill="#ccc"
/>
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.
In our example we used a `<Circle/>` node to display a circle on the screen:
```tsx
view.add(
<Circle
// highlight-start
ref={myCircle}
x={-300}
width={240}
height={240}
fill="#e13238"
/>,
);
```
You may recognize this XML-like syntax from libraries such as React.
That's because Motion Canvas uses the same JavaScript syntax extension called
[JSX](https://reactjs.org/docs/introducing-jsx.html).
However, it's important to remember that Motion Canvas does **not** use React
itself and any preconceptions you may have due to React will most likely not
apply.
A `Circle` is just a class and the above JSX code can be written as:
```ts
view.add(
new Circle({
x: -300,
width: 240,
height: 240,
fill: '#ccc',
}),
);
```
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:
```tsx
// highlight-next-line
const myCircle = useRef<Circle>();
view.add(
<Circle
// highlight-next-line
ref={myCircle}
x={-300}
width={240}
height={240}
fill="#e13238"
/>,
);
```
We then access the circle through `myCircle.value` and animate its properties:
```tsx
yield *
all(
myCircle.value.fill('#e6a700', 1).to('#e13238', 1),
myCircle.value.position.x(300, 1).to(-300, 1),
);
// highlight-end
});
```
Here we create a new circle instance with `<Circle />` and add it to the scene
using `view.add()`. This code alone will show a gray circle in the middle of the
screen, though it won't move.
This snippet may seem a bit confusing so let's break it down.
To animate the circle, we needed to store a reference to it. This is the purpose
of the `useRef` call.
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'`:
```tsx
import {makeKonvaScene} from '@motion-canvas/legacy/lib/scenes';
import {Circle} from 'konva/lib/shapes/Circle';
// highlight-next-line
import {useRef} from '@motion-canvas/core/lib/utils';
export default makeKonvaScene(function* (view) {
<Circle
ref={myCircle}
x={-300}
width={240}
height={240}
// highlight-next-line
const myCircle = useRef();
fill="#e13238"
/>
```
view.add(
<Circle
// highlight-next-line
ref={myCircle}
width={240}
height={240}
fill="#ccc"
/>
Using our reference we can now retrieve this property's value:
```ts
const fill = myCircle.value.fill(); // '#e13238'
```
We can also update it by passing the new value as the first argument:
```ts
myCircle.value.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:
```ts
myCircle.value.fill('#e6a700', 1);
```
This creates a tween animation that smoothly changes the fill color over one
second.
But 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:
```ts
yield * myCircle.value.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:
```ts
yield * myCircle.value.fill('#e6a700', 1);
yield * myCircle.value.fill('#e13238', 1);
```
But since we're animating the same property,
we can write it in a more compact way:
```ts
yield * myCircle.value.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:
```ts
yield * myCircle.value.fill('#e6a700', 1).to('#e13238', 1);
yield * myCircle.value.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:
```ts
yield *
all(
myCircle.value.fill('#e6a700', 1).to('#e13238', 1),
myCircle.value.position.x(300, 1).to(-300, 1),
);
// myCircle.value available here
});
```
Once the `Circle` is created with `ref={myCircle}`, it may be accessed from
`myCircle.value`, which we can use to edit the circle's properties. In Konva,
properties are read using `property()`, and written using `property(value)`. To
update the circle's position, for instance, we use
`all()` takes one or more animations and merges them together.
Now they'll happen at the same time.
```tsx
myCircle.value.position({x: 300, y: 0});
```
This brings us back to our initial example:
When we edit the properties of a component, however, the changes are immediate.
In order to _transition_ to a new value over time, we must change our call in
two ways. We must specify the duration of the transition in seconds to create an
animation, `position(value, duration)`, and we must `yield*` to the animation.
```tsx
yield* myCircle.value.position({x: 300, y: 0}, 1);
```
The `yield*` is important. Calling `position({...}, 1)` alone will not run the
animation; it simply returns a value called a task, which represents the
animation.
```tsx
const task = myCircle.value.position({x: 300, y: 0}, 1);
```
Yielding a task prompts Motion Canvas to run it. Using `yield*` waits for the
animation to complete before continuing the scene function, while using `yield`
plays the animation but continues the scene function immediately.
```tsx
yield* myCircle.value.position({x: 300, y: 0}, 1);
// this line will run after the animation has ended
```
```tsx
yield myCircle.value.position({x: 300, y: 0}, 1);
// this line will run immediately while the animation plays
```
In our example, we used `yield*` so that the scene wouldn't end until the
animation was completed.
```tsx
import {makeKonvaScene} from '@motion-canvas/legacy/lib/scenes';
import {Circle} from 'konva/lib/shapes/Circle';
```tsx title="src/scenes/example.tsx"
import {makeScene2D} from '@motion-canvas/2d/lib/scenes';
import {Circle} from '@motion-canvas/2d/lib/components';
import {useRef} from '@motion-canvas/core/lib/utils';
// make a new konva scene and pass it the scene function
export default makeKonvaScene(function* (view) {
// create a reference to store the circle
const myCircle = useRef();
export default makeScene2D(function* (view) {
const myCircle = useRef<Circle>();
// create a circle and add it to the scene's view
view.add(
<Circle
position={{x: -300, y: 0}} // set an initial position
ref={myCircle} // assign the circle instance to the myCircle ref
// highlight-start
ref={myCircle}
x={-300}
width={240}
height={240}
fill="#ccc"
/>
fill="#e13238"
/>,
);
// animate to a new position over 1 second and
// wait for it to finish before continuing
yield* myCircle.value.position({x: 300, y: 0}, 1);
yield* all(
myCircle.value.fill('#e6a700', 1).to('#e13238', 1),
myCircle.value.position.x(300, 1).to(-300, 1),
);
});
```

View File

@@ -2,7 +2,7 @@
sidebar_position: 2
---
# Rendering an Animation
# Rendering
To render an animation, open the Rendering panel in the top left of the screen
using the button with the icon below.

View File

@@ -4,113 +4,12 @@ title: v12.0.0
# Migrating to version 12.0.0
:::tip
Migrating from version 11 to 12 would be really cumbersome due to the amount of changes between the two versions.
If you're starting a new project, you can quickly scaffold it using:
It's recommended to first finish any animations started with version 11 and only use version 12 for new projects.
You can quickly scaffold a project using the following command:
```bash
npm init @motion-canvas
```
:::
## Install the new version
Upgrade the versions of all motion-canvas packages in your `package.json` file:
```diff
- "@motion-canvas/core": "^11.0.0",
- "@motion-canvas/ui": "^11.0.0",
- "@motion-canvas/vite-plugin": "^11.0.0",
+ "@motion-canvas/core": "^12.0.0",
+ "@motion-canvas/ui": "^12.0.0",
+ "@motion-canvas/vite-plugin": "^12.0.0",
```
To apply the changes, run:
```bash
npm install
```
## Install the legacy package
Since version 12, the Konva-based renderer has been moved to a separate package called
`@motion-canvas/legacy`. To continue using it, it needs to be installed separately:
```bash
npm install @motion-canvas/legacy
```
## Configure Vite
Add the following plugin in your Vite configuration:
```diff title="vite.config.ts"
import {defineConfig} from 'vite';
import motionCanvas from '@motion-canvas/vite-plugin';
+ import legacyRenderer from '@motion-canvas/legacy/vite';
export default defineConfig({
- plugins: [motionCanvas()],
+ plugins: [motionCanvas(), legacyRenderer()],
});
```
## Update imports
Change the necessary imports to use the legacy package:
```diff title="src/scenes/example.tsx"
- import {makeKonvaScene} from '@motion-canvas/core/lib/scenes';
+ import {makeKonvaScene} from '@motion-canvas/legacy/lib/scenes';
```
**Not all imports should be changed.** Only the following modules have been moved
to the legacy package and must be updated:
```ts
// all exports from the following modules are now in the legacy package:
import * from '@motion-canvas/core/lib/animations';
import * from '@motion-canvas/core/lib/components';
import * from '@motion-canvas/core/lib/styles';
import * from '@motion-canvas/core/lib/themes';
// only the listed exports from the following modules are now in the legacy package:
import {slide} from '@motion-canvas/core/lib/utils';
import {cached, getset, KonvaNode} from '@motion-canvas/core/lib/decorators';
import {CanvasHelper} from '@motion-canvas/core/lib/helpers';
import {makeKonvaScene, KonvaScene} from '@motion-canvas/core/lib/scenes';
```
## Configure TypeScript
Change your `tsconfig.json` to extend the legacy package instead of core:
```diff title="tsconfig.json"
{
- "extends": "@motion-canvas/core/tsconfig.project.json",
+ "extends": "@motion-canvas/legacy/tsconfig.project.json",
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
```
Similarly, change your types reference in `motion-canvas.d.ts`:
```diff title="src/motion-canvas.d.ts"
- /// <reference types="@motion-canvas/core/project" />
+ /// <reference types="@motion-canvas/legacy/project" />
```
## Update function names
`TweenFunction`s are now called `InterpolationFunction`s.
The naming convention has changed. Instead of `[type]Tween` they now use
`[type]Lerp`. For instance: `colorTween` is now `colorLerp`.
`InterpolationFunction`s are now called `TimingFunction`s.
Their individual names haven't changed (`easeInExpo`, `easeOutCubic`, etc.),
but referring to them as timing functions is better aligned with the CSS spec.

View File

@@ -114,7 +114,7 @@ const config = {
{
routeBasePath: '/',
sidebarPath: 'sidebars.js',
exclude: ['**/core-api/*.md', '**/legacy-api/*.md'],
exclude: ['**/core-api/*.md', '**/2d-api/*.md'],
editUrl: ({versionDocsDirPath, docPath}) =>
`https://github.com/motion-canvas/motion-canvas/blob/main/${versionDocsDirPath}/${docPath}`,
},
@@ -137,7 +137,7 @@ const config = {
out: 'core-api',
excludeExternals: true,
entryPoints: [
'../core/src',
'../core/',
'../core/src/decorators',
'../core/src/events',
'../core/src/flow',
@@ -157,20 +157,18 @@ const config = {
[
'docusaurus-plugin-typedoc',
{
id: 'legacy',
out: 'legacy-api',
id: '2d',
out: '2d-api',
excludeExternals: true,
entryPoints: [
'../legacy/src/animations',
'../legacy/src/components',
'../legacy/src/decorators',
'../legacy/src/helpers',
'../legacy/src/scenes',
'../legacy/src/styles',
'../legacy/src/themes',
'../legacy/src/utils',
'../2d/src/components',
'../2d/src/curves',
'../2d/src/decorators',
'../2d/src/partials',
'../2d/src/scenes',
'../2d/src/utils',
],
tsconfig: '../legacy/tsconfig.json',
tsconfig: '../2d/tsconfig.json',
},
],
],

View File

@@ -18,11 +18,12 @@
"@docusaurus/core": "^2.0.1",
"@docusaurus/preset-classic": "^2.0.1",
"@mdx-js/react": "^1.6.22",
"@motion-canvas/examples": "*",
"@motion-canvas/player": "*",
"clsx": "^1.2.0",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-player": "^2.10.1"
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^2.0.0",

View File

@@ -53,8 +53,8 @@ const sidebars = {
},
{
type: 'category',
label: 'Legacy',
items: createApiSidebar('legacy-api'),
label: '2D',
items: createApiSidebar('2d-api'),
},
],
};

View File

@@ -45,3 +45,12 @@ a.button:hover {
color: var(--ifm-button-color);
text-decoration: none;
}
motion-canvas-player {
overflow: hidden;
width: 100%;
aspect-ratio: 16 / 9;
margin: 0 auto var(--ifm-leading);
border-radius: var(--ifm-global-radius);
background-color: var(--ifm-background-surface-color);
}

Binary file not shown.

1
packages/examples/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
output

View File

@@ -0,0 +1,18 @@
{
"name": "@motion-canvas/examples",
"private": true,
"version": "0.0.0",
"scripts": {
"serve": "vite",
"build": "tsc && vite build"
},
"dependencies": {
"@motion-canvas/core": "*",
"@motion-canvas/2d": "*"
},
"devDependencies": {
"@motion-canvas/ui": "*",
"@motion-canvas/vite-plugin": "*",
"vite": "^3.0.5"
}
}

View File

@@ -0,0 +1 @@
/// <reference types="@motion-canvas/core/project" />

View File

@@ -0,0 +1,3 @@
{
"version": 0
}

View File

@@ -0,0 +1,3 @@
{
"version": 0
}

View File

@@ -0,0 +1,7 @@
import {Project} from '@motion-canvas/core/lib';
import quickstart from './scenes/quickstart?scene';
export default new Project({
scenes: [quickstart],
});

View File

@@ -0,0 +1,3 @@
{
"version": 0
}

View File

@@ -0,0 +1,3 @@
{
"version": 0
}

View File

@@ -0,0 +1,17 @@
import {makeScene2D} from '@motion-canvas/2d/lib/scenes';
import {Circle} from '@motion-canvas/2d/lib/components';
import {useRef} from '@motion-canvas/core/lib/utils';
import {all} from '@motion-canvas/core/lib/flow';
export default makeScene2D(function* (view) {
const myCircle = useRef<Circle>();
view.add(
<Circle x={-300} ref={myCircle} width={240} height={240} fill="#e13238" />,
);
yield* all(
myCircle.value.position.x(300, 1).to(-300, 1),
myCircle.value.fill('#e6a700', 1).to('#e13238', 1),
);
});

View File

@@ -0,0 +1,7 @@
{
"extends": "@motion-canvas/2d/tsconfig.project.json",
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}

View File

@@ -0,0 +1,18 @@
import {defineConfig} from 'vite';
import motionCanvas from '@motion-canvas/vite-plugin';
export default defineConfig({
plugins: [
motionCanvas({
project: ['./src/quickstart.ts'],
}),
],
build: {
rollupOptions: {
output: {
dir: '../docs/static/examples',
entryFileNames: '[name].js',
},
},
},
});

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="icon"
type="image/png"
sizes="16x16"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAOwAAADsAEnxA+tAAABKklEQVQ4jcWTvUoDURCFv/y0A2tnZ95AW7FZwQdQSO9aRLAJbp0mPkGCTSCBmPQW8Q1SuKTU4AvkEeQO2K6MuSFmN+pCCg9cLhdmzpwzh1tK05RdUN6p+yeCftKK+kmrW4SguvGaNc3PGGRUVEE18x4Dl9dlheP76OZxMQUCIOzVa++/W2jENYaV7oqEWTMETMmhv7fi+w4i4IVhxaaeAq+9es0aL4CJqoaqOsmSrGNsxCZ16ideMeiMVPVrJyISqerC7IhIsN3CoGMeTfYceKARn6/sqOozcADkktmMcU3y5KeZrQQ4ARxwpqpts5O3kIEvGvnJ1vwB7PuquYgc5RVs4tZHeAe8WbOIlIA9r3IJU/DXcc61nXOpcy7I1hb9C5aOLTeHf/6NwCdua48fJxuYPgAAAABJRU5ErkJggg=="
/>
<title>Motion Canvas Player</title>
</head>
<body>
<script type="module" src="/src/main.ts"></script>
<motion-canvas-player
src="/@id/__x00__virtual:template"
height="320"
style="aspect-ratio: 16 / 9"
></motion-canvas-player>
</body>
</html>

View File

@@ -0,0 +1,34 @@
{
"name": "@motion-canvas/player",
"version": "0.0.0",
"description": "A custom element for displaying animations made with Motion Canvas",
"main": "dist/main.js",
"types": "types/main.d.ts",
"author": "motion-canvas",
"license": "MIT",
"type": "module",
"scripts": {
"serve": "vite",
"build": "tsc && vite build"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com/motion-canvas"
},
"repository": {
"type": "git",
"url": "https://github.com/motion-canvas/motion-canvas.git"
},
"files": [
"dist",
"types"
],
"peerDependencies": {
"@motion-canvas/core": "*"
},
"devDependencies": {
"@motion-canvas/core": "^12.0.0-alpha.2",
"terser": "^5.16.1",
"typescript": "^4.6.4",
"vite": "^3.0.4"
}
}

9
packages/player/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare module '*.scss?inline' {
const value: string;
export = value;
}
declare module '*.html?raw' {
const value: string;
export = value;
}

194
packages/player/src/main.ts Normal file
View File

@@ -0,0 +1,194 @@
import type {Project} from '@motion-canvas/core';
import styles from './styles.scss?inline';
import html from './template.html?raw';
const TEMPLATE = `<style>${styles}</style>${html}`;
const ID = 'motion-canvas-player';
enum State {
Initial = 'initial',
Loading = 'loading',
Ready = 'ready',
Error = 'error',
}
class MotionCanvasPlayer extends HTMLElement {
public static get observedAttributes() {
return ['src', 'quality', 'width', 'height'];
}
private get quality() {
const attr = this.getAttribute('quality');
return attr ? parseFloat(attr) : 1;
}
private get width() {
const attr = this.getAttribute('width');
return attr ? parseFloat(attr) : this.defaultWidth;
}
private get height() {
const attr = this.getAttribute('height');
return attr ? parseFloat(attr) : this.defaultHeight;
}
private readonly root: ShadowRoot;
private readonly canvas: HTMLCanvasElement;
private readonly overlay: HTMLCanvasElement;
private readonly button: HTMLDivElement;
private state = State.Initial;
private defaultWidth = 1920;
private defaultHeight = 1080;
private project: Project | null = null;
private abortController: AbortController | null = null;
private requestId: number | null = null;
private renderTime = 0;
private finished = false;
private playing = false;
public constructor() {
super();
this.root = this.attachShadow({mode: 'open'});
this.root.innerHTML = TEMPLATE;
this.canvas = this.root.querySelector('.canvas');
this.overlay = this.root.querySelector('.overlay');
this.button = this.root.querySelector('.button');
this.overlay.addEventListener('click', this.handleClick);
this.setState(State.Initial);
}
private handleClick = () => {
this.setPlaying(!this.playing);
this.button.animate(
[
{scale: `0.9`},
{
scale: `1`,
easing: 'ease-out',
},
],
{duration: 200},
);
};
private setState(state: State) {
this.state = state;
this.setPlaying(this.playing);
}
private setPlaying(value: boolean) {
if (this.state === State.Ready && value) {
this.playing = true;
this.request();
} else {
this.playing = false;
}
this.overlay.className = `overlay state-${this.state}`;
this.overlay.classList.toggle('playing', this.playing);
}
private shouldPlay() {
return this.state === State.Ready && this.playing;
}
private async updateSource(source: string) {
this.setState(State.Loading);
this.abortController?.abort();
this.abortController = new AbortController();
let project: Project;
try {
project = (
await import(/* webpackIgnore: true */ /* @vite-ignore */ source)
).default;
} catch (e) {
this.setState(State.Error);
return;
}
if (this.abortController.signal.aborted) return;
project.framerate = 60;
await project.recalculate();
if (this.abortController.signal.aborted) return;
await project.seek(0);
if (this.abortController.signal.aborted) return;
const size = project.getSize();
this.defaultWidth = size.width;
this.defaultHeight = size.height;
this.finished = false;
this.project = project;
this.project.resolutionScale = this.quality;
this.project.setSize(this.width, this.height);
this.project.logger.onLogged.subscribe(console.log);
this.project.setCanvas(this.canvas);
this.setState(State.Ready);
}
private async run() {
if (this.finished) {
await this.project.seek(0);
}
if (!this.shouldPlay()) return;
this.finished = await this.project.next();
if (!this.shouldPlay()) return;
await this.project.render();
}
private request() {
this.requestId ??= requestAnimationFrame(async time => {
this.requestId = null;
if (time - this.renderTime >= 990 / this.project.framerate) {
this.renderTime = time;
if (!this.shouldPlay()) return;
try {
await this.run();
} catch (e) {
this.setState(State.Error);
return;
}
}
this.request();
});
}
private attributeChangedCallback(name: string, oldValue: any, newValue: any) {
switch (name) {
case 'src':
this.updateSource(newValue);
break;
case 'quality':
if (this.project) {
this.project.resolutionScale = this.quality;
}
break;
case 'width':
case 'height':
if (this.project) {
this.project.setSize(this.width, this.height);
}
break;
}
}
private disconnectedCallback() {
if (this.playing) {
this.setPlaying(false);
}
}
}
if (!customElements.get(ID)) {
customElements.define(ID, MotionCanvasPlayer);
}

View File

@@ -0,0 +1,112 @@
$states: ('initial' 'loading' 'ready' 'error');
@each $state in $states {
.#{$state} {
display: none;
}
.state-#{$state} .#{$state} {
display: block;
}
}
:host {
position: relative;
display: block;
}
.overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
background-color: rgba(0, 0, 0, 0.54);
transition: opacity 0.1s;
&.state-ready {
cursor: pointer;
}
&:hover,
&:not(.playing) {
opacity: 1;
.button {
scale: 1;
transition: scale 0.1s ease-out;
}
}
}
.button {
width: 50%;
max-width: 96px;
aspect-ratio: 1;
scale: 0.5;
transition: scale 0.1s ease-in;
background-size: 100% 100%;
background-repeat: no-repeat;
opacity: 0.87;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZmlsbD0iI2ZmZmZmZiI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE0LjV2LTlsNiA0LjUtNiA0LjV6Ii8+PC9zdmc+');
.playing & {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI0IDI0IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0cHgiIGZpbGw9IiNmZmZmZmYiPjxnPjxyZWN0IGZpbGw9Im5vbmUiIGhlaWdodD0iMjQiIHdpZHRoPSIyNCIvPjxyZWN0IGZpbGw9Im5vbmUiIGhlaWdodD0iMjQiIHdpZHRoPSIyNCIvPjxyZWN0IGZpbGw9Im5vbmUiIGhlaWdodD0iMjQiIHdpZHRoPSIyNCIvPjwvZz48Zz48Zy8+PHBhdGggZD0iTTEyLDJDNi40OCwyLDIsNi40OCwyLDEyczQuNDgsMTAsMTAsMTBzMTAtNC40OCwxMC0xMFMxNy41MiwyLDEyLDJ6IE0xMSwxNkg5VjhoMlYxNnogTTE1LDE2aC0yVjhoMlYxNnoiLz48L2c+PC9zdmc+');
}
}
.canvas {
width: 100%;
display: block;
}
.message {
font-family: 'JetBrains Mono', sans-serif;
text-align: center;
font-size: 20px;
padding: 8px 16px;
margin: 16px;
border-radius: 4px;
color: rgba(255, 255, 255, 0.6);
background-color: rgba(0, 0, 0, 0.87);
}
.loader {
width: 50%;
max-width: 96px;
display: none;
rotate: -90deg;
animation: stroke 2s cubic-bezier(0.5, 0, 0.5, 1) infinite,
rotate 2s linear infinite;
}
$circumference: calc(2 * 3.1415926536 * 9px);
@keyframes stroke {
0% {
stroke-dasharray: $circumference * 0.1 $circumference * 0.9;
stroke-dashoffset: $circumference * 0.05;
}
50% {
stroke-dasharray: $circumference * 0.9 $circumference * 0.1;
stroke-dashoffset: $circumference * -0.05;
}
100% {
stroke-dasharray: $circumference * 0.1 $circumference * 0.9;
stroke-dashoffset: $circumference * -0.95;
}
}
@keyframes rotate {
0% {
rotate: -110deg;
}
100% {
rotate: 250deg;
}
}

View File

@@ -0,0 +1,18 @@
<canvas class="canvas"></canvas>
<div class="overlay">
<div class="button ready"></div>
<div class="message initial">No video provided.</div>
<div class="message error">
An error occurred while loading the animation.
</div>
<svg
class="loader loading"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke="#ffffff"
stroke-width="2"
fill="transparent"
>
<circle cx="12" cy="12" r="9" />
</svg>
</div>

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"noImplicitAny": true,
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": false,
"useDefineForClassFields": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"types": ["node"],
"lib": ["DOM", "DOM.Iterable", "ESNext"]
},
"include": ["src"]
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"extends": ["../../tsdoc.json"]
}

View File

@@ -0,0 +1,26 @@
import {defineConfig} from 'vite';
import * as fs from 'fs';
export default defineConfig({
build: {
minify: 'esbuild',
lib: {
entry: 'src/main.ts',
formats: ['es'],
fileName: 'main',
},
rollupOptions: {
external: ['@motion-canvas/core'],
},
},
plugins: [
{
name: 'template',
load(id) {
if (id === '\0virtual:template') {
return fs.readFileSync('../template/dist/project.js').toString();
}
},
},
],
});

View File

@@ -2,10 +2,8 @@ import styles from './Tabs.module.scss';
import {Icon, IconType} from '../controls';
import {ComponentChildren} from 'preact';
import {useCallback, useEffect, useLayoutEffect} from 'preact/hooks';
import {useCallback, useLayoutEffect} from 'preact/hooks';
import {classes} from '../../utils';
import {useStorage} from '../../hooks';
import {useInspection} from '../../contexts';
export enum TabType {
Link,