create-substrate 0.1.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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +6 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +127 -0
- package/dist/prompts.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +395 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/surfaces/3d-scene.d.ts +3 -0
- package/dist/surfaces/3d-scene.d.ts.map +1 -0
- package/dist/surfaces/3d-scene.js +184 -0
- package/dist/surfaces/3d-scene.js.map +1 -0
- package/dist/surfaces/animation.d.ts +3 -0
- package/dist/surfaces/animation.d.ts.map +1 -0
- package/dist/surfaces/animation.js +211 -0
- package/dist/surfaces/animation.js.map +1 -0
- package/dist/surfaces/blank.d.ts +3 -0
- package/dist/surfaces/blank.d.ts.map +1 -0
- package/dist/surfaces/blank.js +72 -0
- package/dist/surfaces/blank.js.map +1 -0
- package/dist/surfaces/canvas-2d.d.ts +3 -0
- package/dist/surfaces/canvas-2d.d.ts.map +1 -0
- package/dist/surfaces/canvas-2d.js +139 -0
- package/dist/surfaces/canvas-2d.js.map +1 -0
- package/dist/surfaces/data-vis.d.ts +3 -0
- package/dist/surfaces/data-vis.d.ts.map +1 -0
- package/dist/surfaces/data-vis.js +175 -0
- package/dist/surfaces/data-vis.js.map +1 -0
- package/dist/surfaces/image-gen.d.ts +3 -0
- package/dist/surfaces/image-gen.d.ts.map +1 -0
- package/dist/surfaces/image-gen.js +193 -0
- package/dist/surfaces/image-gen.js.map +1 -0
- package/dist/surfaces/index.d.ts +4 -0
- package/dist/surfaces/index.d.ts.map +1 -0
- package/dist/surfaces/index.js +17 -0
- package/dist/surfaces/index.js.map +1 -0
- package/dist/surfaces/node-editor.d.ts +3 -0
- package/dist/surfaces/node-editor.d.ts.map +1 -0
- package/dist/surfaces/node-editor.js +211 -0
- package/dist/surfaces/node-editor.js.map +1 -0
- package/dist/surfaces/types.d.ts +22 -0
- package/dist/surfaces/types.d.ts.map +1 -0
- package/dist/surfaces/types.js +10 -0
- package/dist/surfaces/types.js.map +1 -0
- package/dist/utils/detect-pm.d.ts +5 -0
- package/dist/utils/detect-pm.d.ts.map +1 -0
- package/dist/utils/detect-pm.js +20 -0
- package/dist/utils/detect-pm.js.map +1 -0
- package/dist/utils/fs.d.ts +7 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +52 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +15 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/shell.d.ts +7 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +28 -0
- package/dist/utils/shell.js.map +1 -0
- package/package.json +35 -0
- package/skills/3d-scene/SKILL.md +172 -0
- package/skills/animation/SKILL.md +194 -0
- package/skills/canvas-2d/SKILL.md +132 -0
- package/skills/composing-panels/SKILL.md +309 -0
- package/skills/create-custom-tool/SKILL.md +157 -0
- package/skills/data-visualisation/SKILL.md +228 -0
- package/skills/image-generation/SKILL.md +211 -0
- package/skills/scaffold-playground/SKILL.md +141 -0
- package/skills/substrate-canvas/SKILL.md +217 -0
- package/skills/substrate-controls/SKILL.md +242 -0
- package/skills/substrate-feedback/SKILL.md +219 -0
- package/skills/substrate-interaction/SKILL.md +286 -0
- package/skills/substrate-nodes/SKILL.md +208 -0
- package/skills/substrate-scaffold/SKILL.md +206 -0
- package/skills/theming/SKILL.md +117 -0
- package/skills/wire-interactions/SKILL.md +155 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/surfaces/types.ts"],"names":[],"mappings":"AAeA,MAAM,CAAC,MAAM,eAAe,GAAoB;IAC9C,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,2CAA2C,EAAE;IACpG,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,0DAA0D,EAAE;IACjH,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,qDAAqD,EAAE;IAC9G,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,mDAAmD,EAAE;IAChH,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,yCAAyC,EAAE;IAChG,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACzG,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,wCAAwC,EAAE;CAC1F,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type PackageManager = "npm" | "pnpm" | "bun";
|
|
2
|
+
export declare function detectPackageManager(cwd: string): PackageManager;
|
|
3
|
+
export declare function getInstallCommand(pm: PackageManager): string;
|
|
4
|
+
export declare function getRunCommand(pm: PackageManager, script: string): string;
|
|
5
|
+
//# sourceMappingURL=detect-pm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect-pm.d.ts","sourceRoot":"","sources":["../../src/utils/detect-pm.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;AAEnD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAQhE;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGxE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function detectPackageManager(cwd) {
|
|
4
|
+
if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) {
|
|
5
|
+
return "bun";
|
|
6
|
+
}
|
|
7
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
|
|
8
|
+
return "pnpm";
|
|
9
|
+
}
|
|
10
|
+
return "npm";
|
|
11
|
+
}
|
|
12
|
+
export function getInstallCommand(pm) {
|
|
13
|
+
return `${pm} install`;
|
|
14
|
+
}
|
|
15
|
+
export function getRunCommand(pm, script) {
|
|
16
|
+
if (pm === "npm")
|
|
17
|
+
return `npm run ${script}`;
|
|
18
|
+
return `${pm} ${script}`;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=detect-pm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect-pm.js","sourceRoot":"","sources":["../../src/utils/detect-pm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAIhC,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;QAC5E,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC;QAC5C,OAAO,MAAM,CAAA;IACf,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAkB;IAClD,OAAO,GAAG,EAAE,UAAU,CAAA;AACxB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAkB,EAAE,MAAc;IAC9D,IAAI,EAAE,KAAK,KAAK;QAAE,OAAO,WAAW,MAAM,EAAE,CAAA;IAC5C,OAAO,GAAG,EAAE,IAAI,MAAM,EAAE,CAAA;AAC1B,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function ensureDir(dir: string): void;
|
|
2
|
+
export declare function writeFile(filePath: string, content: string): void;
|
|
3
|
+
export declare function copyDir(src: string, dest: string): void;
|
|
4
|
+
export declare function exists(path: string): boolean;
|
|
5
|
+
export declare function listDirs(dir: string): string[];
|
|
6
|
+
export declare function resolveSkillsDir(): string;
|
|
7
|
+
//# sourceMappingURL=fs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/utils/fs.ts"],"names":[],"mappings":"AAGA,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3C;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAGjE;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAgBvD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5C;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAK9C;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAUzC"}
|
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
export function ensureDir(dir) {
|
|
4
|
+
if (!existsSync(dir)) {
|
|
5
|
+
mkdirSync(dir, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function writeFile(filePath, content) {
|
|
9
|
+
ensureDir(dirname(filePath));
|
|
10
|
+
writeFileSync(filePath, content, "utf-8");
|
|
11
|
+
}
|
|
12
|
+
export function copyDir(src, dest) {
|
|
13
|
+
ensureDir(dest);
|
|
14
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const srcPath = join(src, entry.name);
|
|
17
|
+
const destPath = join(dest, entry.name);
|
|
18
|
+
try {
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
copyDir(srcPath, destPath);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
writeFileSync(destPath, readFileSync(srcPath));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Skip files that can't be read
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function exists(path) {
|
|
32
|
+
return existsSync(path);
|
|
33
|
+
}
|
|
34
|
+
export function listDirs(dir) {
|
|
35
|
+
if (!existsSync(dir))
|
|
36
|
+
return [];
|
|
37
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
38
|
+
.filter((d) => d.isDirectory())
|
|
39
|
+
.map((d) => d.name);
|
|
40
|
+
}
|
|
41
|
+
export function resolveSkillsDir() {
|
|
42
|
+
const distDir = dirname(dirname(new URL(import.meta.url).pathname));
|
|
43
|
+
const skillsDir = join(distDir, "skills");
|
|
44
|
+
if (existsSync(skillsDir))
|
|
45
|
+
return skillsDir;
|
|
46
|
+
const pkgRoot = join(distDir, "..");
|
|
47
|
+
const fallback = join(pkgRoot, "skills");
|
|
48
|
+
if (existsSync(fallback))
|
|
49
|
+
return fallback;
|
|
50
|
+
throw new Error("Could not locate bundled skills directory");
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=fs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/utils/fs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACzF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEzC,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACrC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,OAAe;IACzD,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5B,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;AAC3C,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,IAAY;IAC/C,SAAS,CAAC,IAAI,CAAC,CAAA;IACf,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;IACzD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QACvC,IAAI,CAAC;YACH,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;YAC5B,CAAC;iBAAM,CAAC;gBACN,aAAa,CAAC,QAAQ,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,CAAA;YAChD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,gCAAgC;QAClC,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IAC/B,OAAO,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;SAC7C,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;SAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;IACnE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACzC,IAAI,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAA;IAE3C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACxC,IAAI,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAA;IAEzC,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAA;AAC9D,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const log: {
|
|
2
|
+
info: (msg: string) => void;
|
|
3
|
+
success: (msg: string) => void;
|
|
4
|
+
warn: (msg: string) => void;
|
|
5
|
+
error: (msg: string) => void;
|
|
6
|
+
step: (msg: string) => void;
|
|
7
|
+
blank: () => void;
|
|
8
|
+
title: (msg: string) => void;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,GAAG;gBACF,MAAM;mBACH,MAAM;gBACT,MAAM;iBACL,MAAM;gBACP,MAAM;;iBAEL,MAAM;CAKpB,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export const log = {
|
|
3
|
+
info: (msg) => console.log(chalk.cyan("ℹ"), msg),
|
|
4
|
+
success: (msg) => console.log(chalk.green("✓"), msg),
|
|
5
|
+
warn: (msg) => console.log(chalk.yellow("⚠"), msg),
|
|
6
|
+
error: (msg) => console.error(chalk.red("✗"), msg),
|
|
7
|
+
step: (msg) => console.log(chalk.dim("→"), msg),
|
|
8
|
+
blank: () => console.log(),
|
|
9
|
+
title: (msg) => {
|
|
10
|
+
log.blank();
|
|
11
|
+
console.log(chalk.bold(msg));
|
|
12
|
+
log.blank();
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,IAAI,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;IACxD,OAAO,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;IAC5D,IAAI,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;IAC1D,KAAK,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;IAC1D,IAAI,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;IACvD,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;IAC1B,KAAK,EAAE,CAAC,GAAW,EAAE,EAAE;QACrB,GAAG,CAAC,KAAK,EAAE,CAAA;QACX,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;QAC5B,GAAG,CAAC,KAAK,EAAE,CAAA;IACb,CAAC;CACF,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ExecOptions {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
silent?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function exec(command: string, options?: ExecOptions): string;
|
|
6
|
+
export declare function execSilent(command: string, cwd?: string): string | null;
|
|
7
|
+
//# sourceMappingURL=shell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CAevE;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMvE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { log } from "./logger.js";
|
|
3
|
+
export function exec(command, options = {}) {
|
|
4
|
+
const { cwd, silent = false } = options;
|
|
5
|
+
try {
|
|
6
|
+
const result = execSync(command, {
|
|
7
|
+
cwd,
|
|
8
|
+
encoding: "utf-8",
|
|
9
|
+
stdio: silent ? "pipe" : "inherit",
|
|
10
|
+
});
|
|
11
|
+
return typeof result === "string" ? result.trim() : "";
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (!silent) {
|
|
15
|
+
log.error(`Command failed: ${command}`);
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function execSilent(command, cwd) {
|
|
21
|
+
try {
|
|
22
|
+
return exec(command, { cwd, silent: true });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAOjC,MAAM,UAAU,IAAI,CAAC,OAAe,EAAE,UAAuB,EAAE;IAC7D,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,EAAE,GAAG,OAAO,CAAA;IACvC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,EAAE;YAC/B,GAAG;YACH,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;SACnC,CAAC,CAAA;QACF,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACxD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,GAAG,CAAC,KAAK,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAA;QACzC,CAAC;QACD,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,GAAY;IACtD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-substrate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a Substrate creative tool playground",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-substrate": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"chalk": "^5.4.0",
|
|
15
|
+
"commander": "^13.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^20.0.0",
|
|
19
|
+
"tsx": "^4.0.0",
|
|
20
|
+
"typescript": "^5.0.0"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"skills"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"substrate",
|
|
28
|
+
"canvas",
|
|
29
|
+
"design-tool",
|
|
30
|
+
"vite",
|
|
31
|
+
"react",
|
|
32
|
+
"scaffold"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# 3D scene
|
|
2
|
+
|
|
3
|
+
Guide to building interactive 3D viewports with Substrate — object placement, orbit cameras, material editors, and lighting controls.
|
|
4
|
+
|
|
5
|
+
## When to use this surface
|
|
6
|
+
|
|
7
|
+
Use a 3D scene when the app involves spatial objects in three dimensions: product viewers, architectural walkthroughs, particle systems, shader playgrounds, or any tool where users orbit, zoom, and interact with a scene graph.
|
|
8
|
+
|
|
9
|
+
## Surface setup
|
|
10
|
+
|
|
11
|
+
The 3D viewport lives inside the Substrate layout where the canvas would normally go. Use Three.js (via `@react-three/fiber` and `@react-three/drei`) as the rendering engine.
|
|
12
|
+
|
|
13
|
+
### Dependencies
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install three @react-three/fiber @react-three/drei
|
|
17
|
+
npm install -D @types/three
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Mount the viewport
|
|
21
|
+
|
|
22
|
+
Create a surface component that fills the available space between panels:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
"use client"
|
|
26
|
+
|
|
27
|
+
import { Canvas } from "@react-three/fiber"
|
|
28
|
+
import { OrbitControls, Grid, Environment } from "@react-three/drei"
|
|
29
|
+
|
|
30
|
+
export function SceneViewport() {
|
|
31
|
+
return (
|
|
32
|
+
<div className="absolute inset-0 bg-canvas">
|
|
33
|
+
<Canvas camera={{ position: [5, 5, 5], fov: 50 }}>
|
|
34
|
+
<OrbitControls makeDefault />
|
|
35
|
+
<Grid infiniteGrid fadeDistance={50} />
|
|
36
|
+
<Environment preset="studio" />
|
|
37
|
+
<ambientLight intensity={0.4} />
|
|
38
|
+
<directionalLight position={[10, 10, 5]} intensity={1} />
|
|
39
|
+
{/* Scene objects render here */}
|
|
40
|
+
</Canvas>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Drop this into the playground layout in place of `<DesignCanvas />`.
|
|
47
|
+
|
|
48
|
+
## Store architecture
|
|
49
|
+
|
|
50
|
+
Create a Zustand scene store to manage the 3D scene graph. This follows the same pattern as `document-store` but for 3D objects:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
interface SceneObject {
|
|
54
|
+
id: string
|
|
55
|
+
name: string
|
|
56
|
+
type: "mesh" | "light" | "group" | "camera"
|
|
57
|
+
position: [number, number, number]
|
|
58
|
+
rotation: [number, number, number]
|
|
59
|
+
scale: [number, number, number]
|
|
60
|
+
geometry?: "box" | "sphere" | "cylinder" | "plane" | "torus" | "custom"
|
|
61
|
+
material?: MaterialConfig
|
|
62
|
+
visible: boolean
|
|
63
|
+
locked: boolean
|
|
64
|
+
children?: string[]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Store actions should include: `addObject`, `removeObject`, `updateObject`, `selectObject`, `pushSnapshot` (for undo/redo). Use the same snapshot-based undo pattern as the 2D canvas.
|
|
69
|
+
|
|
70
|
+
## Wiring to Substrate chrome
|
|
71
|
+
|
|
72
|
+
### Toolbar
|
|
73
|
+
|
|
74
|
+
Map toolbar toggles to 3D-specific modes:
|
|
75
|
+
|
|
76
|
+
| Tool | Purpose | Shortcut |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Select | Click objects in the viewport | V |
|
|
79
|
+
| Move | Translate selected object(s) | G |
|
|
80
|
+
| Rotate | Rotate selected object(s) | R |
|
|
81
|
+
| Scale | Scale selected object(s) | S |
|
|
82
|
+
| Add | Open an add menu (cube, sphere, light, etc.) | Shift+A |
|
|
83
|
+
|
|
84
|
+
Use `useToolStore` — the same store the 2D canvas uses. The tool type union can be extended with 3D-specific modes.
|
|
85
|
+
|
|
86
|
+
### Panels
|
|
87
|
+
|
|
88
|
+
**Left panel — scene hierarchy:**
|
|
89
|
+
|
|
90
|
+
A tree view of the scene graph. Each node shows name, type icon, and visibility toggle. Clicking selects the object in the viewport. This follows the same pattern as the layers panel but supports nesting (groups/parent-child).
|
|
91
|
+
|
|
92
|
+
**Right panel — properties inspector:**
|
|
93
|
+
|
|
94
|
+
Use the Pane/Action/Slider compound components to expose:
|
|
95
|
+
|
|
96
|
+
- **Transform pane** — X/Y/Z position, rotation (degrees), scale with number inputs
|
|
97
|
+
- **Geometry pane** — type-specific properties (radius, segments, width/height/depth)
|
|
98
|
+
- **Material pane** — colour picker, metalness/roughness sliders, texture upload
|
|
99
|
+
- **Light pane** (when a light is selected) — intensity slider, colour, shadow toggle
|
|
100
|
+
|
|
101
|
+
### Keyboard shortcuts
|
|
102
|
+
|
|
103
|
+
Extend `useKeyboardShortcuts` or create a `use3DShortcuts` hook:
|
|
104
|
+
|
|
105
|
+
- Delete — remove selected object
|
|
106
|
+
- Cmd+Z / Cmd+Shift+Z — undo/redo via snapshot store
|
|
107
|
+
- Cmd+D — duplicate selected
|
|
108
|
+
- H — toggle visibility of selected
|
|
109
|
+
- Numpad shortcuts for camera views (front/top/right) if desired
|
|
110
|
+
|
|
111
|
+
## Interaction patterns
|
|
112
|
+
|
|
113
|
+
### Object selection
|
|
114
|
+
|
|
115
|
+
`@react-three/fiber` provides `onPointerDown` on mesh components. Use raycasting (built into R3F) rather than manual hit-testing:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<mesh
|
|
119
|
+
onClick={(e) => {
|
|
120
|
+
e.stopPropagation()
|
|
121
|
+
sceneStore.selectObject(object.id, e.shiftKey)
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{/* geometry + material */}
|
|
125
|
+
</mesh>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Transform gizmos
|
|
129
|
+
|
|
130
|
+
Use `@react-three/drei`'s `TransformControls` for move/rotate/scale. Wire the active mode to `useToolStore`:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
{selectedId && (
|
|
134
|
+
<TransformControls
|
|
135
|
+
object={selectedRef}
|
|
136
|
+
mode={activeTool === "rotate" ? "rotate" : activeTool === "scale" ? "scale" : "translate"}
|
|
137
|
+
onObjectChange={() => sceneStore.updateFromRef(selectedId, selectedRef)}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Camera
|
|
143
|
+
|
|
144
|
+
`OrbitControls` handles orbit/zoom/pan. Disable it when a transform gizmo is active to prevent conflicts. The hand tool can temporarily enable a fly/walk camera mode.
|
|
145
|
+
|
|
146
|
+
## Theming
|
|
147
|
+
|
|
148
|
+
The 3D viewport background can read `--canvas` via CSS on the container div. For the Three.js scene background, read it from the DOM:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const style = getComputedStyle(document.documentElement)
|
|
152
|
+
const bg = style.getPropertyValue("--canvas").trim()
|
|
153
|
+
// Convert to Three.js colour and set scene.background
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Grid, gizmo, and selection highlight colours can be configured as constants or driven by additional CSS variables.
|
|
157
|
+
|
|
158
|
+
## What to build
|
|
159
|
+
|
|
160
|
+
Some examples of what this surface enables:
|
|
161
|
+
|
|
162
|
+
- **Product configurator** — load a 3D model, expose material/colour options in panes, orbit to inspect
|
|
163
|
+
- **Scene builder** — add primitives, position them, set up lighting, export as glTF
|
|
164
|
+
- **Shader playground** — custom `ShaderMaterial` with uniforms exposed as sliders in the properties panel
|
|
165
|
+
- **Particle system editor** — particle emitter properties as pane controls, real-time preview in the viewport
|
|
166
|
+
- **Architectural viewer** — import a model, add measurement tools, annotate with text labels
|
|
167
|
+
|
|
168
|
+
## Related reference skills
|
|
169
|
+
|
|
170
|
+
- **substrate-scaffold** — Toolbar, Panel, StatusBar, ZoomControls for the app shell
|
|
171
|
+
- **substrate-controls** — NumberInput, Slider, ColourPicker, ActionControls for property panes
|
|
172
|
+
- **substrate-interaction** — useUndoable, useClipboard, createKeyboardShortcuts, TreeList for scene hierarchy
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Animation
|
|
2
|
+
|
|
3
|
+
Guide to building timeline-based motion tools with Substrate — keyframe editing, easing controls, playback, and animated previews.
|
|
4
|
+
|
|
5
|
+
## When to use this surface
|
|
6
|
+
|
|
7
|
+
Use an animation surface when the app involves motion over time: UI animation prototyping, motion graphics editors, sprite animation, easing explorers, or any tool where users define how things change across a timeline.
|
|
8
|
+
|
|
9
|
+
## Surface setup
|
|
10
|
+
|
|
11
|
+
Animation tools combine a **preview stage** (where the animation plays) with a **timeline** (where keyframes are edited). The preview can be DOM-based (GSAP animating real elements) or canvas-based, depending on what's being animated.
|
|
12
|
+
|
|
13
|
+
### Dependencies
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install gsap
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
GSAP handles the animation engine — tweening, easing, timeline sequencing. For simpler cases, CSS animations or the Web Animations API may suffice, but GSAP provides the most control for a creative tool.
|
|
20
|
+
|
|
21
|
+
### Layout
|
|
22
|
+
|
|
23
|
+
Animation tools typically use a vertical split — preview on top, timeline on the bottom:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌──────────┬────────────────────────┬──────────┐
|
|
27
|
+
│ │ │ │
|
|
28
|
+
│ Left │ Preview stage │ Right │
|
|
29
|
+
│ panel │ (animated elements) │ panel │
|
|
30
|
+
│ │ │ │
|
|
31
|
+
│ Layers │ │ Props │
|
|
32
|
+
│ │ │ │
|
|
33
|
+
├──────────┴────────────────────────┴──────────┤
|
|
34
|
+
│ Timeline │
|
|
35
|
+
│ ──●────────●──────────●─────── ▶ 0:00 / 2:00│
|
|
36
|
+
│ [tracks with keyframe diamonds] │
|
|
37
|
+
└───────────────────────────────────────────────┘
|
|
38
|
+
[ Toolbar ]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Store architecture
|
|
42
|
+
|
|
43
|
+
### Animation store
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
interface AnimationState {
|
|
47
|
+
duration: number // Total duration in seconds
|
|
48
|
+
currentTime: number // Playhead position
|
|
49
|
+
isPlaying: boolean
|
|
50
|
+
fps: number // Preview framerate (default 60)
|
|
51
|
+
layers: AnimationLayer[]
|
|
52
|
+
selectedKeyframes: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface AnimationLayer {
|
|
56
|
+
id: string
|
|
57
|
+
name: string
|
|
58
|
+
targetId: string // Which element this layer animates
|
|
59
|
+
property: string // "x" | "y" | "opacity" | "scale" | "rotation" | ...
|
|
60
|
+
keyframes: Keyframe[]
|
|
61
|
+
visible: boolean
|
|
62
|
+
locked: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Keyframe {
|
|
66
|
+
id: string
|
|
67
|
+
time: number // Position in seconds
|
|
68
|
+
value: number
|
|
69
|
+
easing: EasingType // "linear" | "ease-in" | "ease-out" | "ease-in-out" | "cubic-bezier" | ...
|
|
70
|
+
bezierHandles?: [number, number, number, number] // For custom cubic-bezier
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Element store
|
|
75
|
+
|
|
76
|
+
Reuse the same pattern as the 2D canvas document store, but elements here represent animated objects on the stage rather than static shapes. Each element has base properties that keyframes override at specific times.
|
|
77
|
+
|
|
78
|
+
## Timeline component
|
|
79
|
+
|
|
80
|
+
The timeline is the core component that's unique to animation surfaces. It needs:
|
|
81
|
+
|
|
82
|
+
- **Playhead** — a draggable vertical line showing current time, scrubbable
|
|
83
|
+
- **Tracks** — one row per animated property, showing keyframe diamonds
|
|
84
|
+
- **Keyframe diamonds** — draggable markers at specific times, selectable
|
|
85
|
+
- **Zoom** — horizontal zoom to see more or fewer seconds
|
|
86
|
+
- **Rulers** — time markings along the top (seconds/frames)
|
|
87
|
+
- **Playback controls** — play/pause, jump to start/end, loop toggle
|
|
88
|
+
|
|
89
|
+
### Interaction model
|
|
90
|
+
|
|
91
|
+
The timeline uses horizontal dragging extensively:
|
|
92
|
+
|
|
93
|
+
- Drag the playhead to scrub through time
|
|
94
|
+
- Drag a keyframe diamond to move it in time
|
|
95
|
+
- Shift-click keyframes to multi-select
|
|
96
|
+
- Double-click a track to add a keyframe at that time
|
|
97
|
+
- Right-click a keyframe for easing options
|
|
98
|
+
|
|
99
|
+
This is pointer-event-driven, similar to the 2D canvas interaction model but on a 1D (time) axis rather than 2D space. Consider a dedicated `useTimelineInteraction` hook following the same state machine pattern as `useCanvasInteraction`.
|
|
100
|
+
|
|
101
|
+
## Wiring to Substrate chrome
|
|
102
|
+
|
|
103
|
+
### Toolbar
|
|
104
|
+
|
|
105
|
+
| Tool | Purpose | Shortcut |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| Select | Select keyframes on timeline | V |
|
|
108
|
+
| Add keyframe | Click on timeline to add | K |
|
|
109
|
+
| Play/Pause | Toggle playback | Space |
|
|
110
|
+
| Jump to start | Move playhead to 0 | Home |
|
|
111
|
+
| Jump to end | Move playhead to duration | End |
|
|
112
|
+
| Loop | Toggle loop playback | L |
|
|
113
|
+
|
|
114
|
+
### Left panel — layers
|
|
115
|
+
|
|
116
|
+
List animation layers (one per animated property per element). Show: element name, property name, visibility toggle, lock toggle. Group layers by element.
|
|
117
|
+
|
|
118
|
+
### Right panel — keyframe properties
|
|
119
|
+
|
|
120
|
+
When a keyframe is selected, show:
|
|
121
|
+
|
|
122
|
+
- **Time pane** — exact time position (number input)
|
|
123
|
+
- **Value pane** — the property value at this keyframe
|
|
124
|
+
- **Easing pane** — easing type dropdown plus a visual curve editor for cubic-bezier
|
|
125
|
+
- **Interpolation** — linear vs smooth vs step
|
|
126
|
+
|
|
127
|
+
The easing curve editor is a specialised pane — a small canvas showing the bezier curve with draggable control points.
|
|
128
|
+
|
|
129
|
+
## Preview stage
|
|
130
|
+
|
|
131
|
+
The preview stage renders the current state of the animation at `currentTime`. On each frame:
|
|
132
|
+
|
|
133
|
+
1. Read `currentTime` from the animation store
|
|
134
|
+
2. For each layer, interpolate between the surrounding keyframes using the specified easing
|
|
135
|
+
3. Apply interpolated values to the corresponding elements
|
|
136
|
+
4. Render the stage
|
|
137
|
+
|
|
138
|
+
During playback, a `requestAnimationFrame` loop advances `currentTime` at the configured FPS.
|
|
139
|
+
|
|
140
|
+
### Using GSAP
|
|
141
|
+
|
|
142
|
+
GSAP's timeline can be built from the keyframe data and scrubbed to `currentTime`:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// Build a GSAP timeline from store data
|
|
146
|
+
const tl = gsap.timeline({ paused: true })
|
|
147
|
+
|
|
148
|
+
for (const layer of layers) {
|
|
149
|
+
for (let i = 0; i < layer.keyframes.length - 1; i++) {
|
|
150
|
+
const from = layer.keyframes[i]
|
|
151
|
+
const to = layer.keyframes[i + 1]
|
|
152
|
+
tl.to(targetRef, {
|
|
153
|
+
[layer.property]: to.value,
|
|
154
|
+
duration: to.time - from.time,
|
|
155
|
+
ease: to.easing,
|
|
156
|
+
}, from.time)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Scrub to current time
|
|
161
|
+
tl.seek(currentTime)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Rebuild the GSAP timeline when keyframes change. Seek to `currentTime` on every frame during playback and when the user scrubs.
|
|
165
|
+
|
|
166
|
+
## Keyboard shortcuts
|
|
167
|
+
|
|
168
|
+
- Space — play/pause
|
|
169
|
+
- K — add keyframe at current time for selected property
|
|
170
|
+
- Delete — remove selected keyframes
|
|
171
|
+
- Cmd+Z / Cmd+Shift+Z — undo/redo
|
|
172
|
+
- Left/Right arrows — step forward/back one frame
|
|
173
|
+
- Shift+Left/Right — step forward/back 10 frames
|
|
174
|
+
- Home/End — jump to start/end
|
|
175
|
+
|
|
176
|
+
## Theming
|
|
177
|
+
|
|
178
|
+
The preview stage uses `bg-canvas`. The timeline area can use `bg-panel` or a slightly different shade. Keyframe diamonds, playhead, and track colours should be configurable — consider additional CSS variables like `--timeline-playhead`, `--timeline-keyframe`, `--timeline-track`.
|
|
179
|
+
|
|
180
|
+
## What to build
|
|
181
|
+
|
|
182
|
+
Some examples of what this surface enables:
|
|
183
|
+
|
|
184
|
+
- **UI animation prototyper** — animate component properties (opacity, position, scale), export CSS/GSAP code
|
|
185
|
+
- **Easing explorer** — visual comparison of different easing functions, interactive curve editor
|
|
186
|
+
- **Sprite animator** — frame-by-frame sprite animation with onion skinning
|
|
187
|
+
- **Motion graphics tool** — animate text and shapes along paths with timing controls
|
|
188
|
+
- **Transition builder** — design page/component transitions, preview at different speeds
|
|
189
|
+
|
|
190
|
+
## Related reference skills
|
|
191
|
+
|
|
192
|
+
- **substrate-scaffold** — Toolbar, Panel, StatusBar for the app shell
|
|
193
|
+
- **substrate-controls** — NumberInput, Slider, EasingCurveEditor, ActionControls for keyframe properties
|
|
194
|
+
- **substrate-interaction** — useUndoable, createKeyboardShortcuts, LayerList for animation layers
|