@effect-motion/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,32 +1,14 @@
1
1
  # @effect-motion/cli
2
2
 
3
- Command line for [effect-motion](https://github.com/julia-script/effect-motion): scaffold a project, preview scenes in the browser, render videos — all driven by one `motion.config.ts`.
3
+ Command line for [effect-motion](https://github.com/julia-script/effect-motion): preview scenes in the browser and render videos — all driven by one `motion.config.ts`.
4
4
 
5
5
  ```sh
6
- npx @effect-motion/cli init # scaffold a project
6
+ pnpm create effect-motion # scaffold a project (npm/yarn/bun create work too)
7
7
  motion studio # preview scenes with hot reload
8
8
  motion render # render every target to MP4
9
9
  ```
10
10
 
11
- ## init
12
-
13
- `motion init [directory]` scaffolds a single-project workspace:
14
-
15
- ```
16
- my-project/
17
- ├─ src/
18
- │ ├─ scenes/hello-world.ts # a scene: a module exporting `scene`
19
- │ ├─ assets/ # static files
20
- │ └─ main.ts # the movie — an ordinary scene composing the others
21
- ├─ motion.config.ts # render targets
22
- ├─ AGENTS.md # authoring rules for AI coding agents
23
- ├─ package.json # EXACT pins of effect-motion + effect
24
- └─ tsconfig.json
25
- ```
26
-
27
- Answering `.` scaffolds into the current directory and names the project after it. The package-manager prompt defaults to whichever manager invoked the CLI; `--pm <pnpm|npm|yarn|bun>` and `--no-install` skip prompts/install.
28
-
29
- Dependency versions are pinned **exactly** — the `effect` pin is a determinism invariant (upgrading effect can change seeded random sequences), so upgrade both together, deliberately.
11
+ Scaffolding lives in the [`create-effect-motion`](https://www.npmjs.com/package/create-effect-motion) package — this CLI is installed as a devDependency of the projects it creates.
30
12
 
31
13
  ## motion.config.ts
32
14
 
package/dist/Config.d.ts CHANGED
@@ -6,20 +6,39 @@ import type * as Runner from "effect-motion/Runner";
6
6
  * ConfigLoader.ts.
7
7
  */
8
8
  /**
9
- * Per-target render settings: the Runner `Settings` subset an export can
10
- * honor, plus `dpr`. From the author's perspective dpr is a rendering
11
- * setting like width/height; the render command maps it to the export
12
- * package's supersampling option.
9
+ * Per-target playback settings: the Runner `Settings` subset an export can
10
+ * honor, plus `dpr` (mapped to the export package's supersampling option).
11
+ * Resolution and background are NOT here — they are the scene's own
12
+ * composition config, set in `Scene.make(gen, { width, height, backgroundColor })`.
13
13
  */
14
14
  export interface TargetSettings {
15
- readonly width?: number;
16
- readonly height?: number;
17
15
  readonly frameRate?: number;
18
16
  readonly seed?: Runner.Seed;
19
17
  readonly maxFrames?: number;
20
- readonly backgroundColor?: Runner.Settings["backgroundColor"];
21
18
  readonly dpr?: number;
22
19
  }
20
+ /**
21
+ * Studio preview options for a target's Player — how the scene PLAYS in
22
+ * `motion studio`, as opposed to `settings`, which is what the scene IS.
23
+ * `motion render` ignores them. Field meanings match `PlayerProps` in
24
+ * `@effect-motion/react`.
25
+ */
26
+ export interface PlayerOptions {
27
+ readonly autoPlay?: boolean;
28
+ /**
29
+ * Preview-only frame rate (the Player's `fps` prop) — wins over
30
+ * `settings.frameRate` in the studio (e.g. preview a heavy 60fps target
31
+ * at 30). The scene RUNS at this rate, so previewed frames are not the
32
+ * export's frames.
33
+ */
34
+ readonly fps?: number;
35
+ /** Initial repeat mode; the studio's repeat button toggles it after. */
36
+ readonly defaultRepeatMode?: boolean;
37
+ /** Declare a never-ending scene so the player windows its buffer. */
38
+ readonly isInfinite?: boolean;
39
+ readonly prebufferedFrames?: number;
40
+ readonly bufferCapacity?: number;
41
+ }
23
42
  /** v1 ships MP4 only; the field exists so more containers slot in later. */
24
43
  export type OutputFormat = "mp4";
25
44
  export interface MotionTarget {
@@ -28,6 +47,8 @@ export interface MotionTarget {
28
47
  /** Path to a module exporting `scene`, relative to the config file. */
29
48
  readonly scene: string;
30
49
  readonly settings?: TargetSettings;
50
+ /** Studio-only: how this target previews (autoplay, repeat, …). */
51
+ readonly player?: PlayerOptions;
31
52
  /** Output DIRECTORY (never a file), relative to the config file. */
32
53
  readonly output?: string;
33
54
  readonly format?: OutputFormat;
@@ -49,8 +70,6 @@ export declare const DEFAULT_FORMAT: OutputFormat;
49
70
  export declare const validateConfig: (value: unknown, configPath: string) => MotionConfig;
50
71
  /** Flag values a `motion render` invocation can lay over a target. */
51
72
  export interface RenderOverrides {
52
- readonly width?: number;
53
- readonly height?: number;
54
73
  readonly frameRate?: number;
55
74
  readonly dpr?: number;
56
75
  readonly seed?: Runner.Seed;
@@ -4,7 +4,7 @@
4
4
  * signature; adding a failure mode is a union-member addition handled
5
5
  * exhaustively at exactly one place (the top-level reporter in bin.ts).
6
6
  */
7
- export type MotionCliReason = "ConfigNotFound" | "ConfigInvalid" | "SceneLoadFailed" | "UnknownTarget" | "ScaffoldTargetNotEmpty" | "ScaffoldFailed" | "InstallFailed" | "RenderFailed" | "StudioFailed";
7
+ export type MotionCliReason = "ConfigNotFound" | "ConfigInvalid" | "SceneLoadFailed" | "UnknownTarget" | "RenderFailed" | "StudioFailed";
8
8
  declare const MotionCliError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
9
9
  readonly _tag: "MotionCliError";
10
10
  } & Readonly<A>;
package/dist/cli.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as Effect from "effect/Effect";
2
2
  import { CliError, Command } from "effect/unstable/cli";
3
3
  import { type MotionCliError } from "./MotionCliError.js";
4
4
  export declare const rootCommand: Command.Command<"motion", {}, {}, MotionCliError, never>;
5
- export declare const CLI_VERSION: "0.1.0";
5
+ export declare const CLI_VERSION: string;
6
6
  /**
7
7
  * The single exhaustive failure boundary (design D3a): MotionCliError
8
8
  * prints its message (cause chain under --verbose) and sets a non-zero
package/dist/cli.js CHANGED
@@ -1,18 +1,17 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import * as Console from "effect/Console";
2
3
  import * as Effect from "effect/Effect";
3
4
  import { CliError, Command, Flag, GlobalFlag } from "effect/unstable/cli";
4
- import { initCommand } from "./commands/init.js";
5
5
  import { renderCommand } from "./commands/render.js";
6
6
  import { studioCommand } from "./commands/studio.js";
7
7
  import { renderForTerminal } from "./MotionCliError.js";
8
- import { PINS } from "./pins.js";
9
8
  // registered globally so `--verbose` parses anywhere on the command line;
10
9
  // the reporter reads argv directly because it sits outside handler context
11
10
  const verboseFlag = GlobalFlag.setting("verbose")({
12
11
  flag: Flag.boolean("verbose").pipe(Flag.withDescription("Print full error cause chains")),
13
12
  });
14
- export const rootCommand = Command.make("motion").pipe(Command.withDescription("effect-motion: scaffold projects, preview scenes, render videos"), Command.withSubcommands([initCommand, studioCommand, renderCommand]), Command.withGlobalFlags([verboseFlag]));
15
- export const CLI_VERSION = PINS["@effect-motion/cli"];
13
+ export const rootCommand = Command.make("motion").pipe(Command.withDescription("effect-motion: preview scenes and render videos (scaffold new projects with `pnpm create effect-motion`)"), Command.withSubcommands([studioCommand, renderCommand]), Command.withGlobalFlags([verboseFlag]));
14
+ export const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
16
15
  /**
17
16
  * The single exhaustive failure boundary (design D3a): MotionCliError
18
17
  * prints its message (cause chain under --verbose) and sets a non-zero
@@ -5,8 +5,6 @@ import { Command } from "effect/unstable/cli";
5
5
  import { MotionCliError } from "../MotionCliError.js";
6
6
  export declare const renderCommand: Command.Command<"render", {
7
7
  readonly config: Option.Option<string>;
8
- readonly width: Option.Option<number>;
9
- readonly height: Option.Option<number>;
10
8
  readonly fps: Option.Option<number>;
11
9
  readonly dpr: Option.Option<number>;
12
10
  readonly seed: Option.Option<string>;
@@ -13,8 +13,6 @@ import { makeViteLoader } from "../ViteLoader.js";
13
13
  const opt = (o) => Option.getOrUndefined(o);
14
14
  const renderFlags = {
15
15
  config: Flag.optional(Flag.string("config").pipe(Flag.withDescription("Path to a motion.config.ts (tsc -p style)"))),
16
- width: Flag.optional(Flag.integer("width")),
17
- height: Flag.optional(Flag.integer("height")),
18
16
  fps: Flag.optional(Flag.integer("fps").pipe(Flag.withDescription("Frame rate override"))),
19
17
  dpr: Flag.optional(Flag.float("dpr").pipe(Flag.withDescription("Supersampling factor (output pixels = scene × dpr)"))),
20
18
  seed: Flag.optional(Flag.string("seed")),
@@ -26,8 +24,6 @@ const renderFlags = {
26
24
  };
27
25
  const overridesFrom = (input) => {
28
26
  const raw = {
29
- width: opt(input.width),
30
- height: opt(input.height),
31
27
  frameRate: opt(input.fps),
32
28
  dpr: opt(input.dpr),
33
29
  seed: opt(input.seed),
@@ -61,6 +61,11 @@ const handler = (input) => Effect.gen(function* () {
61
61
  resolve: {
62
62
  dedupe: ["react", "react-dom", "effect", "effect-motion"],
63
63
  },
64
+ // the studio app's JSX must not depend on the project's
65
+ // tsconfig — scaffolded projects set no `jsx`, and esbuild's
66
+ // fallback is the classic transform, which crashes with
67
+ // "React is not defined" before anything mounts
68
+ esbuild: { jsx: "automatic" },
64
69
  plugins: [
65
70
  {
66
71
  name: "motion:project-watch",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@effect-motion/cli",
3
- "version": "0.2.0",
4
- "description": "Command line for effect-motion: scaffold projects (init), preview scenes with hot reload (studio), and render videos from motion.config.ts (render)",
3
+ "version": "0.4.0",
4
+ "description": "Command line for effect-motion: preview scenes with hot reload (studio) and render videos from motion.config.ts (render). Scaffold new projects with `pnpm create effect-motion`",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -31,7 +31,6 @@
31
31
  },
32
32
  "files": [
33
33
  "dist",
34
- "templates",
35
34
  "studio-app"
36
35
  ],
37
36
  "publishConfig": {
@@ -41,8 +40,8 @@
41
40
  "@effect/platform-node": "4.0.0-beta.98",
42
41
  "effect": "4.0.0-beta.98",
43
42
  "vite": "^7.0.0",
44
- "@effect-motion/export": "^0.3.0",
45
- "effect-motion": "^0.3.0"
43
+ "@effect-motion/export": "^0.4.0",
44
+ "effect-motion": "^0.4.0"
46
45
  },
47
46
  "devDependencies": {
48
47
  "@types/node": "^26.1.1",
@@ -52,7 +51,7 @@
52
51
  "react-dom": "^19.2.0",
53
52
  "typescript": "^7.0.2",
54
53
  "vitest": "^4.1.10",
55
- "@effect-motion/react": "^0.3.0"
54
+ "@effect-motion/react": "^0.4.0"
56
55
  },
57
56
  "scripts": {
58
57
  "build": "tsc -p tsconfig.build.json",
@@ -9,9 +9,21 @@ type TargetLike = {
9
9
  readonly name?: string;
10
10
  readonly scene?: string;
11
11
  readonly settings?: Record<string, unknown>;
12
+ readonly player?: Record<string, unknown>;
12
13
  };
13
14
  type ConfigLike = { readonly targets?: ReadonlyArray<TargetLike> };
14
15
 
16
+ /** the PlayerProps subset a config's `player` block may set */
17
+ type PlayerOptions = Pick<
18
+ PlayerProps,
19
+ | "autoPlay"
20
+ | "defaultRepeatMode"
21
+ | "isInfinite"
22
+ | "prebufferedFrames"
23
+ | "bufferCapacity"
24
+ | "fps"
25
+ >;
26
+
15
27
  type SceneEntry = {
16
28
  /** picker identity: `target:<name>` for config targets, `file:<path>` otherwise */
17
29
  readonly key: string;
@@ -20,6 +32,8 @@ type SceneEntry = {
20
32
  readonly path: string;
21
33
  readonly registered: boolean;
22
34
  readonly settings: PlayerProps["settings"] | undefined;
35
+ /** studio preview options from the target's `player` block */
36
+ readonly player: PlayerOptions | undefined;
23
37
  readonly load: () => Promise<unknown>;
24
38
  };
25
39
 
@@ -59,6 +73,7 @@ const buildEntries = (config: ConfigLike | null): ReadonlyArray<SceneEntry> => {
59
73
  // a registered scene previews with its target settings so the
60
74
  // preview aspect matches the export
61
75
  settings: target.settings as PlayerProps["settings"],
76
+ player: target.player as PlayerOptions | undefined,
62
77
  load: globLoaders.get(path) ?? loadByAbsolutePath(path),
63
78
  });
64
79
  }
@@ -70,6 +85,7 @@ const buildEntries = (config: ConfigLike | null): ReadonlyArray<SceneEntry> => {
70
85
  path,
71
86
  registered: false,
72
87
  settings: undefined,
88
+ player: undefined,
73
89
  load,
74
90
  });
75
91
  }
@@ -174,17 +190,28 @@ export const App = () => {
174
190
  <pre>{state.message}</pre>
175
191
  </div>
176
192
  )}
177
- {state._tag === "ready" && (
178
- <Player
179
- key={state.key}
180
- scene={state.scene}
181
- autoPlay
182
- defaultRepeatMode
183
- {...(entry?.settings !== undefined
184
- ? { settings: entry.settings }
185
- : {})}
186
- />
187
- )}
193
+ {state._tag === "ready" &&
194
+ (() => {
195
+ // player.fps is a preview override of the scene's own rate,
196
+ // so it lands in settings.frameRate (the Player's one clock,
197
+ // which wins over its fps prop); everything else is a plain
198
+ // Player prop and wins over the studio defaults
199
+ const { fps, ...playerProps } = entry?.player ?? {};
200
+ const settings =
201
+ fps !== undefined
202
+ ? { ...entry?.settings, frameRate: fps }
203
+ : entry?.settings;
204
+ return (
205
+ <Player
206
+ key={state.key}
207
+ scene={state.scene}
208
+ autoPlay
209
+ defaultRepeatMode
210
+ {...(settings !== undefined ? { settings } : {})}
211
+ {...playerProps}
212
+ />
213
+ );
214
+ })()}
188
215
  {(state._tag === "idle" || state._tag === "loading") &&
189
216
  entries.length === 0 && (
190
217
  <p className="studio-empty">
@@ -1,9 +0,0 @@
1
- import * as Option from "effect/Option";
2
- import { Command, Prompt } from "effect/unstable/cli";
3
- import { ChildProcessSpawner } from "effect/unstable/process";
4
- import { MotionCliError } from "../MotionCliError.js";
5
- export declare const initCommand: Command.Command<"init", {
6
- readonly directory: Option.Option<string>;
7
- readonly pm: Option.Option<"bun" | "npm" | "pnpm" | "yarn">;
8
- readonly noInstall: boolean;
9
- }, {}, MotionCliError, ChildProcessSpawner.ChildProcessSpawner | Prompt.Environment>;
@@ -1,94 +0,0 @@
1
- import * as Console from "effect/Console";
2
- import * as Effect from "effect/Effect";
3
- import * as Option from "effect/Option";
4
- import { Path } from "effect/Path";
5
- import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
6
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
7
- import { MotionCliError } from "../MotionCliError.js";
8
- import { ensureEmptyDir, resolveProjectDir, scaffoldProject, } from "../scaffold.js";
9
- const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
10
- /** The manager that invoked us (`pnpm create …` etc.), if detectable. */
11
- const detectPackageManager = () => {
12
- const agent = process.env.npm_config_user_agent ?? "";
13
- return PACKAGE_MANAGERS.find((pm) => agent.startsWith(pm));
14
- };
15
- const initFlags = {
16
- directory: Argument.string("directory").pipe(Argument.withDescription('Target directory ("." scaffolds into the current directory)'), Argument.optional),
17
- pm: Flag.optional(Flag.choice("pm", PACKAGE_MANAGERS).pipe(Flag.withDescription("Package manager (skips the prompt)"))),
18
- noInstall: Flag.boolean("no-install").pipe(Flag.withDescription("Skip dependency installation")),
19
- };
20
- const promptDirectory = Prompt.text({
21
- message: 'Where should the project be created? ("." for the current directory)',
22
- default: "my-motion-project",
23
- });
24
- const promptPackageManager = Effect.suspend(() => {
25
- const detected = detectPackageManager();
26
- // detected manager listed first so plain Enter picks it
27
- const ordered = [
28
- ...(detected ? [detected] : []),
29
- ...PACKAGE_MANAGERS.filter((pm) => pm !== detected),
30
- ];
31
- return Prompt.select({
32
- message: "Which package manager?",
33
- choices: ordered.map((pm) => ({ title: pm, value: pm })),
34
- });
35
- });
36
- const runInstall = (pm, dir) => Effect.gen(function* () {
37
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
38
- const command = ChildProcess.make(pm, ["install"], {
39
- cwd: dir,
40
- stdin: "inherit",
41
- stdout: "inherit",
42
- stderr: "inherit",
43
- });
44
- yield* Effect.scoped(Effect.gen(function* () {
45
- const handle = yield* spawner.spawn(command);
46
- const code = yield* handle.exitCode;
47
- if (code !== 0) {
48
- return yield* new MotionCliError({
49
- reason: "InstallFailed",
50
- message: `${pm} install exited with code ${code} in ${dir} — run it manually`,
51
- cause: code,
52
- });
53
- }
54
- })).pipe(Effect.catchTag("PlatformError", (cause) => Effect.fail(new MotionCliError({
55
- reason: "InstallFailed",
56
- message: `could not run "${pm} install" in ${dir} — is ${pm} installed?`,
57
- cause,
58
- }))));
59
- });
60
- const handler = (input) => Effect.gen(function* () {
61
- const path = yield* Path;
62
- const cwd = process.cwd();
63
- const dirInput = Option.getOrUndefined(input.directory) ?? (yield* promptDirectory);
64
- const { dir, name } = resolveProjectDir(path, cwd, dirInput);
65
- yield* ensureEmptyDir(dir);
66
- const pm = Option.getOrUndefined(input.pm) ?? (yield* promptPackageManager);
67
- yield* scaffoldProject(dir, name);
68
- yield* Console.log(`Scaffolded ${name} in ${dir}`);
69
- if (input.noInstall) {
70
- yield* Console.log([
71
- "",
72
- "Next steps:",
73
- dir === cwd ? "" : ` cd ${path.relative(cwd, dir)}`,
74
- ` ${pm} install`,
75
- ` ${pm === "npm" ? "npm run" : pm} studio`,
76
- ]
77
- .filter((line) => line !== "")
78
- .join("\n"));
79
- return;
80
- }
81
- yield* runInstall(pm, dir);
82
- yield* Console.log([
83
- "",
84
- `${name} is ready.`,
85
- dir === cwd ? "" : ` cd ${path.relative(cwd, dir)}`,
86
- ` ${pm === "npm" ? "npm run" : pm} studio # preview scenes with hot reload`,
87
- ` ${pm === "npm" ? "npm run" : pm} render # render targets from motion.config.ts`,
88
- ]
89
- .filter((line) => line !== "")
90
- .join("\n"));
91
- }).pipe(
92
- // ctrl-c in a prompt is an interruption, not a failure
93
- Effect.catchTag("QuitError", () => Effect.interrupt));
94
- export const initCommand = Command.make("init", initFlags, handler).pipe(Command.withDescription("Scaffold a new effect-motion project"));
package/dist/pins.d.ts DELETED
@@ -1,23 +0,0 @@
1
- /**
2
- * Exact versions a scaffolded project is pinned to — the set this CLI
3
- * release was built and tested against. The effect pin is a determinism
4
- * invariant (upgrading effect can change seeded random sequences), so
5
- * scaffolds never use ranges or `latest`. Updated by the CLI's own release
6
- * process.
7
- */
8
- export declare const PINS: {
9
- readonly effect: "4.0.0-beta.98";
10
- readonly "effect-motion": "0.2.0";
11
- readonly "@effect-motion/react": "0.2.0";
12
- readonly "@effect-motion/export": "0.2.0";
13
- readonly "@effect-motion/cli": "0.1.0";
14
- };
15
- /** Non-determinism-critical companions; ranges are fine here. */
16
- export declare const COMPANIONS: {
17
- readonly react: "^19.2.0";
18
- readonly "react-dom": "^19.2.0";
19
- readonly typescript: "^7.0.2";
20
- readonly "@types/react": "^19.2.0";
21
- readonly "@types/react-dom": "^19.2.0";
22
- readonly "@types/node": "^26.1.1";
23
- };
package/dist/pins.js DELETED
@@ -1,23 +0,0 @@
1
- /**
2
- * Exact versions a scaffolded project is pinned to — the set this CLI
3
- * release was built and tested against. The effect pin is a determinism
4
- * invariant (upgrading effect can change seeded random sequences), so
5
- * scaffolds never use ranges or `latest`. Updated by the CLI's own release
6
- * process.
7
- */
8
- export const PINS = {
9
- effect: "4.0.0-beta.98",
10
- "effect-motion": "0.2.0",
11
- "@effect-motion/react": "0.2.0",
12
- "@effect-motion/export": "0.2.0",
13
- "@effect-motion/cli": "0.1.0",
14
- };
15
- /** Non-determinism-critical companions; ranges are fine here. */
16
- export const COMPANIONS = {
17
- react: "^19.2.0",
18
- "react-dom": "^19.2.0",
19
- typescript: "^7.0.2",
20
- "@types/react": "^19.2.0",
21
- "@types/react-dom": "^19.2.0",
22
- "@types/node": "^26.1.1",
23
- };
@@ -1,20 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import { FileSystem } from "effect/FileSystem";
3
- import { Path } from "effect/Path";
4
- import { MotionCliError } from "./MotionCliError.js";
5
- /**
6
- * The non-interactive core of `motion init`: everything except the prompts,
7
- * so tests can drive it directly. Copies templates/default into the target
8
- * directory and generates package.json from the pinned versions.
9
- */
10
- /** Directory of the shipped templates (dist/scaffold.js → ../templates). */
11
- export declare const templatesDir: (path: Path) => string;
12
- /** `.` means "here"; the project is named after the resolved directory. */
13
- export declare const resolveProjectDir: (path: Path, cwd: string, input: string) => {
14
- dir: string;
15
- name: string;
16
- };
17
- /** Non-empty means anything but dotfiles (a fresh `git init` is fine). */
18
- export declare const ensureEmptyDir: (dir: string) => Effect.Effect<undefined, MotionCliError, FileSystem>;
19
- /** Copy the template tree + write the generated package.json. */
20
- export declare const scaffoldProject: (dir: string, name: string) => Effect.Effect<void, MotionCliError, FileSystem | Path>;
package/dist/scaffold.js DELETED
@@ -1,86 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import { FileSystem } from "effect/FileSystem";
3
- import { Path } from "effect/Path";
4
- import { MotionCliError } from "./MotionCliError.js";
5
- import { COMPANIONS, PINS } from "./pins.js";
6
- /**
7
- * The non-interactive core of `motion init`: everything except the prompts,
8
- * so tests can drive it directly. Copies templates/default into the target
9
- * directory and generates package.json from the pinned versions.
10
- */
11
- /** Directory of the shipped templates (dist/scaffold.js → ../templates). */
12
- export const templatesDir = (path) => path.join(path.dirname(new URL(import.meta.url).pathname), "..", "templates", "default");
13
- /** `.` means "here"; the project is named after the resolved directory. */
14
- export const resolveProjectDir = (path, cwd, input) => {
15
- const dir = path.resolve(cwd, input);
16
- return { dir, name: path.basename(dir) };
17
- };
18
- /** Non-empty means anything but dotfiles (a fresh `git init` is fine). */
19
- export const ensureEmptyDir = (dir) => Effect.gen(function* () {
20
- const fs = yield* FileSystem;
21
- if (!(yield* fs.exists(dir)))
22
- return;
23
- const entries = yield* fs.readDirectory(dir);
24
- const meaningful = entries.filter((entry) => !entry.startsWith("."));
25
- if (meaningful.length > 0) {
26
- return yield* new MotionCliError({
27
- reason: "ScaffoldTargetNotEmpty",
28
- message: `${dir} is not empty (found ${meaningful.slice(0, 3).join(", ")}${meaningful.length > 3 ? ", …" : ""}) — choose an empty or new directory`,
29
- });
30
- }
31
- }).pipe(wrapFsError("ScaffoldFailed", `could not inspect ${dir}`));
32
- const wrapFsError = (reason, message) => (effect) => Effect.mapError(effect, (cause) => cause instanceof MotionCliError
33
- ? cause
34
- : new MotionCliError({ reason, message, cause }));
35
- const packageJson = (name) => `${JSON.stringify({
36
- name,
37
- private: true,
38
- version: "0.0.0",
39
- type: "module",
40
- scripts: {
41
- studio: "motion studio",
42
- render: "motion render",
43
- },
44
- dependencies: {
45
- "@effect-motion/export": PINS["@effect-motion/export"],
46
- "@effect-motion/react": PINS["@effect-motion/react"],
47
- effect: PINS.effect,
48
- "effect-motion": PINS["effect-motion"],
49
- react: COMPANIONS.react,
50
- "react-dom": COMPANIONS["react-dom"],
51
- },
52
- devDependencies: {
53
- "@effect-motion/cli": PINS["@effect-motion/cli"],
54
- "@types/node": COMPANIONS["@types/node"],
55
- "@types/react": COMPANIONS["@types/react"],
56
- "@types/react-dom": COMPANIONS["@types/react-dom"],
57
- typescript: COMPANIONS.typescript,
58
- },
59
- }, null, "\t")}\n`;
60
- /** Copy the template tree + write the generated package.json. */
61
- export const scaffoldProject = (dir, name) => Effect.gen(function* () {
62
- const fs = yield* FileSystem;
63
- const path = yield* Path;
64
- const templates = templatesDir(path);
65
- yield* fs.makeDirectory(dir, { recursive: true });
66
- yield* copyTree(fs, path, templates, dir);
67
- // npm mangles nested .gitignore/package.json files in published
68
- // tarballs, so both ship outside the template tree
69
- yield* fs.writeFileString(path.join(dir, "package.json"), packageJson(name));
70
- yield* fs.rename(path.join(dir, "_gitignore"), path.join(dir, ".gitignore"));
71
- }).pipe(wrapFsError("ScaffoldFailed", `could not scaffold ${dir}`));
72
- const copyTree = (fs, path, from, to) => Effect.gen(function* () {
73
- const entries = yield* fs.readDirectory(from);
74
- for (const entry of entries) {
75
- const src = path.join(from, entry);
76
- const dst = path.join(to, entry);
77
- const info = yield* fs.stat(src);
78
- if (info.type === "Directory") {
79
- yield* fs.makeDirectory(dst, { recursive: true });
80
- yield* copyTree(fs, path, src, dst);
81
- }
82
- else {
83
- yield* fs.copyFile(src, dst);
84
- }
85
- }
86
- });
@@ -1,59 +0,0 @@
1
- # Working in this project
2
-
3
- This is an [effect-motion](https://github.com/julia-script/effect-motion) project: motion graphics written as deterministic, frame-exact scenes in TypeScript, rendered to video. Read this before writing or editing scenes.
4
-
5
- ## Layout and commands
6
-
7
- - `src/scenes/*.ts` — one scene per module, each exporting `scene`. Any file here is previewable without registration.
8
- - `src/main.ts` — the movie: an ordinary scene that sequences the others (`Scene.play` + `handle.finished`). Nothing is special about it.
9
- - `motion.config.ts` — render targets. Output is always `<output>/<name>.mp4`; never write output paths by hand.
10
- - `src/assets/` — static files (images, fonts).
11
- - `motion studio` — browser preview with hot reload (scene picker lists config targets plus unregistered scenes).
12
- - `motion render [name...]` — render targets; `motion render ./src/scenes/foo.ts` renders one file with defaults. Flags beat config beat library defaults. `--verbose` prints full error cause chains.
13
-
14
- Verify a scene change by rendering it (`motion render <target>`) or checking it in the running studio — not by reading code alone.
15
-
16
- ## Writing scenes
17
-
18
- A scene is an Effect generator: instantiate entities, then yield animations.
19
-
20
- ```ts
21
- import { Color, Motion, Physics, Scene, Shapes } from "effect-motion";
22
-
23
- export const scene = Scene.make(function* () {
24
- const dot = yield* Scene.instantiate(Shapes.Circle, {
25
- x: 300, y: 540, radius: 80, fill: Color.hex("#7f5af0"),
26
- });
27
- yield* Motion.tweenTo(dot, { x: 1620 }, "1200 millis", "easeInOutCubic");
28
- yield* Physics.springTo(dot, { y: 300 }, Physics.springs.wobbly);
29
- });
30
- ```
31
-
32
- - **Animators come in pairs**: `verb(instance, from, to, …)` (explicit origin) and `verbTo(instance, to, …)` (origin read from the instance). Prefer the `To` form unless you need a fixed origin.
33
- - **Prefer semantic helpers** (`Motion.moveTo`, `Motion.fadeTo`, `Physics.springTo`) over raw `tweenTo` when one exists — they carry per-entity meaning (moving a Line translates both endpoints; moving a Group carries its subtree). Use `tweenTo` for fields without a trait (`radius`, `width`, custom fields).
34
- - **Springs have no duration** — length emerges from the simulation (presets in `Physics.springs`). Springy motion on raw fields uses elastic/bounce *easings*, not physics.
35
- - **Every animator is a dual**: `Motion.tweenTo(dot, …)` or `dot.pipe(Motion.tweenTo(…))` — both are idiomatic.
36
- - **Composition**: sequence by yielding one animation after another; `Scene.all([...])` runs them together; `Scene.chain`/`Scene.stagger` sequence with schedules; `Scene.fork` starts a branch you can join later; `Scene.play(otherScene)` mounts a whole scene (await `handle.finished`). `Scene.finish` marks a scene's semantic end — anything after it is a tail that keeps playing without being waited on.
37
-
38
- ## Determinism rules (non-negotiable)
39
-
40
- - **Never** use `Math.random()`, `Date.now()`, or any wall-clock/OS state in a scene — every run must be byte-identical. Use the provided seeded random (`Effect.random`, seeded from `settings.seed`).
41
- - Durations land exactly on target on the final frame; springs snap on settle. Don't add "fudge" frames.
42
- - Scene coordinates are the `settings.width`/`height` of the target that renders them (this template: 1920×1080). `dpr` scales output pixels, not coordinates.
43
- - The `effect` dependency is pinned **exactly** — upgrading it can change seeded-random sequences. Never bump it casually; upgrade `effect` and `effect-motion` together, deliberately.
44
-
45
- ## Config
46
-
47
- ```ts
48
- export default defineConfig({
49
- targets: [{
50
- name: "intro", // unique — doubles as the output basename
51
- scene: "./src/scenes/intro.ts",
52
- settings: { width: 1920, height: 1080, frameRate: 60, dpr: 1 },
53
- output: "./output", // a DIRECTORY; file name is derived
54
- // frames: 600 // REQUIRED if the scene is infinite
55
- }],
56
- });
57
- ```
58
-
59
- A scene used by several targets renders once per target (e.g. different resolutions). An infinite scene (one that never finishes) must set `frames`, or rendering would never end.
@@ -1,3 +0,0 @@
1
- node_modules/
2
- output/
3
- .motion/
@@ -1,20 +0,0 @@
1
- import { defineConfig } from "@effect-motion/cli";
2
-
3
- // Each target is one rendered video: <output>/<name>.mp4
4
- // `motion render` renders all of them; `motion render <name>` picks one.
5
- export default defineConfig({
6
- targets: [
7
- {
8
- name: "hello-world",
9
- scene: "./src/scenes/hello-world.ts",
10
- settings: { width: 1920, height: 1080, frameRate: 60 },
11
- output: "./output",
12
- },
13
- {
14
- name: "main",
15
- scene: "./src/main.ts",
16
- settings: { width: 1920, height: 1080, frameRate: 60 },
17
- output: "./output",
18
- },
19
- ],
20
- });
File without changes
@@ -1,12 +0,0 @@
1
- import { Scene } from "effect-motion";
2
- import { scene as helloWorld } from "./scenes/hello-world";
3
-
4
- // The movie: an ordinary scene that sequences the scenes in src/scenes.
5
- // Nothing is special about this file — it is one more target in
6
- // motion.config.ts. Add scenes and chain them here.
7
- export const scene = Scene.make(function* () {
8
- const hello = yield* Scene.play(helloWorld);
9
- yield* hello.finished;
10
- // const next = yield* Scene.play(anotherScene);
11
- // yield* next.finished;
12
- });
@@ -1,15 +0,0 @@
1
- import { Color, Motion, Scene, Shapes } from "effect-motion";
2
-
3
- // A scene is a generator: instantiate entities, then yield animations.
4
- // Preview it with `motion studio`, render it with `motion render`.
5
- export const scene = Scene.make(function* () {
6
- const circle = yield* Scene.instantiate(Shapes.Circle, {
7
- x: 300,
8
- y: 540,
9
- radius: 80,
10
- fill: Color.hex("#7f5af0"),
11
- });
12
-
13
- yield* Motion.tweenTo(circle, { x: 1620 }, "1200 millis", "easeInOutCubic");
14
- yield* Motion.fadeTo(circle, 0, "400 millis");
15
- });
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "exactOptionalPropertyTypes": true,
8
- "noUncheckedIndexedAccess": true,
9
- "skipLibCheck": true,
10
- "noEmit": true,
11
- "lib": ["ES2022"],
12
- "types": ["node"]
13
- },
14
- "include": ["src", "motion.config.ts"]
15
- }