Skip to content

Commit

Permalink
feat: headless rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
aarthificial committed May 17, 2024
1 parent 9b01eb5 commit 6622c65
Show file tree
Hide file tree
Showing 15 changed files with 1,307 additions and 410 deletions.
1,103 changes: 758 additions & 345 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions packages/core/src/app/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {Scene} from '../scenes';
import {ReadOnlyTimeEvents} from '../scenes/timeEvents';
import {clampRemap} from '../tweening';
import {Vector2} from '../types';
import {Semaphore} from '../utils';
import {Semaphore, errorToLog} from '../utils';
import type {Exporter} from './Exporter';
import {PlaybackManager, PlaybackState} from './PlaybackManager';
import {PlaybackStatus} from './PlaybackStatus';
Expand All @@ -20,6 +20,7 @@ export interface RendererSettings extends StageSettings {
name: string;
options: unknown;
};
variables?: Record<string, unknown>;
}

export enum RendererState {
Expand Down Expand Up @@ -107,7 +108,7 @@ export class Renderer {
this.abortController = new AbortController();
result = await this.run(settings, this.abortController.signal);
} catch (e: any) {
this.project.logger.error(e);
this.project.logger.error(errorToLog(e));
result = RendererResult.Error;
if (this.exporter) {
try {
Expand Down Expand Up @@ -268,7 +269,10 @@ export class Renderer {
resolutionScale: settings.resolutionScale,
});
scene.meta.set(description.meta.get());
scene.variables.updateSignals(this.project.variables ?? {});
scene.variables.updateSignals({
...(this.project.variables ?? {}),
...(settings.variables ?? {}),
});
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/plugin/Plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface Plugin {
*
* @param settings - The project settings.
*/
settings?(settings: ProjectSettings): ProjectSettings | void;
settings?(settings: ProjectSettings): Partial<ProjectSettings> | void;

/**
* Receive the project instance right after it is initialized.
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@motion-canvas/2d": "*",
"@motion-canvas/core": "*",
"jest-image-snapshot": "^6.2.0",
"puppeteer": "^21.5.2",
"puppeteer": "^22.9.0",
"vitest": "^0.34.6"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/ffmpeg/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default (): Plugin => {
name: 'motion-canvas/ffmpeg',
[PLUGIN_OPTIONS]: {
entryPoint: '@motion-canvas/ffmpeg/lib/client',
async config(value) {
async configResolved(value) {
config = value;
},
},
Expand Down
25 changes: 25 additions & 0 deletions packages/renderer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@motion-canvas/renderer",
"version": "3.16.0",
"type": "module",
"main": "lib/index.js",
"bin": {
"motion-canvas-render": "lib/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc -w"
},
"dependencies": {
"@motion-canvas/vite-plugin": "^3.15.1",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"kleur": "^4.1.5",
"ora": "^8.0.1",
"puppeteer": "^22.9.0"
},
"devDependencies": {
"@motion-canvas/core": "^3.16.0",
"@types/cli-progress": "^3.11.5"
}
}
113 changes: 113 additions & 0 deletions packages/renderer/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env node

import type {RendererSettings} from '@motion-canvas/core';
import {program} from 'commander';
import * as path from 'path';
import * as process from 'process';
import {
parseJSONFile,
parseJSONString,
parseRecord,
parseTuple,
} from './parsing.js';
import {render} from './puppeteer.js';

program
.name('motion-canvas-render')
.description(
'CLI tool for rendering Motion Canvas projects in headless mode.',
);

program
.argument('<path>', 'path to the project')
.option('-p, --project <name>', 'name of the project to render')
.option('-o, --output <path>', 'path to the output directory')
.option('--product <name>', 'browser to use', 'firefox')
.option('--debug', 'enable debug mode')
.option('--background <color>', 'background color')
.option('--range <from>:<to>', 'time range in seconds', parseTuple)
.option(
'--size <width>:<height>',
'size of the project in pixels',
parseTuple,
)
.option('--fps <number>', 'frames per second', parseInt)
.option('--scale <number>', 'resolution scale', parseFloat)
.option('--exporter <name>', 'name of the exporter')
.option(
'--exportOptions <key=value...>',
'key-value pairs of export options',
parseRecord,
)
.option(
'--variables <key=value...>',
'key-value paris of project variables',
parseRecord,
)
.option(
'--variablesJSON <json>',
'JSON string with project variables',
parseJSONString,
)
.option(
'--variablesFile <json>',
'Path to a JSON file with project variables',
parseJSONFile,
)
.action((projectPath, options) => {
const settings: Partial<RendererSettings> = {};
if (options.background) {
settings.background = options.background;
}
if (options.range) {
settings.range = options.range;
}
if (options.size) {
settings.size = options.size;
}
if (options.fps) {
settings.fps = options.fps;
}
if (options.scale) {
settings.resolutionScale = options.scale;
}

settings.exporter = {
name: options.exporter ?? '',
options: options.exportOptions ?? {},
};

let variables: Record<string, string> = {};
if (options.variables) {
variables = options.variables;
}
if (options.variablesJSON) {
variables = {
...variables,
...options.variablesJSON,
};
}
if (options.variablesFile) {
variables = {
...variables,
...options.variablesFile,
};
}
settings.variables = variables;

const output = options.output
? path.resolve(process.cwd(), options.output)
: undefined;
process.chdir(path.resolve(process.cwd(), projectPath));
return render(
{
output,
project: options.project,
product: options.product,
debug: options.debug,
},
settings,
);
});

program.parse();
74 changes: 74 additions & 0 deletions packages/renderer/src/parsing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {InvalidOptionArgumentError} from 'commander';
import * as fs from 'fs';
import * as path from 'path';
import * as process from 'process';

export function parseTuple(value: string) {
const [from, to] = value.split(':');
if (to === undefined) {
throw new InvalidOptionArgumentError(`Missing colon.`);
}

const parsedFrom = parseFloat(from);
if (isNaN(parsedFrom)) {
throw new InvalidOptionArgumentError(`"${from}" is not a number.`);
}

const parsedTo = parseFloat(to);
if (isNaN(parsedTo)) {
throw new InvalidOptionArgumentError(`"${to}" is not a number.`);
}

return [parsedFrom, parsedTo];
}

export function parseRecord(
value: string,
previous: Record<string, string> = {},
) {
const [key, val] = value.split('=');
if (val === undefined) {
throw new InvalidOptionArgumentError(`Missing equals sign.`);
}

return {
...previous,
[key]: val,
};
}

export function parseJSONString(
value: string,
previous: Record<string, string> = {},
) {
let parsed: Record<string, string>;
try {
parsed = JSON.parse(value);
} catch (error) {
throw new InvalidOptionArgumentError(error as string);
}

if (!parsed && typeof parsed !== 'object') {
throw new InvalidOptionArgumentError('Not a valid JSON object.');
}

return {
...previous,
...parsed,
};
}

export function parseJSONFile(
file: string,
previous: Record<string, string> = {},
) {
const filePath = path.resolve(process.cwd(), file);
let value: string;
try {
value = fs.readFileSync(filePath, 'utf-8');
} catch (error) {
throw new InvalidOptionArgumentError(error as string);
}

return parseJSONString(value, previous);
}
Loading

0 comments on commit 6622c65

Please sign in to comment.