mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 06:48:12 -05:00
feat: support for multiple projects (#57)
This PR makes it possible to specify multiple project files when configuring Motion Canvas:
```ts
export default defineConfig({
plugins: [
motionCanvas({
project: [
'./src/projectA.ts',
'./src/projectB.ts',
],
}),
],
});
```
When visiting [http://localhost:9000](http://localhost:9000),
Motion Canvas will display a project selection screen.
Each project receives its own url and is hosted separately.
For instance, `./src/projectA.ts`
would be available at [http://localhost:9000/projectA](http://localhost:9000/projectA).
When editing, it's possible to open the project selection view by clicking on the
Motion Canvas icon located in the top left corner.
When rendering, each project is saved in its own folder inside the output directory.
The project name should no longer be configured when creating a new instance.
Instead, the name is derived based on the file name.
```ts
export default new Project({
name: 'something' // <- does nothing
scenes: [example],
background: '#141414',
});
```
This commit is contained in:
@@ -10,7 +10,7 @@ export const ProjectSize = {
|
||||
};
|
||||
|
||||
export interface ProjectConfig {
|
||||
name: string;
|
||||
name?: string;
|
||||
scenes: Scene[];
|
||||
audio?: string;
|
||||
audioOffset?: number;
|
||||
@@ -140,7 +140,6 @@ export class Project {
|
||||
private height: number;
|
||||
|
||||
public constructor({
|
||||
name,
|
||||
scenes,
|
||||
audio,
|
||||
audioOffset,
|
||||
@@ -150,7 +149,6 @@ export class Project {
|
||||
}: ProjectConfig) {
|
||||
this.setCanvas(canvas);
|
||||
this.setSize(size);
|
||||
this.name = name;
|
||||
this.background = background;
|
||||
|
||||
if (audio) {
|
||||
@@ -320,6 +318,7 @@ export class Project {
|
||||
frame,
|
||||
data: this.canvas.toDataURL(this._fileType, this._quality),
|
||||
mimeType: this._fileType,
|
||||
project: this.name,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
7
packages/core/src/vite-env.d.ts
vendored
7
packages/core/src/vite-env.d.ts
vendored
@@ -6,7 +6,12 @@ declare module 'vite/types/customEvent' {
|
||||
interface CustomEventMap {
|
||||
'motion-canvas:meta': {source: string; data: import('./Meta').Metadata};
|
||||
'motion-canvas:meta-ack': {source: string};
|
||||
'motion-canvas:export': {frame: number; data: string; mimeType: string};
|
||||
'motion-canvas:export': {
|
||||
frame: number;
|
||||
data: string;
|
||||
mimeType: string;
|
||||
project: string;
|
||||
};
|
||||
'motion-canvas:export-ack': {frame: number};
|
||||
'motion-canvas:assets': {urls: string[]};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {Project} from '@motion-canvas/core/lib';
|
||||
import example from './scenes/example?scene';
|
||||
|
||||
export default new Project({
|
||||
name: 'project',
|
||||
scenes: [example],
|
||||
background: '#141414',
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import {Project} from '@motion-canvas/core/lib';
|
||||
import example from './scenes/example?scene';
|
||||
|
||||
export default new Project({
|
||||
name: 'project',
|
||||
scenes: [example],
|
||||
background: '#141414',
|
||||
});
|
||||
|
||||
@@ -22,13 +22,13 @@ the entire length of the video, it will only render a portion of the frames in
|
||||
your animation. This can be useful for quickly rendering small changes in your
|
||||
animation.
|
||||
|
||||
##### FPS (Frames Per Second)
|
||||
##### Frame rate
|
||||
|
||||
The frame rate at which the preview plays and the number of frames that will
|
||||
render per one second of runtime. The most common values are 24, 30, and 60,
|
||||
though any whole integer value is allowed. Motion Canvas animations are
|
||||
resilient to changes in frame rate, so most animations will not be affected by
|
||||
changing the FPS.
|
||||
changing it.
|
||||
|
||||
##### Resolution
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {Project} from '@motion-canvas/core/lib';
|
||||
import example from './scenes/example?scene';
|
||||
|
||||
export default new Project({
|
||||
name: 'project',
|
||||
scenes: [example],
|
||||
background: '#141414',
|
||||
});
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
sizes="16x16"
|
||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAOwAAADsAEnxA+tAAABKklEQVQ4jcWTvUoDURCFv/y0A2tnZ95AW7FZwQdQSO9aRLAJbp0mPkGCTSCBmPQW8Q1SuKTU4AvkEeQO2K6MuSFmN+pCCg9cLhdmzpwzh1tK05RdUN6p+yeCftKK+kmrW4SguvGaNc3PGGRUVEE18x4Dl9dlheP76OZxMQUCIOzVa++/W2jENYaV7oqEWTMETMmhv7fi+w4i4IVhxaaeAq+9es0aL4CJqoaqOsmSrGNsxCZ16ideMeiMVPVrJyISqerC7IhIsN3CoGMeTfYceKARn6/sqOozcADkktmMcU3y5KeZrQQ4ARxwpqpts5O3kIEvGvnJ1vwB7PuquYgc5RVs4tZHeAe8WbOIlIA9r3IJU/DXcc61nXOpcy7I1hb9C5aOLTeHf/6NwCdua48fJxuYPgAAAABJRU5ErkJggg=="
|
||||
/>
|
||||
<link rel="stylesheet" href="{{style}}" />
|
||||
<title>Motion Canvas</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="/@id/__x00__virtual:editor"></script>
|
||||
<script type="module" src="{{source}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,13 +9,17 @@
|
||||
sizes="16x16"
|
||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAOwAAADsAEnxA+tAAABKklEQVQ4jcWTvUoDURCFv/y0A2tnZ95AW7FZwQdQSO9aRLAJbp0mPkGCTSCBmPQW8Q1SuKTU4AvkEeQO2K6MuSFmN+pCCg9cLhdmzpwzh1tK05RdUN6p+yeCftKK+kmrW4SguvGaNc3PGGRUVEE18x4Dl9dlheP76OZxMQUCIOzVa++/W2jENYaV7oqEWTMETMmhv7fi+w4i4IVhxaaeAq+9es0aL4CJqoaqOsmSrGNsxCZ16ideMeiMVPVrJyISqerC7IhIsN3CoGMeTfYceKARn6/sqOozcADkktmMcU3y5KeZrQQ4ARxwpqpts5O3kIEvGvnJ1vwB7PuquYgc5RVs4tZHeAe8WbOIlIA9r3IJU/DXcc61nXOpcy7I1hb9C5aOLTeHf/6NwCdua48fJxuYPgAAAABJRU5ErkJggg=="
|
||||
/>
|
||||
<title>Editor | Motion Canvas</title>
|
||||
<title>Motion Canvas</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
import project from '@motion-canvas/template/dist/template.mjs';
|
||||
import editor from '/src/main.tsx';
|
||||
editor(project);
|
||||
import {index} from '/src/main.tsx';
|
||||
index([
|
||||
{name: 'project.html', url: './src/project.ts'},
|
||||
{name: 'exampleA', url: './src/exampleA.ts'},
|
||||
{name: 'exampleB', url: './src/exampleB.ts'},
|
||||
{name: 'exampleC', url: './src/exampleC.ts'},
|
||||
]);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"types",
|
||||
"editor.html"
|
||||
"types"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@motion-canvas/core": "*"
|
||||
|
||||
21
packages/ui/project.html
Normal file
21
packages/ui/project.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!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</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
import project from '@motion-canvas/template/dist/projectA';
|
||||
import {editor} from '/src/main.tsx';
|
||||
editor(project);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,42 +0,0 @@
|
||||
import './index.scss';
|
||||
|
||||
import {Sidebar} from './components/sidebar';
|
||||
import {Timeline} from './components/timeline';
|
||||
import {Viewport} from './components/viewport';
|
||||
import {ResizeableLayout} from './components/layout';
|
||||
import {useState, useMemo} from 'preact/hooks';
|
||||
import {AppContext} from './AppContext';
|
||||
import type {InspectedElement} from '@motion-canvas/core/lib/scenes';
|
||||
|
||||
export function App() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [inspectedElement, setInspectedElement] =
|
||||
useState<InspectedElement | null>(null);
|
||||
|
||||
const contextState = useMemo(
|
||||
() => ({
|
||||
inspectedElement,
|
||||
setInspectedElement,
|
||||
}),
|
||||
[inspectedElement],
|
||||
);
|
||||
|
||||
return (
|
||||
<ResizeableLayout
|
||||
id={'main-timeline'}
|
||||
size={0.7}
|
||||
vertical
|
||||
start={
|
||||
<AppContext.Provider value={contextState}>
|
||||
<ResizeableLayout
|
||||
resizeable={sidebarOpen}
|
||||
id={'sidebar-vieport'}
|
||||
start={<Sidebar setOpen={setSidebarOpen} />}
|
||||
end={<Viewport />}
|
||||
/>
|
||||
</AppContext.Provider>
|
||||
}
|
||||
end={<Timeline />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import {createContext} from 'preact';
|
||||
import type {InspectedElement} from '@motion-canvas/core/lib/scenes';
|
||||
|
||||
export interface AppState {
|
||||
inspectedElement: InspectedElement | null;
|
||||
setInspectedElement: (element: InspectedElement | null) => void;
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppState>({
|
||||
inspectedElement: null,
|
||||
setInspectedElement: () => {
|
||||
throw new Error('setSelectedNode not implemented');
|
||||
},
|
||||
});
|
||||
|
||||
export {AppContext};
|
||||
26
packages/ui/src/Editor.tsx
Normal file
26
packages/ui/src/Editor.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import {Sidebar} from './components/sidebar';
|
||||
import {Timeline} from './components/timeline';
|
||||
import {Viewport} from './components/viewport';
|
||||
import {ResizeableLayout} from './components/layout';
|
||||
import {useState} from 'preact/hooks';
|
||||
|
||||
export function Editor() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ResizeableLayout
|
||||
id={'main-timeline'}
|
||||
size={0.7}
|
||||
vertical
|
||||
start={
|
||||
<ResizeableLayout
|
||||
resizeable={sidebarOpen}
|
||||
id={'sidebar-vieport'}
|
||||
start={<Sidebar setOpen={setSidebarOpen} />}
|
||||
end={<Viewport />}
|
||||
/>
|
||||
}
|
||||
end={<Timeline />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
52
packages/ui/src/Index.module.scss
Normal file
52
packages/ui/src/Index.module.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
.root {
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
margin: 16px;
|
||||
max-width: 480px;
|
||||
max-height: calc(100% - 32px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
background-color: var(--surface-color);
|
||||
border-bottom: 1px solid var(--background-color-dark);
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.element {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-bottom: 4px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.32);
|
||||
}
|
||||
26
packages/ui/src/Index.tsx
Normal file
26
packages/ui/src/Index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import styles from './Index.module.scss';
|
||||
|
||||
export interface ProjectData {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface IndexProps {
|
||||
projects: ProjectData[];
|
||||
}
|
||||
|
||||
export function Index({projects}: IndexProps) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>Projects</div>
|
||||
<div className={styles.list}>
|
||||
{projects.map(project => (
|
||||
<a className={styles.element} href={`/${project.name}`}>
|
||||
<div className={styles.title}>{project.name}</div>
|
||||
<div className={styles.subtitle}>{project.url}</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
height: 24px;
|
||||
display: block;
|
||||
content: '';
|
||||
background: #ffffff;
|
||||
background: var(--icon-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
}
|
||||
|
||||
&.main::after {
|
||||
background-color: #fff;
|
||||
background-color: var(--icon-color);
|
||||
}
|
||||
|
||||
.iconInput + &:hover::after {
|
||||
background-color: #fff;
|
||||
background-color: var(--icon-color);
|
||||
}
|
||||
|
||||
.iconInput:checked + & {
|
||||
@@ -49,7 +49,7 @@
|
||||
background: var(--theme);
|
||||
}
|
||||
&.main:after {
|
||||
background: #ffffff;
|
||||
background: var(--icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background-color: #fff;
|
||||
background-color: var(--icon-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,13 +88,14 @@
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.select,
|
||||
.input,
|
||||
.button,
|
||||
.value {
|
||||
background: #404040;
|
||||
background: var(--surface-color-light);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
@@ -108,13 +109,13 @@
|
||||
min-width: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: #333;
|
||||
background-color: var(--surface-color-hover);
|
||||
}
|
||||
|
||||
&.main {
|
||||
background-color: var(--theme);
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
font-weight: 700;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 2px white inset;
|
||||
@@ -165,6 +166,9 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.motionCanvas:after {
|
||||
-webkit-mask-image: url('../../img/icons/motion_canvas.svg');
|
||||
}
|
||||
.pause:after {
|
||||
-webkit-mask-image: url('../../img/icons/pause.svg');
|
||||
}
|
||||
|
||||
@@ -4,13 +4,22 @@ import {IconType} from './IconType';
|
||||
import {classes} from '../../utils';
|
||||
import {JSX} from 'preact';
|
||||
|
||||
interface IconProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
type IntrinsicTag = keyof JSX.IntrinsicElements;
|
||||
|
||||
type IconProps<T extends IntrinsicTag> = JSX.IntrinsicElements[T] & {
|
||||
type: IconType;
|
||||
className?: string;
|
||||
}
|
||||
as?: T;
|
||||
};
|
||||
|
||||
export function Icon({type, className, ...rest}: IconProps) {
|
||||
export function Icon<T extends IntrinsicTag = 'div'>({
|
||||
type,
|
||||
className,
|
||||
as = 'div' as T,
|
||||
...rest
|
||||
}: IconProps<T>) {
|
||||
const As = as as string;
|
||||
return (
|
||||
<div className={classes(styles.icon, styles[type], className)} {...rest} />
|
||||
<As className={classes(styles.icon, styles[type], className)} {...rest} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
export enum IconType {
|
||||
dragIndicator = 'dragIndicator',
|
||||
motionCanvas = 'motionCanvas',
|
||||
pause = 'pause',
|
||||
play = 'play',
|
||||
repeat = 'repeat',
|
||||
skipNext = 'skipNext',
|
||||
skipPrevious = 'skipPrevious',
|
||||
volumeOn = 'volumeOn',
|
||||
volumeOff = 'volumeOff',
|
||||
tune = 'tune',
|
||||
videoSettings = 'videoSettings',
|
||||
volumeOn = 'volumeOn',
|
||||
volumeOff = 'volumeOff',
|
||||
schedule = 'schedule',
|
||||
dragIndicator = 'dragIndicator',
|
||||
}
|
||||
|
||||
@@ -4,5 +4,11 @@ import type {JSX} from 'preact';
|
||||
type LabelProps = JSX.HTMLAttributes<HTMLLabelElement>;
|
||||
|
||||
export function Label(props: LabelProps) {
|
||||
return <label className={styles.label} {...props} />;
|
||||
return (
|
||||
<label
|
||||
title={props.children as string}
|
||||
className={styles.label}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
background-color: #141414;
|
||||
background-color: var(--background-color-dark);
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.progress {
|
||||
height: 2px;
|
||||
background-color: #080808;
|
||||
background-color: var(--background-color-dark);
|
||||
}
|
||||
|
||||
.progressFill {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {parseColor} from 'mix-color';
|
||||
import {useContext, useMemo, useState} from 'preact/hooks';
|
||||
import {AppContext} from '../../AppContext';
|
||||
import {useMemo, useState} from 'preact/hooks';
|
||||
|
||||
import {useCurrentScene, useCurrentFrame} from '../../hooks';
|
||||
import {classes} from '../../utils';
|
||||
@@ -10,9 +9,10 @@ import styles from './Sidebar.module.scss';
|
||||
import type {InspectedAttributes} from '@motion-canvas/core/lib/scenes';
|
||||
import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
||||
import {Pane} from '../tabs';
|
||||
import {useInspection} from '../../contexts';
|
||||
|
||||
export function Properties() {
|
||||
const {inspectedElement} = useContext(AppContext);
|
||||
const {inspectedElement} = useInspection();
|
||||
const scene = useCurrentScene();
|
||||
|
||||
const {attributes, supportsInspection} = useMemo(
|
||||
|
||||
@@ -32,7 +32,7 @@ export function Rendering() {
|
||||
return (
|
||||
<Pane title="Rendering">
|
||||
<Group>
|
||||
<Label>Range</Label>
|
||||
<Label>range</Label>
|
||||
<Input
|
||||
min={0}
|
||||
max={state.endFrame}
|
||||
@@ -54,7 +54,7 @@ export function Rendering() {
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Label>FPS</Label>
|
||||
<Label>frame rate</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -66,7 +66,7 @@ export function Rendering() {
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Label>Resolution</Label>
|
||||
<Label>resolution</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -76,7 +76,7 @@ export function Rendering() {
|
||||
player.project.setSize(value, height);
|
||||
}}
|
||||
/>
|
||||
X
|
||||
x
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -88,7 +88,7 @@ export function Rendering() {
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Label>Scale</Label>
|
||||
<Label>scale</Label>
|
||||
<Select
|
||||
options={scales}
|
||||
value={state.scale}
|
||||
@@ -96,7 +96,7 @@ export function Rendering() {
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Label>Color Space</Label>
|
||||
<Label>color space</Label>
|
||||
<Select
|
||||
options={colorSpaces}
|
||||
value={state.colorSpace}
|
||||
@@ -104,7 +104,7 @@ export function Rendering() {
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Label>File Type</Label>
|
||||
<Label>file type</Label>
|
||||
<Select
|
||||
options={fileTypes}
|
||||
value={state.fileType}
|
||||
@@ -112,7 +112,7 @@ export function Rendering() {
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Label>Quality</Label>
|
||||
<Label>quality</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.root {
|
||||
background-color: #242424;
|
||||
background-color: var(--surface-color);
|
||||
}
|
||||
|
||||
.copied {
|
||||
@@ -48,7 +48,7 @@
|
||||
display: block;
|
||||
content: '';
|
||||
margin-bottom: 4px;
|
||||
background-color: #444;
|
||||
background-color: var(--surface-color-light);
|
||||
}
|
||||
|
||||
.threadList {
|
||||
@@ -67,7 +67,7 @@
|
||||
width: 16px;
|
||||
height: 2px;
|
||||
margin-right: 4px;
|
||||
background-color: #444;
|
||||
background-color: var(--surface-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {IconType} from '../controls';
|
||||
import {Tabs} from '../tabs';
|
||||
import {Tabs, TabType} from '../tabs';
|
||||
import {Properties} from './Properties';
|
||||
import {Rendering} from './Rendering';
|
||||
import {Threads} from './Threads';
|
||||
@@ -10,19 +10,35 @@ interface SidebarProps {
|
||||
|
||||
export function Sidebar({setOpen}: SidebarProps) {
|
||||
return (
|
||||
<Tabs onToggle={tab => setOpen(tab >= 0)} id="sidebar">
|
||||
{{
|
||||
icon: IconType.tune,
|
||||
pane: <Properties />,
|
||||
}}
|
||||
{{
|
||||
icon: IconType.videoSettings,
|
||||
pane: <Rendering />,
|
||||
}}
|
||||
{{
|
||||
icon: IconType.schedule,
|
||||
pane: <Threads />,
|
||||
}}
|
||||
</Tabs>
|
||||
<>
|
||||
<Tabs onToggle={tab => setOpen(tab >= 0)} id="sidebar">
|
||||
{{
|
||||
type: TabType.Link,
|
||||
icon: IconType.motionCanvas,
|
||||
url: window.location.pathname === '/' ? undefined : '/',
|
||||
}}
|
||||
{{
|
||||
type: TabType.Space,
|
||||
}}
|
||||
{{
|
||||
type: TabType.Pane,
|
||||
icon: IconType.tune,
|
||||
pane: <Properties />,
|
||||
}}
|
||||
{{
|
||||
type: TabType.Pane,
|
||||
icon: IconType.videoSettings,
|
||||
pane: <Rendering />,
|
||||
}}
|
||||
{{
|
||||
type: TabType.Pane,
|
||||
icon: IconType.schedule,
|
||||
pane: <Threads />,
|
||||
}}
|
||||
{{
|
||||
type: TabType.Space,
|
||||
}}
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background-color: #242424;
|
||||
background-color: var(--surface-color);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
padding: 4px 0;
|
||||
width: 48px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background-color: #181818;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -27,9 +28,18 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #242424;
|
||||
background: var(--surface-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.16;
|
||||
}
|
||||
}
|
||||
|
||||
.space {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.panes {
|
||||
|
||||
@@ -2,49 +2,89 @@ import styles from './Tabs.module.scss';
|
||||
|
||||
import {Icon, IconType} from '../controls';
|
||||
import {ComponentChildren} from 'preact';
|
||||
import {useCallback, useContext, useEffect} from 'preact/hooks';
|
||||
import {useCallback, useEffect, useLayoutEffect} from 'preact/hooks';
|
||||
import {classes} from '../../utils';
|
||||
import {useStorage} from '../../hooks';
|
||||
import {AppContext} from '../../AppContext';
|
||||
import {useInspection} from '../../contexts';
|
||||
|
||||
export enum TabType {
|
||||
Link,
|
||||
Pane,
|
||||
Space,
|
||||
}
|
||||
|
||||
type Tab =
|
||||
| {
|
||||
type: TabType.Pane;
|
||||
icon: IconType;
|
||||
pane: ComponentChildren;
|
||||
}
|
||||
| {
|
||||
type: TabType.Link;
|
||||
icon: IconType;
|
||||
url?: string;
|
||||
}
|
||||
| {
|
||||
type: TabType.Space;
|
||||
};
|
||||
|
||||
interface TabsProps {
|
||||
children: {icon: IconType; pane: ComponentChildren}[];
|
||||
children: Tab[];
|
||||
onToggle?: (tab: number) => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function Tabs({children, onToggle, id}: TabsProps) {
|
||||
const [tab, setTab] = useStorage(id, 1);
|
||||
const [tab, setTab] = useStorage(id, -1);
|
||||
const toggleTab = useCallback(
|
||||
(value: number) => {
|
||||
const newTab = value === tab ? -1 : value;
|
||||
const newTab = value === tab ? -1 : getPane(children[value]) ? value : -1;
|
||||
setTab(newTab);
|
||||
},
|
||||
[tab, setTab],
|
||||
[tab, setTab, children],
|
||||
);
|
||||
useEffect(() => {
|
||||
onToggle?.(tab);
|
||||
useLayoutEffect(() => {
|
||||
if (tab > -1 && !getPane(children[tab])) {
|
||||
setTab(-1);
|
||||
} else {
|
||||
onToggle?.(tab);
|
||||
}
|
||||
}, [onToggle, tab]);
|
||||
|
||||
const {inspectedElement} = useContext(AppContext);
|
||||
const {inspectedElement} = useInspection();
|
||||
useEffect(() => {
|
||||
if (inspectedElement && tab !== -1) {
|
||||
setTab(0);
|
||||
setTab(2);
|
||||
}
|
||||
}, [inspectedElement]);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.panes}>{children[tab]?.pane}</div>
|
||||
<div className={styles.panes}>{getPane(children[tab])}</div>
|
||||
<div className={styles.tabs}>
|
||||
{children.map(({icon}, index) => (
|
||||
<Icon
|
||||
type={icon}
|
||||
onClick={() => toggleTab(index)}
|
||||
className={classes(styles.tab, [styles.active, tab === index])}
|
||||
/>
|
||||
))}
|
||||
{children.map((data, index) =>
|
||||
data.type === TabType.Link ? (
|
||||
<Icon
|
||||
as="a"
|
||||
type={data.icon}
|
||||
href={data.url}
|
||||
className={classes(styles.tab, [styles.disabled, !data.url])}
|
||||
/>
|
||||
) : data.type === TabType.Pane ? (
|
||||
<Icon
|
||||
type={data.icon}
|
||||
onClick={() => toggleTab(index)}
|
||||
className={classes(styles.tab, [styles.active, tab === index])}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.space} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getPane(tab: Tab) {
|
||||
return tab && tab.type === TabType.Pane ? tab.pane : false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.root {
|
||||
background-color: #242424;
|
||||
background-color: var(--surface-color);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
overflow-y: hidden;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
background-color: #181818;
|
||||
background-color: var(--background-color);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -38,14 +38,14 @@
|
||||
.trackContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #242424;
|
||||
background-color: var(--surface-color);
|
||||
|
||||
&::before {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: block;
|
||||
content: '';
|
||||
background-color: #181818;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
.scene {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
background-color: #444;
|
||||
background-color: var(--surface-color-light);
|
||||
overflow: hidden;
|
||||
|
||||
.transition {
|
||||
@@ -208,7 +208,7 @@
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: bold;
|
||||
color: rgba(0, 0, 0, 1);
|
||||
color: #000000;
|
||||
content: attr(data-frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import styles from './Viewport.module.scss';
|
||||
import {useCurrentScene, usePlayerTime} from '../../hooks';
|
||||
import {useContext, useLayoutEffect, useRef} from 'preact/hooks';
|
||||
import {ViewportContext} from './ViewportContext';
|
||||
import {AppContext} from '../../AppContext';
|
||||
import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
||||
import {useInspection} from '../../contexts';
|
||||
|
||||
export function Debug() {
|
||||
const time = usePlayerTime();
|
||||
@@ -11,7 +11,7 @@ export function Debug() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>();
|
||||
const contextRef = useRef<CanvasRenderingContext2D>();
|
||||
const state = useContext(ViewportContext);
|
||||
const {inspectedElement, setInspectedElement} = useContext(AppContext);
|
||||
const {inspectedElement, setInspectedElement} = useInspection();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
contextRef.current ??= canvasRef.current.getContext('2d');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {useCallback, useContext, useEffect, useRef} from 'preact/hooks';
|
||||
import {AppContext} from '../../AppContext';
|
||||
import {useCallback, useEffect, useRef} from 'preact/hooks';
|
||||
|
||||
import {
|
||||
useCurrentScene,
|
||||
@@ -15,7 +14,7 @@ import {Grid} from './Grid';
|
||||
import styles from './Viewport.module.scss';
|
||||
import {ViewportContext, ViewportState} from './ViewportContext';
|
||||
import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
||||
import {usePlayer} from '../../contexts';
|
||||
import {useInspection, usePlayer} from '../../contexts';
|
||||
|
||||
const ZOOM_SPEED = 0.1;
|
||||
|
||||
@@ -36,7 +35,7 @@ export function View() {
|
||||
zoom: 1,
|
||||
grid: false,
|
||||
});
|
||||
const {inspectedElement, setInspectedElement} = useContext(AppContext);
|
||||
const {inspectedElement, setInspectedElement} = useInspection();
|
||||
|
||||
useEffect(() => {
|
||||
setState({
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
|
||||
.playback {
|
||||
background: #181818;
|
||||
background: var(--background-color);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content 1fr;
|
||||
padding: 12px;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './inspection';
|
||||
export * from './player';
|
||||
export * from './project';
|
||||
export * from './timeline';
|
||||
|
||||
38
packages/ui/src/contexts/inspection.tsx
Normal file
38
packages/ui/src/contexts/inspection.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import {ComponentChildren, createContext} from 'preact';
|
||||
import type {InspectedElement} from '@motion-canvas/core/lib/scenes';
|
||||
import {useContext, useMemo, useState} from 'preact/hooks';
|
||||
|
||||
export interface AppState {
|
||||
inspectedElement: InspectedElement | null;
|
||||
setInspectedElement: (element: InspectedElement | null) => void;
|
||||
}
|
||||
|
||||
const InspectionContext = createContext<AppState>({
|
||||
inspectedElement: null,
|
||||
setInspectedElement: () => {
|
||||
throw new Error('setSelectedNode not implemented');
|
||||
},
|
||||
});
|
||||
|
||||
export function InspectionProvider({children}: {children: ComponentChildren}) {
|
||||
const [inspectedElement, setInspectedElement] =
|
||||
useState<InspectedElement | null>(null);
|
||||
|
||||
const state = useMemo(
|
||||
() => ({
|
||||
inspectedElement,
|
||||
setInspectedElement,
|
||||
}),
|
||||
[inspectedElement],
|
||||
);
|
||||
|
||||
return (
|
||||
<InspectionContext.Provider value={state}>
|
||||
{children}
|
||||
</InspectionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useInspection() {
|
||||
return useContext(InspectionContext);
|
||||
}
|
||||
14
packages/ui/src/img/icons/motion_canvas.svg
Normal file
14
packages/ui/src/img/icons/motion_canvas.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
||||
<path d="M23.6,17c-0.1,0.7-0.7,1.1-1.3,1.1c-0.1,0-0.1,0-0.2,0l-4.8-0.8l0.8,4.8c0.1,0.7-0.4,1.4-1.1,1.5c-0.1,0-0.1,0-0.2,0
|
||||
c-0.7,0-1.2-0.5-1.3-1.1l-0.7-4.8l-4.3,2.2c-0.7,0.3-1.5,0.1-1.8-0.6c-0.1-0.2-0.1-0.4-0.1-0.6c0-0.5,0.3-0.9,0.7-1.2l4.3-2.2
|
||||
L10.1,12c-0.5-0.5-0.5-1.4,0-1.9c0.3-0.3,0.6-0.4,0.9-0.4s0.7,0.1,0.9,0.4l3.4,3.4l2.2-4.3c0.3-0.7,1.1-0.9,1.8-0.6
|
||||
c0.5,0.2,0.7,0.7,0.7,1.2c0,0.2-0.1,0.4-0.1,0.6l-2.2,4.3l4.8,0.7C23.3,15.6,23.7,16.3,23.6,17z"/>
|
||||
<path d="M15.5,8.9L15.5,8.9c-0.5,0.5-1.4,0.5-1.9,0l-2.8-2.8c-0.5-0.5-0.5-1.4,0-1.9l0,0c0.5-0.5,1.4-0.5,1.9,0L15.5,7
|
||||
C16,7.5,16,8.4,15.5,8.9z"/>
|
||||
<path d="M9.4,2.8L9.4,2.8C8.8,3.3,8,3.3,7.5,2.8l0,0C7,2.2,7,1.4,7.5,0.9l0,0c0.5-0.5,1.4-0.5,1.9,0l0,0C9.9,1.4,9.9,2.2,9.4,2.8z"
|
||||
/>
|
||||
<path d="M8.4,8.4L8.4,8.4c-0.5,0.5-1.4,0.5-1.9,0L2.3,4.2c-0.5-0.5-0.5-1.4,0-1.9l0,0c0.5-0.5,1.4-0.5,1.9,0l4.2,4.2
|
||||
C8.9,7.1,8.9,7.9,8.4,8.4z"/>
|
||||
<path d="M8.9,15.5L8.9,15.5C8.4,16,7.5,16,7,15.5L1.3,9.8c-0.5-0.5-0.5-1.4,0-1.9l0,0c0.5-0.5,1.4-0.5,1.9,0l5.7,5.7
|
||||
C9.4,14.1,9.4,15,8.9,15.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,21 +1,30 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');
|
||||
|
||||
:root {
|
||||
--theme: #33a6ff;
|
||||
--theme-overlay: rgba(51, 166, 255, 0.24);
|
||||
--icon-color: #ffffff;
|
||||
--background-color: #181818;
|
||||
--background-color-dark: #141414;
|
||||
--surface-color: #242424;
|
||||
--surface-color-light: #444444;
|
||||
--surface-color-hover: #333333;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: 'JetBrains Mono', sans-serif;
|
||||
font-size: 14px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #242424;
|
||||
background-color: var(--surface-color);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.24);
|
||||
border: 6px solid #242424;
|
||||
border: 6px solid var(--surface-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -29,11 +38,14 @@ body {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #000000;
|
||||
background-color: var(--background-color);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import './index.scss';
|
||||
|
||||
import type {Project} from '@motion-canvas/core/lib';
|
||||
import {Player} from '@motion-canvas/core/lib/player';
|
||||
import {render} from 'preact';
|
||||
import {App} from './App';
|
||||
import {PlayerProvider, ProjectProvider} from './contexts';
|
||||
import {ComponentChild, render} from 'preact';
|
||||
import {Editor} from './Editor';
|
||||
import {Index, ProjectData} from './Index';
|
||||
import {InspectionProvider, PlayerProvider, ProjectProvider} from './contexts';
|
||||
|
||||
export default (project: Project) => {
|
||||
const app = document.createElement('main');
|
||||
function renderRoot(vnode: ComponentChild) {
|
||||
const root = document.createElement('main');
|
||||
document.body.appendChild(root);
|
||||
render(vnode, root);
|
||||
}
|
||||
|
||||
export function editor(project: Project) {
|
||||
const player = new Player(project);
|
||||
document.body.appendChild(app);
|
||||
document.title = `${project.name} | Motion Canvas`;
|
||||
render(
|
||||
renderRoot(
|
||||
<PlayerProvider player={player}>
|
||||
<ProjectProvider project={project}>
|
||||
<App />
|
||||
<InspectionProvider>
|
||||
<Editor />
|
||||
</InspectionProvider>
|
||||
</ProjectProvider>
|
||||
</PlayerProvider>,
|
||||
app,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function index(projects: ProjectData[]) {
|
||||
renderRoot(<Index projects={projects} />);
|
||||
}
|
||||
|
||||
11
packages/ui/types/main.d.ts
vendored
11
packages/ui/types/main.d.ts
vendored
@@ -1,3 +1,10 @@
|
||||
import type {Project} from '@motion-canvas/core/lib';
|
||||
declare const _default: (project: Project) => void;
|
||||
export default _default;
|
||||
|
||||
export function editor(project: Project): void;
|
||||
|
||||
export function index(
|
||||
projects: {
|
||||
name: string;
|
||||
url: string;
|
||||
}[],
|
||||
): void;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {defineConfig} from 'vite';
|
||||
import preact from '@preact/preset-vite';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
@@ -12,5 +13,17 @@ export default defineConfig({
|
||||
external: ['@motion-canvas/core'],
|
||||
},
|
||||
},
|
||||
plugins: [preact()],
|
||||
plugins: [
|
||||
preact(),
|
||||
{
|
||||
name: 'copy-files',
|
||||
async buildStart() {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: 'editor.html',
|
||||
source: await fs.promises.readFile('./editor.html'),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = (env, argv) => ({
|
||||
entry: './src/index.ts',
|
||||
mode: argv.mode ?? 'development',
|
||||
devtool: argv.mode === 'production' ? false : 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'ts-loader',
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
type: 'asset',
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{loader: 'style-loader'},
|
||||
{loader: 'css-loader', options: {modules: true}},
|
||||
{loader: 'sass-loader'},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
devServer: {
|
||||
port: 9001,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -6,15 +6,25 @@ import mime from 'mime-types';
|
||||
|
||||
export interface MotionCanvasPluginConfig {
|
||||
/**
|
||||
* The import path of the project file.
|
||||
* The import path of the project file or an array of paths.
|
||||
*
|
||||
* @remarks
|
||||
* The file must contain a default export exposing an instance of the
|
||||
* Each file must contain a default export exposing an instance of the
|
||||
* {@link Project} class.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* motionCanvas({
|
||||
* project: [
|
||||
* './src/firstProject.ts',
|
||||
* './src/secondProject.ts',
|
||||
* ]
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @default './src/project.ts'
|
||||
*/
|
||||
project?: string;
|
||||
project?: string | string[];
|
||||
/**
|
||||
* A directory path to which the animation will be rendered.
|
||||
*
|
||||
@@ -37,41 +47,57 @@ export interface MotionCanvasPluginConfig {
|
||||
* @default /\.(wav|mp3|ogg)$/
|
||||
*/
|
||||
bufferedAssets?: RegExp;
|
||||
editor?: {
|
||||
/**
|
||||
* The import path of the editor factory file.
|
||||
*
|
||||
* @remarks
|
||||
* The file must contain a default export exposing a factory function.
|
||||
* This function will be called with the project as its first argument.
|
||||
* Its task is to create the user interface.
|
||||
*
|
||||
* @default '\@motion-canvas/ui'
|
||||
*/
|
||||
factory?: string;
|
||||
/**
|
||||
* The import path of the editor styles.
|
||||
*
|
||||
* @default '\@motion-canvas/ui/dist/style.css'
|
||||
*/
|
||||
styles?: string;
|
||||
};
|
||||
/**
|
||||
* The import path of the editor package.
|
||||
*
|
||||
* @remarks
|
||||
* This path will be resolved using Node.js module resolution rules.
|
||||
* It should lead to a directory containing the following files:
|
||||
* - `editor.html` - The HTML template for the editor.
|
||||
* - `styles.css` - The editor styles.
|
||||
* - `main.js` - A module exporting necessary factory functions.
|
||||
*
|
||||
* `main.js` should export the following functions:
|
||||
* - `editor` - Receives the project as its first argument and creates the
|
||||
* user interface.
|
||||
* - `index` - Receives a list of all projects as its first argument and
|
||||
* creates the initial page for selecting a project.
|
||||
*
|
||||
* @default '\@motion-canvas/ui'
|
||||
*/
|
||||
editor?: string;
|
||||
}
|
||||
|
||||
interface ProjectData {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default ({
|
||||
project = './src/project.ts',
|
||||
output = './output',
|
||||
bufferedAssets = /\.(wav|mp3|ogg)$/,
|
||||
editor: {
|
||||
styles = '@motion-canvas/ui/dist/style.css',
|
||||
factory = '@motion-canvas/ui',
|
||||
} = {},
|
||||
editor = '@motion-canvas/ui',
|
||||
}: MotionCanvasPluginConfig = {}): Plugin => {
|
||||
const editorPath = path.dirname(require.resolve('@motion-canvas/ui'));
|
||||
const editorId = 'virtual:editor';
|
||||
const resolvedEditorId = '\0' + editorId;
|
||||
const editorPath = path.dirname(require.resolve(editor));
|
||||
const editorFile = fs.readFileSync(path.resolve(editorPath, 'editor.html'));
|
||||
const htmlParts = editorFile
|
||||
.toString()
|
||||
.replace('{{style}}', `/@fs/${path.resolve(editorPath, 'style.css')}`)
|
||||
.split('{{source}}');
|
||||
const createHtml = (src: string) => htmlParts[0] + src + htmlParts[1];
|
||||
|
||||
const resolvedEditorId = '\0virtual:editor';
|
||||
const timeStamps: Record<string, number> = {};
|
||||
const outputPath = path.resolve(output);
|
||||
const projects: ProjectData[] = [];
|
||||
const projectLookup: Record<string, ProjectData> = {};
|
||||
for (const url of typeof project === 'string' ? [project] : project) {
|
||||
const {name} = path.parse(url);
|
||||
const data = {name, url};
|
||||
projects.push(data);
|
||||
projectLookup[name] = data;
|
||||
}
|
||||
|
||||
let viteConfig: ResolvedConfig;
|
||||
|
||||
@@ -94,24 +120,37 @@ export default ({
|
||||
async configResolved(resolvedConfig) {
|
||||
viteConfig = resolvedConfig;
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id === editorId) {
|
||||
return resolvedEditorId;
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
if (id === resolvedEditorId) {
|
||||
return source(
|
||||
`import '${styles}';`,
|
||||
`import editor from '${factory}';`,
|
||||
`import project from '${project}?project';`,
|
||||
`editor(project);`,
|
||||
);
|
||||
}
|
||||
|
||||
const [base, query] = id.split('?');
|
||||
const {name, dir} = path.posix.parse(base);
|
||||
|
||||
if (id.startsWith(resolvedEditorId)) {
|
||||
if (projects.length === 1) {
|
||||
return source(
|
||||
`import {editor} from '${editor}';`,
|
||||
`import project from '${projects[0].url}?project';`,
|
||||
`editor(project);`,
|
||||
);
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const params = new URLSearchParams(query);
|
||||
const name = params.get('project');
|
||||
if (name && name in projectLookup) {
|
||||
return source(
|
||||
`import {editor} from '${editor}';`,
|
||||
`import project from '${projectLookup[name].url}?project';`,
|
||||
`editor(project);`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return source(
|
||||
`import {index} from '${editor}';`,
|
||||
`index(${JSON.stringify(projects)});`,
|
||||
);
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const params = new URLSearchParams(query);
|
||||
if (params.has('scene')) {
|
||||
@@ -142,7 +181,6 @@ export default ({
|
||||
if (params.has('project')) {
|
||||
const metaFile = `${name}.meta`;
|
||||
await createMeta(path.join(dir, metaFile));
|
||||
const projectFile = `${name}`;
|
||||
|
||||
return source(
|
||||
`import '@motion-canvas/core/lib/patches/Factory';`,
|
||||
@@ -150,8 +188,9 @@ export default ({
|
||||
`import '@motion-canvas/core/lib/patches/Shape';`,
|
||||
`import '@motion-canvas/core/lib/patches/Container';`,
|
||||
`import meta from './${metaFile}';`,
|
||||
`import project from './${projectFile}';`,
|
||||
`import project from './${name}';`,
|
||||
`project.meta = meta`,
|
||||
`project.name = '${name}'`,
|
||||
`export default project;`,
|
||||
);
|
||||
}
|
||||
@@ -242,16 +281,18 @@ export default ({
|
||||
const file = fs.readFileSync(
|
||||
path.resolve(viteConfig.root, req.url.slice(1)),
|
||||
);
|
||||
const stream = Readable.from(file);
|
||||
stream.on('end', console.log).pipe(res);
|
||||
Readable.from(file).pipe(res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/') {
|
||||
const stream = fs.createReadStream(
|
||||
path.resolve(editorPath, '../editor.html'),
|
||||
);
|
||||
stream.pipe(res);
|
||||
res.end(createHtml('/@id/__x00__virtual:editor'));
|
||||
return;
|
||||
}
|
||||
|
||||
const name = req.url?.slice(1);
|
||||
if (name && name in projectLookup) {
|
||||
res.end(createHtml(`/@id/__x00__virtual:editor?project=${name}`));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -268,10 +309,10 @@ export default ({
|
||||
});
|
||||
server.ws.on(
|
||||
'motion-canvas:export',
|
||||
async ({frame, mimeType, data}, client) => {
|
||||
async ({frame, mimeType, data, project}, client) => {
|
||||
const name = frame.toString().padStart(6, '0');
|
||||
const extension = mime.extension(mimeType);
|
||||
const file = path.join(outputPath, name + '.' + extension);
|
||||
const file = path.join(outputPath, project, name + '.' + extension);
|
||||
|
||||
const directory = path.dirname(file);
|
||||
if (!fs.existsSync(directory)) {
|
||||
@@ -293,9 +334,12 @@ export default ({
|
||||
jsxImportSource: '@motion-canvas/core/lib',
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: `${project}?project`,
|
||||
formats: ['es'],
|
||||
assetsDir: './',
|
||||
rollupOptions: {
|
||||
preserveEntrySignatures: 'strict',
|
||||
input: Object.fromEntries(
|
||||
projects.map(project => [project.name, project.url + '?project']),
|
||||
),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
||||
Reference in New Issue
Block a user