mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 14:57:56 -05:00
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:
@@ -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
79
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
3
packages/docs/.gitignore
vendored
3
packages/docs/.gitignore
vendored
@@ -8,7 +8,8 @@
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
docs/core-api
|
||||
docs/legacy-api
|
||||
docs/2d-api
|
||||
static/examples
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -53,8 +53,8 @@ const sidebars = {
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Legacy',
|
||||
items: createApiSidebar('legacy-api'),
|
||||
label: '2D',
|
||||
items: createApiSidebar('2d-api'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
BIN
packages/docs/static/video/animation.mp4
vendored
BIN
packages/docs/static/video/animation.mp4
vendored
Binary file not shown.
1
packages/examples/.gitignore
vendored
Normal file
1
packages/examples/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
output
|
||||
18
packages/examples/package.json
Normal file
18
packages/examples/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
packages/examples/src/motion-canvas.d.ts
vendored
Normal file
1
packages/examples/src/motion-canvas.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="@motion-canvas/core/project" />
|
||||
3
packages/examples/src/project.meta
Normal file
3
packages/examples/src/project.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": 0
|
||||
}
|
||||
3
packages/examples/src/quickstart.meta
Normal file
3
packages/examples/src/quickstart.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": 0
|
||||
}
|
||||
7
packages/examples/src/quickstart.ts
Normal file
7
packages/examples/src/quickstart.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
|
||||
import quickstart from './scenes/quickstart?scene';
|
||||
|
||||
export default new Project({
|
||||
scenes: [quickstart],
|
||||
});
|
||||
3
packages/examples/src/scenes/example.meta
Normal file
3
packages/examples/src/scenes/example.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": 0
|
||||
}
|
||||
3
packages/examples/src/scenes/quickstart.meta
Normal file
3
packages/examples/src/scenes/quickstart.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": 0
|
||||
}
|
||||
17
packages/examples/src/scenes/quickstart.tsx
Normal file
17
packages/examples/src/scenes/quickstart.tsx
Normal 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),
|
||||
);
|
||||
});
|
||||
7
packages/examples/tsconfig.json
Normal file
7
packages/examples/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@motion-canvas/2d/tsconfig.project.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/examples/vite.config.ts
Normal file
18
packages/examples/vite.config.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
22
packages/player/index.html
Normal file
22
packages/player/index.html
Normal 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>
|
||||
34
packages/player/package.json
Normal file
34
packages/player/package.json
Normal 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
9
packages/player/src/global.d.ts
vendored
Normal 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
194
packages/player/src/main.ts
Normal 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);
|
||||
}
|
||||
112
packages/player/src/styles.scss
Normal file
112
packages/player/src/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
18
packages/player/src/template.html
Normal file
18
packages/player/src/template.html
Normal 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>
|
||||
19
packages/player/tsconfig.json
Normal file
19
packages/player/tsconfig.json
Normal 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"]
|
||||
}
|
||||
4
packages/player/tsdoc.json
Normal file
4
packages/player/tsdoc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["../../tsdoc.json"]
|
||||
}
|
||||
26
packages/player/vite.config.ts
Normal file
26
packages/player/vite.config.ts
Normal 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();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user