create-effect-motion 0.4.0 → 0.6.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 +5 -5
- package/dist/bin.js +0 -0
- package/dist/create.js +18 -18
- package/package.json +44 -44
- package/templates/default/AGENTS.md +22 -16
- package/templates/default/render.ts +14 -0
- package/templates/default/src/main.ts +2 -2
- package/templates/default/src/scenes/hello-world.ts +7 -5
- package/templates/default/studio.ts +16 -0
- package/templates/default/tsconfig.json +1 -1
- package/templates/default/motion.config.ts +0 -20
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Scaffold a new [effect-motion](https://github.com/julia-script/effect-motion) project:
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
|
|
7
|
-
# or: npm create effect-motion /
|
|
6
|
+
bun create effect-motion
|
|
7
|
+
# or: npm create effect-motion / pnpm create effect-motion / yarn create effect-motion
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
The prompts ask for a target directory, a package manager, and whether to set up [Biome](https://biomejs.dev) for linting/formatting. The generated project:
|
|
@@ -25,8 +25,8 @@ my-motion-project/
|
|
|
25
25
|
Answering `.` scaffolds into the current directory and names the project after it. A `git init` runs automatically unless the directory already sits inside a repository. Then:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
bun run studio # preview scenes with hot reload
|
|
29
|
+
bun run render # render targets from motion.config.ts to MP4
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Flags
|
|
@@ -34,7 +34,7 @@ pnpm render # render targets from motion.config.ts to MP4
|
|
|
34
34
|
Every prompt has a flag twin, so the scaffolder runs non-interactively in scripts and CI:
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
create-effect-motion my-app --pm
|
|
37
|
+
create-effect-motion my-app --pm bun --no-biome --no-install
|
|
38
38
|
create-effect-motion --yes # accept every default (-y)
|
|
39
39
|
```
|
|
40
40
|
|
package/dist/bin.js
CHANGED
|
File without changes
|
package/dist/create.js
CHANGED
|
@@ -8,24 +8,24 @@ import { MotionCliError, renderForTerminal } from "./MotionCliError.js";
|
|
|
8
8
|
import { VERSION } from "./pins.js";
|
|
9
9
|
import { ensureEmptyDir, resolveProjectDir, scaffoldProject, } from "./scaffold.js";
|
|
10
10
|
const DEFAULT_DIRECTORY = "my-motion-project";
|
|
11
|
-
const PACKAGE_MANAGERS = ["
|
|
12
|
-
/** The manager that invoked us (`
|
|
11
|
+
const PACKAGE_MANAGERS = ["bun", "pnpm", "npm", "yarn"];
|
|
12
|
+
/** The manager that invoked us (`bun create …` etc.), if detectable. */
|
|
13
13
|
const detectPackageManager = () => {
|
|
14
14
|
const agent = process.env.npm_config_user_agent ?? "";
|
|
15
15
|
return PACKAGE_MANAGERS.find((pm) => agent.startsWith(pm));
|
|
16
16
|
};
|
|
17
17
|
const flags = {
|
|
18
|
-
directory: Argument.
|
|
19
|
-
pm: Flag.optional(Flag.
|
|
20
|
-
biome: Flag.
|
|
21
|
-
noBiome: Flag.
|
|
22
|
-
noInstall: Flag.
|
|
23
|
-
yes: Flag.
|
|
18
|
+
directory: Argument.String("directory").pipe(Argument.withDescription('Target directory ("." scaffolds into the current directory)'), Argument.optional),
|
|
19
|
+
pm: Flag.optional(Flag.Literals("pm", PACKAGE_MANAGERS).pipe(Flag.withDescription("Package manager (skips the prompt)"))),
|
|
20
|
+
biome: Flag.Boolean("biome").pipe(Flag.withDefault(false), Flag.withDescription("Set up Biome for linting/formatting (skips the prompt)")),
|
|
21
|
+
noBiome: Flag.Boolean("no-biome").pipe(Flag.withDefault(false), Flag.withDescription("Skip the Biome setup (skips the prompt)")),
|
|
22
|
+
noInstall: Flag.Boolean("no-install").pipe(Flag.withDefault(false), Flag.withDescription("Skip dependency installation")),
|
|
23
|
+
yes: Flag.Boolean("yes").pipe(Flag.withDefault(false), Flag.withAlias("y"), Flag.withDescription("Accept the default answer for every prompt not answered by a flag")),
|
|
24
24
|
};
|
|
25
|
-
const promptDirectory = Prompt.
|
|
25
|
+
const promptDirectory = Prompt.run(Prompt.String({
|
|
26
26
|
message: 'Where should the project be created? ("." for the current directory)',
|
|
27
27
|
default: DEFAULT_DIRECTORY,
|
|
28
|
-
});
|
|
28
|
+
}));
|
|
29
29
|
const promptPackageManager = Effect.suspend(() => {
|
|
30
30
|
const detected = detectPackageManager();
|
|
31
31
|
// detected manager listed first so plain Enter picks it
|
|
@@ -33,15 +33,15 @@ const promptPackageManager = Effect.suspend(() => {
|
|
|
33
33
|
...(detected ? [detected] : []),
|
|
34
34
|
...PACKAGE_MANAGERS.filter((pm) => pm !== detected),
|
|
35
35
|
];
|
|
36
|
-
return Prompt.
|
|
36
|
+
return Prompt.run(Prompt.Select({
|
|
37
37
|
message: "Which package manager?",
|
|
38
38
|
choices: ordered.map((pm) => ({ title: pm, value: pm })),
|
|
39
|
-
});
|
|
39
|
+
}));
|
|
40
40
|
});
|
|
41
|
-
const promptBiome = Prompt.
|
|
41
|
+
const promptBiome = Prompt.run(Prompt.Confirm({
|
|
42
42
|
message: "Add Biome for linting/formatting?",
|
|
43
43
|
initial: true,
|
|
44
|
-
});
|
|
44
|
+
}));
|
|
45
45
|
const runInstall = (pm, dir) => Effect.gen(function* () {
|
|
46
46
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
47
47
|
const command = ChildProcess.make(pm, ["install"], {
|
|
@@ -100,7 +100,7 @@ const handler = (input) => Effect.gen(function* () {
|
|
|
100
100
|
yield* ensureEmptyDir(dir);
|
|
101
101
|
const pm = Option.getOrUndefined(input.pm) ??
|
|
102
102
|
(input.yes
|
|
103
|
-
? (detectPackageManager() ?? "
|
|
103
|
+
? (detectPackageManager() ?? "bun")
|
|
104
104
|
: yield* promptPackageManager);
|
|
105
105
|
// explicit flags win over --yes; --no-biome beats --biome if both are passed
|
|
106
106
|
const biome = input.noBiome
|
|
@@ -127,7 +127,7 @@ const handler = (input) => Effect.gen(function* () {
|
|
|
127
127
|
`${name} is ready.`,
|
|
128
128
|
dir === cwd ? "" : ` cd ${path.relative(cwd, dir)}`,
|
|
129
129
|
` ${pm === "npm" ? "npm run" : pm} studio # preview scenes with hot reload`,
|
|
130
|
-
` ${pm === "npm" ? "npm run" : pm} render # render
|
|
130
|
+
` ${pm === "npm" ? "npm run" : pm} render # execute render.ts (the render program)`,
|
|
131
131
|
]
|
|
132
132
|
.filter((line) => line !== "")
|
|
133
133
|
.join("\n"));
|
|
@@ -136,8 +136,8 @@ const handler = (input) => Effect.gen(function* () {
|
|
|
136
136
|
Effect.catchTag("QuitError", () => Effect.interrupt));
|
|
137
137
|
// registered globally so `--verbose` parses anywhere on the command line;
|
|
138
138
|
// the reporter reads argv directly because it sits outside handler context
|
|
139
|
-
const verboseFlag = GlobalFlag.
|
|
140
|
-
flag: Flag.
|
|
139
|
+
const verboseFlag = GlobalFlag.Setting("verbose")({
|
|
140
|
+
flag: Flag.Boolean("verbose").pipe(Flag.withDefault(false), Flag.withDescription("Print full error cause chains")),
|
|
141
141
|
});
|
|
142
142
|
export const rootCommand = Command.make("create-effect-motion", flags, handler).pipe(Command.withDescription("Scaffold a new effect-motion project"), Command.withGlobalFlags([verboseFlag]));
|
|
143
143
|
export const CLI_VERSION = VERSION;
|
package/package.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
2
|
+
"name": "create-effect-motion",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Scaffold a new effect-motion project — run with `bun create effect-motion` (or npm/pnpm/yarn create)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/julia-script/effect-motion.git",
|
|
10
|
+
"directory": "packages/create-effect-motion"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/julia-script/effect-motion#readme",
|
|
13
|
+
"bugs": "https://github.com/julia-script/effect-motion/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"effect",
|
|
16
|
+
"motion",
|
|
17
|
+
"create",
|
|
18
|
+
"scaffold",
|
|
19
|
+
"template",
|
|
20
|
+
"motion-graphics"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"create-effect-motion": "./dist/bin.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"templates"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.build.json",
|
|
31
|
+
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"check": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@effect/platform-node": "4.0.0-rc.115",
|
|
37
|
+
"effect": "4.0.0-rc.115"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@biomejs/biome": "^2.5.3",
|
|
41
|
+
"@types/node": "^26.1.1",
|
|
42
|
+
"typescript": "^7.0.2",
|
|
43
|
+
"vitest": "^4.1.10"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -4,14 +4,15 @@ This is an [effect-motion](https://github.com/julia-script/effect-motion) projec
|
|
|
4
4
|
|
|
5
5
|
## Layout and commands
|
|
6
6
|
|
|
7
|
-
- `src/scenes/*.ts` — one scene per module, each exporting `scene`.
|
|
7
|
+
- `src/scenes/*.ts` — one scene per module, each exporting `scene`. `Scene.make("Display Name", gen, meta?)` optionally names a scene for the studio picker.
|
|
8
8
|
- `src/main.ts` — the movie: an ordinary scene that sequences the others (`Scene.play` + `handle.finished`). Nothing is special about it.
|
|
9
|
-
- `
|
|
9
|
+
- `studio.ts` — the studio registration: `studioConfig({ scenes, layers })`. Record keys are unique identifiers; ONLY registered scenes appear in the picker, so add an import + entry for every new scene. Scenes with typed resources (fonts, images) need their loaders in `layers` — the file will not compile until every registered scene is covered.
|
|
10
|
+
- `render.ts` — an ordinary program default-exporting a `Video.render(...)` effect. More outputs are more calls; loader layers are provided here with `Effect.provide` (compile-checked). Knobs (paths, fps, seed) live in this code — there are no CLI flags.
|
|
10
11
|
- `src/assets/` — static files (images, fonts).
|
|
11
|
-
- `motion studio` — browser preview with hot reload
|
|
12
|
-
- `motion render [
|
|
12
|
+
- `motion studio [file]` — browser preview with hot reload of `studio.ts` (or the given entrypoint).
|
|
13
|
+
- `motion render [file]` — execute `render.ts` (or the given entrypoint) with the platform provided. `--verbose` prints full error cause chains. The same file runs standalone via `tsx render.ts` by piping through `NodeServices` from `@effect/platform-node`.
|
|
13
14
|
|
|
14
|
-
Verify a scene change by rendering it (`motion render
|
|
15
|
+
Verify a scene change by rendering it (`motion render`) or checking it in the running studio — not by reading code alone.
|
|
15
16
|
|
|
16
17
|
## Writing scenes
|
|
17
18
|
|
|
@@ -39,21 +40,26 @@ export const scene = Scene.make(function* () {
|
|
|
39
40
|
|
|
40
41
|
- **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
42
|
- Durations land exactly on target on the final frame; springs snap on settle. Don't add "fudge" frames.
|
|
42
|
-
- Scene coordinates are the `
|
|
43
|
+
- Scene coordinates are the scene's OWN comp config — `Scene.make(gen, { width, height, backgroundColor })` (this template: 1920×1080). `dpr` (a `Video.render` option) scales output pixels, not coordinates.
|
|
43
44
|
- 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
|
|
|
45
|
-
##
|
|
46
|
+
## Entrypoints
|
|
46
47
|
|
|
47
48
|
```ts
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
scene
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
// studio.ts — what the studio previews
|
|
50
|
+
export default studioConfig({
|
|
51
|
+
scenes: {
|
|
52
|
+
intro, // key = identifier, label = scene name ?? key
|
|
53
|
+
fancy: { scene: fancy, fps: 30 }, // per-entry player options
|
|
54
|
+
},
|
|
55
|
+
// layers: Layer.mergeAll(Font.layer(...), …) // REQUIRED once a scene declares resources
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// render.ts — what `motion render` executes
|
|
59
|
+
export default Effect.gen(function* () {
|
|
60
|
+
yield* Video.render(intro, "./output/intro.mp4", { settings: { frameRate: 60 } });
|
|
61
|
+
// yield* Video.render(intro, "./output/intro-hd.mp4", { dpr: 2 }); // more outputs = more calls
|
|
56
62
|
});
|
|
57
63
|
```
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
Render the same scene several times for variants (resolutions, dpr). An infinite scene (one that never finishes) must pass `frames` in its `Video.render` options, or rendering would never end.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Video } from "@effect-motion/export";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { scene as main } from "./src/main";
|
|
4
|
+
|
|
5
|
+
// An ordinary program: `motion render` executes this default export with
|
|
6
|
+
// the platform provided (it also runs standalone via `tsx render.ts` by
|
|
7
|
+
// piping through NodeServices from @effect/platform-node). More outputs
|
|
8
|
+
// are more Video.render calls; scenes with typed resources provide their
|
|
9
|
+
// loader layers right here with Effect.provide — checked at compile time.
|
|
10
|
+
export default Effect.gen(function* () {
|
|
11
|
+
yield* Video.render(main, "./output/main.mp4", {
|
|
12
|
+
settings: { frameRate: 60 },
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -2,8 +2,8 @@ import { Scene } from "effect-motion";
|
|
|
2
2
|
import { scene as helloWorld } from "./scenes/hello-world";
|
|
3
3
|
|
|
4
4
|
// The movie: an ordinary scene that sequences the scenes in src/scenes.
|
|
5
|
-
// Nothing is special about this file —
|
|
6
|
-
//
|
|
5
|
+
// Nothing is special about this file — studio.ts registers it and
|
|
6
|
+
// render.ts renders it like any other scene. Add scenes and chain them here.
|
|
7
7
|
export const scene = Scene.make(function* () {
|
|
8
8
|
const hello = yield* Scene.play(helloWorld);
|
|
9
9
|
yield* hello.finished;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { Color, Motion, Scene, Shapes } from "effect-motion";
|
|
2
2
|
|
|
3
3
|
// A scene is a generator: instantiate entities, then yield animations.
|
|
4
|
-
//
|
|
5
|
-
|
|
4
|
+
// The leading string is a DISPLAY name (the studio picker label); the
|
|
5
|
+
// studio.ts record key is its identifier. Preview with `motion studio`,
|
|
6
|
+
// render with `motion render`.
|
|
7
|
+
export const scene = Scene.make("Hello World", function* () {
|
|
6
8
|
const circle = yield* Scene.instantiate(Shapes.Circle, {
|
|
7
|
-
x:
|
|
8
|
-
y:
|
|
9
|
+
x: -660,
|
|
10
|
+
y: 0,
|
|
9
11
|
radius: 80,
|
|
10
12
|
fill: Color.hex("#7f5af0"),
|
|
11
13
|
});
|
|
12
14
|
|
|
13
|
-
yield* Motion.tweenTo(circle, { x:
|
|
15
|
+
yield* Motion.tweenTo(circle, { x: 660 }, "1200 millis", "easeInOutCubic");
|
|
14
16
|
yield* Motion.fadeTo(circle, 0, "400 millis");
|
|
15
17
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { studioConfig } from "@effect-motion/cli";
|
|
2
|
+
import { scene as main } from "./src/main";
|
|
3
|
+
import { scene as helloWorld } from "./src/scenes/hello-world";
|
|
4
|
+
|
|
5
|
+
// The studio's registration: keys are the picker's unique identifiers
|
|
6
|
+
// (labels come from Scene.make's display name when set). Register every
|
|
7
|
+
// scene you want to preview — `motion studio` serves exactly this record.
|
|
8
|
+
// When a scene declares typed resources (fonts, images), provide their
|
|
9
|
+
// loaders here as one `layers` (Layer.mergeAll(Font.layer(...), ...)) —
|
|
10
|
+
// the file will not compile until every registered scene is covered.
|
|
11
|
+
export default studioConfig({
|
|
12
|
+
scenes: {
|
|
13
|
+
"hello-world": helloWorld,
|
|
14
|
+
main,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -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
|
-
});
|