@effect-motion/cli 0.3.2 → 0.5.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 +3 -21
- package/dist/MotionCliError.d.ts +1 -1
- package/dist/StudioConfig.d.ts +71 -0
- package/dist/StudioConfig.js +76 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -4
- package/dist/commands/render.d.ts +2 -12
- package/dist/commands/render.js +44 -142
- package/dist/commands/studio.d.ts +1 -1
- package/dist/commands/studio.js +57 -45
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- package/package.json +5 -6
- package/studio-app/App.tsx +80 -158
- package/studio-app/StudioConfig.ts +5 -0
- package/studio-app/entry.ts +4 -0
- package/dist/Config.d.ts +0 -78
- package/dist/Config.js +0 -67
- package/dist/ConfigLoader.d.ts +0 -15
- package/dist/ConfigLoader.js +0 -58
- package/dist/commands/init.d.ts +0 -9
- package/dist/commands/init.js +0 -94
- package/dist/pins.d.ts +0 -23
- package/dist/pins.js +0 -23
- package/dist/scaffold.d.ts +0 -20
- package/dist/scaffold.js +0 -86
- package/studio-app/project.ts +0 -3
- package/templates/default/AGENTS.md +0 -59
- package/templates/default/_gitignore +0 -3
- package/templates/default/motion.config.ts +0 -20
- package/templates/default/src/assets/.gitkeep +0 -0
- package/templates/default/src/main.ts +0 -12
- package/templates/default/src/scenes/hello-world.ts +0 -15
- package/templates/default/tsconfig.json +0 -15
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
|
-
};
|
package/dist/scaffold.d.ts
DELETED
|
@@ -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
|
-
});
|
package/studio-app/project.ts
DELETED
|
@@ -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,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
|
-
}
|