retiam 1.0.0 → 1.0.1
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/bin/cli.js +213 -209
- package/bin/export.js +322 -322
- package/bin/init.js +425 -425
- package/bin/lint.js +354 -354
- package/bin/render-config.js +87 -0
- package/bin/render.js +517 -600
- package/bin/snapshot.js +231 -231
- package/dist/{UTIF-pBEih-vH.js → UTIF-CuM_xgsi.js} +1 -1
- package/dist/assets/roboto-msdf.json +3642 -3642
- package/dist/{index-CnM0BhUR.js → index-C7UzgTiw.js} +4728 -4728
- package/dist/{main-Bw2LTrnR.js → main-CCBHkqdA.js} +1 -1
- package/dist/retiam.es.js +1 -1
- package/package.json +106 -116
- package/readme.md +155 -143
package/bin/init.js
CHANGED
|
@@ -1,425 +1,425 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// =========================================================================
|
|
4
|
-
// File: bin/init.js - Retiam Project Starter & Skeleton Generator
|
|
5
|
-
// =========================================================================
|
|
6
|
-
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import readline from 'readline/promises';
|
|
10
|
-
import { spawnSync } from 'child_process';
|
|
11
|
-
|
|
12
|
-
const args = process.argv.slice(2);
|
|
13
|
-
|
|
14
|
-
// Check for --help
|
|
15
|
-
const isHelp = args.includes('--help') || args.includes('-h') || args.includes('help');
|
|
16
|
-
if (isHelp) {
|
|
17
|
-
console.log(`
|
|
18
|
-
🚀 Retiam WebGPU Engine - Project Initializer
|
|
19
|
-
|
|
20
|
-
Usage:
|
|
21
|
-
npx retiam init [directory] [options]
|
|
22
|
-
npx retiam create [directory] [options]
|
|
23
|
-
|
|
24
|
-
Arguments:
|
|
25
|
-
[directory] Destination directory (default: ./retiam-animation)
|
|
26
|
-
|
|
27
|
-
Options:
|
|
28
|
-
-n, --name <name> Project name for package.json (default: directory name)
|
|
29
|
-
--title <text> Hero text to display in Hello World scene (default: Hello World)
|
|
30
|
-
--pm <manager> Package manager: npm, pnpm, yarn, bun (default: auto-detected or npm)
|
|
31
|
-
-f, --force Overwrite destination directory if files already exist
|
|
32
|
-
-y, --yes Skip interactive prompts and use defaults
|
|
33
|
-
--install Automatically install dependencies after generation
|
|
34
|
-
--git Initialize a Git repository (git init)
|
|
35
|
-
--json Output structured machine-readable JSON for agents and CI
|
|
36
|
-
--help, -h Show this help message and exit
|
|
37
|
-
|
|
38
|
-
Examples:
|
|
39
|
-
npx retiam init
|
|
40
|
-
npx retiam init my-animation
|
|
41
|
-
npx retiam create intro-scene --title "Quantum Mechanics" --pm pnpm
|
|
42
|
-
npx retiam init my-project -y --install
|
|
43
|
-
npx retiam init agent-project --json --yes
|
|
44
|
-
`);
|
|
45
|
-
process.exit(0);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Auto-detect package manager from npm_config_user_agent
|
|
49
|
-
function detectPackageManager() {
|
|
50
|
-
const userAgent = process.env.npm_config_user_agent || '';
|
|
51
|
-
if (userAgent.startsWith('pnpm')) return 'pnpm';
|
|
52
|
-
if (userAgent.startsWith('yarn')) return 'yarn';
|
|
53
|
-
if (userAgent.startsWith('bun')) return 'bun';
|
|
54
|
-
return 'npm';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Parse CLI flags
|
|
58
|
-
let targetDir = null;
|
|
59
|
-
let projectName = null;
|
|
60
|
-
let heroTitle = 'Hello World';
|
|
61
|
-
let packageManager = detectPackageManager();
|
|
62
|
-
let force = false;
|
|
63
|
-
let nonInteractive = false;
|
|
64
|
-
let runInstall = false;
|
|
65
|
-
let initGit = false;
|
|
66
|
-
let jsonOutput = false;
|
|
67
|
-
|
|
68
|
-
for (let i = 0; i < args.length; i++) {
|
|
69
|
-
const arg = args[i];
|
|
70
|
-
if (arg === '--json') {
|
|
71
|
-
jsonOutput = true;
|
|
72
|
-
nonInteractive = true;
|
|
73
|
-
} else if (arg === '-y' || arg === '--yes') {
|
|
74
|
-
nonInteractive = true;
|
|
75
|
-
} else if (arg === '-f' || arg === '--force') {
|
|
76
|
-
force = true;
|
|
77
|
-
} else if (arg === '--install') {
|
|
78
|
-
runInstall = true;
|
|
79
|
-
} else if (arg === '--git') {
|
|
80
|
-
initGit = true;
|
|
81
|
-
} else if (arg === '-n' || arg === '--name') {
|
|
82
|
-
projectName = args[++i];
|
|
83
|
-
} else if (arg === '--title') {
|
|
84
|
-
heroTitle = args[++i] || 'Hello World';
|
|
85
|
-
} else if (arg === '--pm') {
|
|
86
|
-
packageManager = args[++i] || packageManager;
|
|
87
|
-
} else if (!arg.startsWith('-') && !targetDir) {
|
|
88
|
-
targetDir = arg;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function main() {
|
|
93
|
-
// Interactive setup if TTY and not in --yes / --json mode
|
|
94
|
-
const isTTY = process.stdin.isTTY && !nonInteractive;
|
|
95
|
-
|
|
96
|
-
if (isTTY) {
|
|
97
|
-
const rl = readline.createInterface({
|
|
98
|
-
input: process.stdin,
|
|
99
|
-
output: process.stdout
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
console.log(`\n✨ Welcome to Retiam - WebGPU Animation Project Generator\n`);
|
|
104
|
-
|
|
105
|
-
if (!targetDir) {
|
|
106
|
-
const inputDir = await rl.question(`📁 Destination directory (./retiam-animation): `);
|
|
107
|
-
targetDir = inputDir.trim() || 'retiam-animation';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const defaultName = path.basename(path.resolve(process.cwd(), targetDir))
|
|
111
|
-
.toLowerCase()
|
|
112
|
-
.replace(/[^a-z0-9_-]/g, '-');
|
|
113
|
-
|
|
114
|
-
if (!projectName) {
|
|
115
|
-
const inputName = await rl.question(`📦 Project name (${defaultName}): `);
|
|
116
|
-
projectName = inputName.trim() || defaultName;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const inputTitle = await rl.question(`🎨 Starter animation text (${heroTitle}): `);
|
|
120
|
-
if (inputTitle.trim()) {
|
|
121
|
-
heroTitle = inputTitle.trim();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const inputPm = await rl.question(`⚡ Package manager [npm/pnpm/yarn/bun] (${packageManager}): `);
|
|
125
|
-
if (inputPm.trim()) {
|
|
126
|
-
packageManager = inputPm.trim().toLowerCase();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const inputInstall = await rl.question(`📥 Run '${packageManager} install' now? [y/N]: `);
|
|
130
|
-
if (inputInstall.trim().toLowerCase().startsWith('y')) {
|
|
131
|
-
runInstall = true;
|
|
132
|
-
}
|
|
133
|
-
} finally {
|
|
134
|
-
rl.close();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!targetDir) {
|
|
139
|
-
targetDir = 'retiam-animation';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const resolvedDir = path.resolve(process.cwd(), targetDir);
|
|
143
|
-
const resolvedName = projectName || path.basename(resolvedDir)
|
|
144
|
-
.toLowerCase()
|
|
145
|
-
.replace(/[^a-z0-9_-]/g, '-') || 'retiam-animation';
|
|
146
|
-
|
|
147
|
-
// Conflict check
|
|
148
|
-
if (fs.existsSync(resolvedDir)) {
|
|
149
|
-
const existingFiles = fs.readdirSync(resolvedDir).filter(f => !f.startsWith('.'));
|
|
150
|
-
if (existingFiles.length > 0 && !force) {
|
|
151
|
-
if (jsonOutput) {
|
|
152
|
-
console.log(JSON.stringify({
|
|
153
|
-
status: 'error',
|
|
154
|
-
message: `Target directory '${targetDir}' already exists and is not empty. Use -f or --force to overwrite.`
|
|
155
|
-
}));
|
|
156
|
-
} else {
|
|
157
|
-
console.error(`\n❌ Error: Directory '${targetDir}' is not empty (${existingFiles.length} files found).`);
|
|
158
|
-
console.error(` Use -f or --force to proceed anyway.\n`);
|
|
159
|
-
}
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Ensure directory structure
|
|
165
|
-
const srcDir = path.join(resolvedDir, 'src');
|
|
166
|
-
const scenesDir = path.join(srcDir, 'scenes');
|
|
167
|
-
fs.mkdirSync(scenesDir, { recursive: true });
|
|
168
|
-
|
|
169
|
-
// 1. package.json
|
|
170
|
-
const packageJson = {
|
|
171
|
-
name: resolvedName,
|
|
172
|
-
version: '0.1.0',
|
|
173
|
-
private: true,
|
|
174
|
-
type: 'module',
|
|
175
|
-
scripts: {
|
|
176
|
-
dev: 'vite',
|
|
177
|
-
build: 'tsc && vite build',
|
|
178
|
-
preview: 'vite preview',
|
|
179
|
-
render: 'retiam render src/scenes/HelloWorld.ts'
|
|
180
|
-
},
|
|
181
|
-
dependencies: {
|
|
182
|
-
retiam: '^1.0.0'
|
|
183
|
-
},
|
|
184
|
-
devDependencies: {
|
|
185
|
-
'@webgpu/types': '^0.1.31',
|
|
186
|
-
typescript: '^5.5.0',
|
|
187
|
-
vite: '^5.4.0'
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// 2. tsconfig.json
|
|
192
|
-
const tsconfigJson = {
|
|
193
|
-
compilerOptions: {
|
|
194
|
-
target: 'ES2022',
|
|
195
|
-
module: 'ESNext',
|
|
196
|
-
moduleResolution: 'bundler',
|
|
197
|
-
lib: ['ES2022', 'DOM', 'DOM.Iterable'],
|
|
198
|
-
types: ['@webgpu/types', 'vite/client'],
|
|
199
|
-
strict: true,
|
|
200
|
-
esModuleInterop: true,
|
|
201
|
-
skipLibCheck: true,
|
|
202
|
-
isolatedModules: true
|
|
203
|
-
},
|
|
204
|
-
include: ['src']
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// 3. vite.config.ts
|
|
208
|
-
const viteConfig = `import { defineConfig } from 'vite';
|
|
209
|
-
|
|
210
|
-
export default defineConfig({
|
|
211
|
-
optimizeDeps: {
|
|
212
|
-
esbuildOptions: {
|
|
213
|
-
target: 'es2022',
|
|
214
|
-
supported: {
|
|
215
|
-
'top-level-await': true
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
esbuild: {
|
|
220
|
-
target: 'es2022'
|
|
221
|
-
},
|
|
222
|
-
server: {
|
|
223
|
-
port: 3000,
|
|
224
|
-
open: true
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
`;
|
|
228
|
-
|
|
229
|
-
// 4. index.html
|
|
230
|
-
const indexHtml = `<!DOCTYPE html>
|
|
231
|
-
<html lang="en">
|
|
232
|
-
<head>
|
|
233
|
-
<meta charset="UTF-8" />
|
|
234
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
235
|
-
<title>${resolvedName} - Retiam WebGPU Animation</title>
|
|
236
|
-
<style>
|
|
237
|
-
* {
|
|
238
|
-
box-sizing: border-box;
|
|
239
|
-
}
|
|
240
|
-
body {
|
|
241
|
-
margin: 0;
|
|
242
|
-
overflow: hidden;
|
|
243
|
-
background-color: #080C14;
|
|
244
|
-
width: 100vw;
|
|
245
|
-
height: 100vh;
|
|
246
|
-
}
|
|
247
|
-
canvas {
|
|
248
|
-
display: block;
|
|
249
|
-
width: 100vw;
|
|
250
|
-
height: 100vh;
|
|
251
|
-
}
|
|
252
|
-
</style>
|
|
253
|
-
</head>
|
|
254
|
-
<body>
|
|
255
|
-
<canvas id="canvas"></canvas>
|
|
256
|
-
<script type="module" src="/src/main.ts"></script>
|
|
257
|
-
</body>
|
|
258
|
-
</html>
|
|
259
|
-
`;
|
|
260
|
-
|
|
261
|
-
// 5. src/main.ts
|
|
262
|
-
const mainTs = `import HelloWorldScene from './scenes/HelloWorld';
|
|
263
|
-
|
|
264
|
-
async function bootstrap() {
|
|
265
|
-
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
|
266
|
-
if (!canvas) {
|
|
267
|
-
console.error('Canvas element not found');
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
canvas.width = window.innerWidth;
|
|
272
|
-
canvas.height = window.innerHeight;
|
|
273
|
-
|
|
274
|
-
window.addEventListener('resize', () => {
|
|
275
|
-
canvas.width = window.innerWidth;
|
|
276
|
-
canvas.height = window.innerHeight;
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const scene = new HelloWorldScene();
|
|
280
|
-
await scene.start(canvas);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
bootstrap().catch((err) => {
|
|
284
|
-
console.error('Failed to initialize Retiam animation:', err);
|
|
285
|
-
});
|
|
286
|
-
`;
|
|
287
|
-
|
|
288
|
-
// 6. src/scenes/HelloWorld.ts
|
|
289
|
-
const helloWorldTs = `import { Scene, hex, SDFText, Write } from "retiam";
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Starter Retiam Scene: Hello World
|
|
293
|
-
* Features a cinematic Write animation over an obsidian dark substrate.
|
|
294
|
-
*/
|
|
295
|
-
export default class HelloWorldScene extends Scene {
|
|
296
|
-
public async run(): Promise<void> {
|
|
297
|
-
// 1. Setup camera and obsidian canvas
|
|
298
|
-
this.camera.position.set([0, 0, 14]);
|
|
299
|
-
this.clearColor(hex("#080C14"));
|
|
300
|
-
|
|
301
|
-
// 2. Create the hero typography
|
|
302
|
-
const text = new SDFText("${heroTitle.replace(/"/g, '\\"')}", { fontSize: 1.1 });
|
|
303
|
-
text.paint.fill = hex("#38BDF8");
|
|
304
|
-
text.center();
|
|
305
|
-
|
|
306
|
-
// 3. Register entity and frame camera with balanced padding
|
|
307
|
-
this.add(text);
|
|
308
|
-
this.camera.frame(text, { padding: 0.3 });
|
|
309
|
-
|
|
310
|
-
// 4. Choreographed motion: Write animation
|
|
311
|
-
await this.play(new Write(text, { duration: 1500 }));
|
|
312
|
-
await this.wait(2000);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
`;
|
|
316
|
-
|
|
317
|
-
// 7. .gitignore
|
|
318
|
-
const gitignore = `node_modules
|
|
319
|
-
dist
|
|
320
|
-
outputs
|
|
321
|
-
.DS_Store
|
|
322
|
-
*.local
|
|
323
|
-
`;
|
|
324
|
-
|
|
325
|
-
// 8. README.md
|
|
326
|
-
const readme = `# ${resolvedName}
|
|
327
|
-
|
|
328
|
-
A cinematic WebGPU mathematical animation built with [Retiam](https://github.com/abhilasbiswas/Retiam).
|
|
329
|
-
|
|
330
|
-
## 🚀 Quick Start
|
|
331
|
-
|
|
332
|
-
1. **Install dependencies**:
|
|
333
|
-
\`\`\`bash
|
|
334
|
-
${packageManager} install
|
|
335
|
-
\`\`\`
|
|
336
|
-
|
|
337
|
-
2. **Start real-time development server (60-144 FPS)**:
|
|
338
|
-
\`\`\`bash
|
|
339
|
-
${packageManager} run dev
|
|
340
|
-
\`\`\`
|
|
341
|
-
|
|
342
|
-
3. **Render broadcast video (MP4/WebM)**:
|
|
343
|
-
\`\`\`bash
|
|
344
|
-
${packageManager} run render
|
|
345
|
-
\`\`\`
|
|
346
|
-
|
|
347
|
-
## 📁 Project Structure
|
|
348
|
-
|
|
349
|
-
- \`src/main.ts\`: Entrypoint mounting the WebGPU canvas.
|
|
350
|
-
- \`src/scenes/HelloWorld.ts\`: Starter scene with hero typography and \`Write\` animation.
|
|
351
|
-
- \`vite.config.ts\`: Real-time WebGPU bundler configuration with hot module reload.
|
|
352
|
-
`;
|
|
353
|
-
|
|
354
|
-
// Write all template files
|
|
355
|
-
fs.writeFileSync(path.join(resolvedDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
356
|
-
fs.writeFileSync(path.join(resolvedDir, 'tsconfig.json'), JSON.stringify(tsconfigJson, null, 2) + '\n', 'utf8');
|
|
357
|
-
fs.writeFileSync(path.join(resolvedDir, 'vite.config.ts'), viteConfig, 'utf8');
|
|
358
|
-
fs.writeFileSync(path.join(resolvedDir, 'index.html'), indexHtml, 'utf8');
|
|
359
|
-
fs.writeFileSync(path.join(resolvedDir, '.gitignore'), gitignore, 'utf8');
|
|
360
|
-
fs.writeFileSync(path.join(resolvedDir, 'README.md'), readme, 'utf8');
|
|
361
|
-
fs.writeFileSync(path.join(srcDir, 'main.ts'), mainTs, 'utf8');
|
|
362
|
-
fs.writeFileSync(path.join(scenesDir, 'HelloWorld.ts'), helloWorldTs, 'utf8');
|
|
363
|
-
|
|
364
|
-
const createdFiles = [
|
|
365
|
-
'package.json',
|
|
366
|
-
'tsconfig.json',
|
|
367
|
-
'vite.config.ts',
|
|
368
|
-
'index.html',
|
|
369
|
-
'.gitignore',
|
|
370
|
-
'README.md',
|
|
371
|
-
'src/main.ts',
|
|
372
|
-
'src/scenes/HelloWorld.ts'
|
|
373
|
-
];
|
|
374
|
-
|
|
375
|
-
// Optional git init
|
|
376
|
-
if (initGit) {
|
|
377
|
-
try {
|
|
378
|
-
spawnSync('git', ['init'], { cwd: resolvedDir, stdio: 'ignore' });
|
|
379
|
-
} catch (_) {}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Optional install
|
|
383
|
-
if (runInstall) {
|
|
384
|
-
if (!jsonOutput) {
|
|
385
|
-
console.log(`\n📦 Installing dependencies with ${packageManager}...`);
|
|
386
|
-
}
|
|
387
|
-
try {
|
|
388
|
-
spawnSync(packageManager, ['install'], { cwd: resolvedDir, stdio: jsonOutput ? 'ignore' : 'inherit' });
|
|
389
|
-
} catch (err) {
|
|
390
|
-
if (!jsonOutput) {
|
|
391
|
-
console.warn(`⚠️ Failed to run '${packageManager} install'. Please run it manually.`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (jsonOutput) {
|
|
397
|
-
console.log(JSON.stringify({
|
|
398
|
-
status: 'success',
|
|
399
|
-
directory: targetDir,
|
|
400
|
-
absolutePath: resolvedDir,
|
|
401
|
-
projectName: resolvedName,
|
|
402
|
-
packageManager,
|
|
403
|
-
files: createdFiles
|
|
404
|
-
}, null, 2));
|
|
405
|
-
} else {
|
|
406
|
-
console.log(`\n✨ Successfully created Retiam project in '${targetDir}'!\n`);
|
|
407
|
-
console.log(`Next steps:`);
|
|
408
|
-
if (path.resolve(process.cwd()) !== resolvedDir) {
|
|
409
|
-
console.log(` 1. cd ${targetDir}`);
|
|
410
|
-
}
|
|
411
|
-
if (!runInstall) {
|
|
412
|
-
console.log(` 2. ${packageManager} install`);
|
|
413
|
-
console.log(` 3. ${packageManager} run dev # Real-time WebGPU preview`);
|
|
414
|
-
console.log(` 4. ${packageManager} run render # Export MP4 broadcast video\n`);
|
|
415
|
-
} else {
|
|
416
|
-
console.log(` 2. ${packageManager} run dev # Real-time WebGPU preview`);
|
|
417
|
-
console.log(` 3. ${packageManager} run render # Export MP4 broadcast video\n`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
main().catch((err) => {
|
|
423
|
-
console.error('Initialization failed:', err);
|
|
424
|
-
process.exit(1);
|
|
425
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// =========================================================================
|
|
4
|
+
// File: bin/init.js - Retiam Project Starter & Skeleton Generator
|
|
5
|
+
// =========================================================================
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import readline from 'readline/promises';
|
|
10
|
+
import { spawnSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
// Check for --help
|
|
15
|
+
const isHelp = args.includes('--help') || args.includes('-h') || args.includes('help');
|
|
16
|
+
if (isHelp) {
|
|
17
|
+
console.log(`
|
|
18
|
+
🚀 Retiam WebGPU Engine - Project Initializer
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
npx retiam init [directory] [options]
|
|
22
|
+
npx retiam create [directory] [options]
|
|
23
|
+
|
|
24
|
+
Arguments:
|
|
25
|
+
[directory] Destination directory (default: ./retiam-animation)
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
-n, --name <name> Project name for package.json (default: directory name)
|
|
29
|
+
--title <text> Hero text to display in Hello World scene (default: Hello World)
|
|
30
|
+
--pm <manager> Package manager: npm, pnpm, yarn, bun (default: auto-detected or npm)
|
|
31
|
+
-f, --force Overwrite destination directory if files already exist
|
|
32
|
+
-y, --yes Skip interactive prompts and use defaults
|
|
33
|
+
--install Automatically install dependencies after generation
|
|
34
|
+
--git Initialize a Git repository (git init)
|
|
35
|
+
--json Output structured machine-readable JSON for agents and CI
|
|
36
|
+
--help, -h Show this help message and exit
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
npx retiam init
|
|
40
|
+
npx retiam init my-animation
|
|
41
|
+
npx retiam create intro-scene --title "Quantum Mechanics" --pm pnpm
|
|
42
|
+
npx retiam init my-project -y --install
|
|
43
|
+
npx retiam init agent-project --json --yes
|
|
44
|
+
`);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Auto-detect package manager from npm_config_user_agent
|
|
49
|
+
function detectPackageManager() {
|
|
50
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
51
|
+
if (userAgent.startsWith('pnpm')) return 'pnpm';
|
|
52
|
+
if (userAgent.startsWith('yarn')) return 'yarn';
|
|
53
|
+
if (userAgent.startsWith('bun')) return 'bun';
|
|
54
|
+
return 'npm';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse CLI flags
|
|
58
|
+
let targetDir = null;
|
|
59
|
+
let projectName = null;
|
|
60
|
+
let heroTitle = 'Hello World';
|
|
61
|
+
let packageManager = detectPackageManager();
|
|
62
|
+
let force = false;
|
|
63
|
+
let nonInteractive = false;
|
|
64
|
+
let runInstall = false;
|
|
65
|
+
let initGit = false;
|
|
66
|
+
let jsonOutput = false;
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < args.length; i++) {
|
|
69
|
+
const arg = args[i];
|
|
70
|
+
if (arg === '--json') {
|
|
71
|
+
jsonOutput = true;
|
|
72
|
+
nonInteractive = true;
|
|
73
|
+
} else if (arg === '-y' || arg === '--yes') {
|
|
74
|
+
nonInteractive = true;
|
|
75
|
+
} else if (arg === '-f' || arg === '--force') {
|
|
76
|
+
force = true;
|
|
77
|
+
} else if (arg === '--install') {
|
|
78
|
+
runInstall = true;
|
|
79
|
+
} else if (arg === '--git') {
|
|
80
|
+
initGit = true;
|
|
81
|
+
} else if (arg === '-n' || arg === '--name') {
|
|
82
|
+
projectName = args[++i];
|
|
83
|
+
} else if (arg === '--title') {
|
|
84
|
+
heroTitle = args[++i] || 'Hello World';
|
|
85
|
+
} else if (arg === '--pm') {
|
|
86
|
+
packageManager = args[++i] || packageManager;
|
|
87
|
+
} else if (!arg.startsWith('-') && !targetDir) {
|
|
88
|
+
targetDir = arg;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
// Interactive setup if TTY and not in --yes / --json mode
|
|
94
|
+
const isTTY = process.stdin.isTTY && !nonInteractive;
|
|
95
|
+
|
|
96
|
+
if (isTTY) {
|
|
97
|
+
const rl = readline.createInterface({
|
|
98
|
+
input: process.stdin,
|
|
99
|
+
output: process.stdout
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
console.log(`\n✨ Welcome to Retiam - WebGPU Animation Project Generator\n`);
|
|
104
|
+
|
|
105
|
+
if (!targetDir) {
|
|
106
|
+
const inputDir = await rl.question(`📁 Destination directory (./retiam-animation): `);
|
|
107
|
+
targetDir = inputDir.trim() || 'retiam-animation';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const defaultName = path.basename(path.resolve(process.cwd(), targetDir))
|
|
111
|
+
.toLowerCase()
|
|
112
|
+
.replace(/[^a-z0-9_-]/g, '-');
|
|
113
|
+
|
|
114
|
+
if (!projectName) {
|
|
115
|
+
const inputName = await rl.question(`📦 Project name (${defaultName}): `);
|
|
116
|
+
projectName = inputName.trim() || defaultName;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const inputTitle = await rl.question(`🎨 Starter animation text (${heroTitle}): `);
|
|
120
|
+
if (inputTitle.trim()) {
|
|
121
|
+
heroTitle = inputTitle.trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const inputPm = await rl.question(`⚡ Package manager [npm/pnpm/yarn/bun] (${packageManager}): `);
|
|
125
|
+
if (inputPm.trim()) {
|
|
126
|
+
packageManager = inputPm.trim().toLowerCase();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const inputInstall = await rl.question(`📥 Run '${packageManager} install' now? [y/N]: `);
|
|
130
|
+
if (inputInstall.trim().toLowerCase().startsWith('y')) {
|
|
131
|
+
runInstall = true;
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
rl.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!targetDir) {
|
|
139
|
+
targetDir = 'retiam-animation';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const resolvedDir = path.resolve(process.cwd(), targetDir);
|
|
143
|
+
const resolvedName = projectName || path.basename(resolvedDir)
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.replace(/[^a-z0-9_-]/g, '-') || 'retiam-animation';
|
|
146
|
+
|
|
147
|
+
// Conflict check
|
|
148
|
+
if (fs.existsSync(resolvedDir)) {
|
|
149
|
+
const existingFiles = fs.readdirSync(resolvedDir).filter(f => !f.startsWith('.'));
|
|
150
|
+
if (existingFiles.length > 0 && !force) {
|
|
151
|
+
if (jsonOutput) {
|
|
152
|
+
console.log(JSON.stringify({
|
|
153
|
+
status: 'error',
|
|
154
|
+
message: `Target directory '${targetDir}' already exists and is not empty. Use -f or --force to overwrite.`
|
|
155
|
+
}));
|
|
156
|
+
} else {
|
|
157
|
+
console.error(`\n❌ Error: Directory '${targetDir}' is not empty (${existingFiles.length} files found).`);
|
|
158
|
+
console.error(` Use -f or --force to proceed anyway.\n`);
|
|
159
|
+
}
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Ensure directory structure
|
|
165
|
+
const srcDir = path.join(resolvedDir, 'src');
|
|
166
|
+
const scenesDir = path.join(srcDir, 'scenes');
|
|
167
|
+
fs.mkdirSync(scenesDir, { recursive: true });
|
|
168
|
+
|
|
169
|
+
// 1. package.json
|
|
170
|
+
const packageJson = {
|
|
171
|
+
name: resolvedName,
|
|
172
|
+
version: '0.1.0',
|
|
173
|
+
private: true,
|
|
174
|
+
type: 'module',
|
|
175
|
+
scripts: {
|
|
176
|
+
dev: 'vite',
|
|
177
|
+
build: 'tsc && vite build',
|
|
178
|
+
preview: 'vite preview',
|
|
179
|
+
render: 'retiam render src/scenes/HelloWorld.ts'
|
|
180
|
+
},
|
|
181
|
+
dependencies: {
|
|
182
|
+
retiam: '^1.0.0'
|
|
183
|
+
},
|
|
184
|
+
devDependencies: {
|
|
185
|
+
'@webgpu/types': '^0.1.31',
|
|
186
|
+
typescript: '^5.5.0',
|
|
187
|
+
vite: '^5.4.0'
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// 2. tsconfig.json
|
|
192
|
+
const tsconfigJson = {
|
|
193
|
+
compilerOptions: {
|
|
194
|
+
target: 'ES2022',
|
|
195
|
+
module: 'ESNext',
|
|
196
|
+
moduleResolution: 'bundler',
|
|
197
|
+
lib: ['ES2022', 'DOM', 'DOM.Iterable'],
|
|
198
|
+
types: ['@webgpu/types', 'vite/client'],
|
|
199
|
+
strict: true,
|
|
200
|
+
esModuleInterop: true,
|
|
201
|
+
skipLibCheck: true,
|
|
202
|
+
isolatedModules: true
|
|
203
|
+
},
|
|
204
|
+
include: ['src']
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// 3. vite.config.ts
|
|
208
|
+
const viteConfig = `import { defineConfig } from 'vite';
|
|
209
|
+
|
|
210
|
+
export default defineConfig({
|
|
211
|
+
optimizeDeps: {
|
|
212
|
+
esbuildOptions: {
|
|
213
|
+
target: 'es2022',
|
|
214
|
+
supported: {
|
|
215
|
+
'top-level-await': true
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
esbuild: {
|
|
220
|
+
target: 'es2022'
|
|
221
|
+
},
|
|
222
|
+
server: {
|
|
223
|
+
port: 3000,
|
|
224
|
+
open: true
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
// 4. index.html
|
|
230
|
+
const indexHtml = `<!DOCTYPE html>
|
|
231
|
+
<html lang="en">
|
|
232
|
+
<head>
|
|
233
|
+
<meta charset="UTF-8" />
|
|
234
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
235
|
+
<title>${resolvedName} - Retiam WebGPU Animation</title>
|
|
236
|
+
<style>
|
|
237
|
+
* {
|
|
238
|
+
box-sizing: border-box;
|
|
239
|
+
}
|
|
240
|
+
body {
|
|
241
|
+
margin: 0;
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
background-color: #080C14;
|
|
244
|
+
width: 100vw;
|
|
245
|
+
height: 100vh;
|
|
246
|
+
}
|
|
247
|
+
canvas {
|
|
248
|
+
display: block;
|
|
249
|
+
width: 100vw;
|
|
250
|
+
height: 100vh;
|
|
251
|
+
}
|
|
252
|
+
</style>
|
|
253
|
+
</head>
|
|
254
|
+
<body>
|
|
255
|
+
<canvas id="canvas"></canvas>
|
|
256
|
+
<script type="module" src="/src/main.ts"></script>
|
|
257
|
+
</body>
|
|
258
|
+
</html>
|
|
259
|
+
`;
|
|
260
|
+
|
|
261
|
+
// 5. src/main.ts
|
|
262
|
+
const mainTs = `import HelloWorldScene from './scenes/HelloWorld';
|
|
263
|
+
|
|
264
|
+
async function bootstrap() {
|
|
265
|
+
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
|
266
|
+
if (!canvas) {
|
|
267
|
+
console.error('Canvas element not found');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
canvas.width = window.innerWidth;
|
|
272
|
+
canvas.height = window.innerHeight;
|
|
273
|
+
|
|
274
|
+
window.addEventListener('resize', () => {
|
|
275
|
+
canvas.width = window.innerWidth;
|
|
276
|
+
canvas.height = window.innerHeight;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const scene = new HelloWorldScene();
|
|
280
|
+
await scene.start(canvas);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
bootstrap().catch((err) => {
|
|
284
|
+
console.error('Failed to initialize Retiam animation:', err);
|
|
285
|
+
});
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
// 6. src/scenes/HelloWorld.ts
|
|
289
|
+
const helloWorldTs = `import { Scene, hex, SDFText, Write } from "retiam";
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Starter Retiam Scene: Hello World
|
|
293
|
+
* Features a cinematic Write animation over an obsidian dark substrate.
|
|
294
|
+
*/
|
|
295
|
+
export default class HelloWorldScene extends Scene {
|
|
296
|
+
public async run(): Promise<void> {
|
|
297
|
+
// 1. Setup camera and obsidian canvas
|
|
298
|
+
this.camera.position.set([0, 0, 14]);
|
|
299
|
+
this.clearColor(hex("#080C14"));
|
|
300
|
+
|
|
301
|
+
// 2. Create the hero typography
|
|
302
|
+
const text = new SDFText("${heroTitle.replace(/"/g, '\\"')}", { fontSize: 1.1 });
|
|
303
|
+
text.paint.fill = hex("#38BDF8");
|
|
304
|
+
text.center();
|
|
305
|
+
|
|
306
|
+
// 3. Register entity and frame camera with balanced padding
|
|
307
|
+
this.add(text);
|
|
308
|
+
this.camera.frame(text, { padding: 0.3 });
|
|
309
|
+
|
|
310
|
+
// 4. Choreographed motion: Write animation
|
|
311
|
+
await this.play(new Write(text, { duration: 1500 }));
|
|
312
|
+
await this.wait(2000);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
`;
|
|
316
|
+
|
|
317
|
+
// 7. .gitignore
|
|
318
|
+
const gitignore = `node_modules
|
|
319
|
+
dist
|
|
320
|
+
outputs
|
|
321
|
+
.DS_Store
|
|
322
|
+
*.local
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
// 8. README.md
|
|
326
|
+
const readme = `# ${resolvedName}
|
|
327
|
+
|
|
328
|
+
A cinematic WebGPU mathematical animation built with [Retiam](https://github.com/abhilasbiswas/Retiam).
|
|
329
|
+
|
|
330
|
+
## 🚀 Quick Start
|
|
331
|
+
|
|
332
|
+
1. **Install dependencies**:
|
|
333
|
+
\`\`\`bash
|
|
334
|
+
${packageManager} install
|
|
335
|
+
\`\`\`
|
|
336
|
+
|
|
337
|
+
2. **Start real-time development server (60-144 FPS)**:
|
|
338
|
+
\`\`\`bash
|
|
339
|
+
${packageManager} run dev
|
|
340
|
+
\`\`\`
|
|
341
|
+
|
|
342
|
+
3. **Render broadcast video (MP4/WebM)**:
|
|
343
|
+
\`\`\`bash
|
|
344
|
+
${packageManager} run render
|
|
345
|
+
\`\`\`
|
|
346
|
+
|
|
347
|
+
## 📁 Project Structure
|
|
348
|
+
|
|
349
|
+
- \`src/main.ts\`: Entrypoint mounting the WebGPU canvas.
|
|
350
|
+
- \`src/scenes/HelloWorld.ts\`: Starter scene with hero typography and \`Write\` animation.
|
|
351
|
+
- \`vite.config.ts\`: Real-time WebGPU bundler configuration with hot module reload.
|
|
352
|
+
`;
|
|
353
|
+
|
|
354
|
+
// Write all template files
|
|
355
|
+
fs.writeFileSync(path.join(resolvedDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
356
|
+
fs.writeFileSync(path.join(resolvedDir, 'tsconfig.json'), JSON.stringify(tsconfigJson, null, 2) + '\n', 'utf8');
|
|
357
|
+
fs.writeFileSync(path.join(resolvedDir, 'vite.config.ts'), viteConfig, 'utf8');
|
|
358
|
+
fs.writeFileSync(path.join(resolvedDir, 'index.html'), indexHtml, 'utf8');
|
|
359
|
+
fs.writeFileSync(path.join(resolvedDir, '.gitignore'), gitignore, 'utf8');
|
|
360
|
+
fs.writeFileSync(path.join(resolvedDir, 'README.md'), readme, 'utf8');
|
|
361
|
+
fs.writeFileSync(path.join(srcDir, 'main.ts'), mainTs, 'utf8');
|
|
362
|
+
fs.writeFileSync(path.join(scenesDir, 'HelloWorld.ts'), helloWorldTs, 'utf8');
|
|
363
|
+
|
|
364
|
+
const createdFiles = [
|
|
365
|
+
'package.json',
|
|
366
|
+
'tsconfig.json',
|
|
367
|
+
'vite.config.ts',
|
|
368
|
+
'index.html',
|
|
369
|
+
'.gitignore',
|
|
370
|
+
'README.md',
|
|
371
|
+
'src/main.ts',
|
|
372
|
+
'src/scenes/HelloWorld.ts'
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
// Optional git init
|
|
376
|
+
if (initGit) {
|
|
377
|
+
try {
|
|
378
|
+
spawnSync('git', ['init'], { cwd: resolvedDir, stdio: 'ignore' });
|
|
379
|
+
} catch (_) {}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Optional install
|
|
383
|
+
if (runInstall) {
|
|
384
|
+
if (!jsonOutput) {
|
|
385
|
+
console.log(`\n📦 Installing dependencies with ${packageManager}...`);
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
spawnSync(packageManager, ['install'], { cwd: resolvedDir, stdio: jsonOutput ? 'ignore' : 'inherit' });
|
|
389
|
+
} catch (err) {
|
|
390
|
+
if (!jsonOutput) {
|
|
391
|
+
console.warn(`⚠️ Failed to run '${packageManager} install'. Please run it manually.`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (jsonOutput) {
|
|
397
|
+
console.log(JSON.stringify({
|
|
398
|
+
status: 'success',
|
|
399
|
+
directory: targetDir,
|
|
400
|
+
absolutePath: resolvedDir,
|
|
401
|
+
projectName: resolvedName,
|
|
402
|
+
packageManager,
|
|
403
|
+
files: createdFiles
|
|
404
|
+
}, null, 2));
|
|
405
|
+
} else {
|
|
406
|
+
console.log(`\n✨ Successfully created Retiam project in '${targetDir}'!\n`);
|
|
407
|
+
console.log(`Next steps:`);
|
|
408
|
+
if (path.resolve(process.cwd()) !== resolvedDir) {
|
|
409
|
+
console.log(` 1. cd ${targetDir}`);
|
|
410
|
+
}
|
|
411
|
+
if (!runInstall) {
|
|
412
|
+
console.log(` 2. ${packageManager} install`);
|
|
413
|
+
console.log(` 3. ${packageManager} run dev # Real-time WebGPU preview`);
|
|
414
|
+
console.log(` 4. ${packageManager} run render # Export MP4 broadcast video\n`);
|
|
415
|
+
} else {
|
|
416
|
+
console.log(` 2. ${packageManager} run dev # Real-time WebGPU preview`);
|
|
417
|
+
console.log(` 3. ${packageManager} run render # Export MP4 broadcast video\n`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
main().catch((err) => {
|
|
423
|
+
console.error('Initialization failed:', err);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
});
|