@flatkit/compiler 0.25.0 → 0.27.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/analysis.js +1 -1
- package/dist/{chunk-MRRRN2CS.js → chunk-IYPM3CU5.js} +37 -2
- package/dist/chunk-IYPM3CU5.js.map +1 -0
- package/dist/{chunk-GAS27LSJ.js → chunk-S63MOX6G.js} +18 -14
- package/dist/chunk-S63MOX6G.js.map +1 -0
- package/dist/cli/flatc.js +2 -2
- package/dist/cli/render.d.ts +36 -2
- package/dist/cli/render.js +76 -24
- package/dist/cli/render.js.map +1 -1
- package/dist/index.js +2 -2
- package/docs/README.md +55 -0
- package/docs/animating-symbols.md +357 -0
- package/docs/behavior-and-interactions.md +306 -0
- package/docs/dsl-gotchas.md +428 -0
- package/docs/embedding-fonts.md +95 -0
- package/docs/expressions-and-stdlib.md +107 -0
- package/docs/getting-started.md +107 -0
- package/docs/host-integration.md +162 -0
- package/docs/scene-and-drawing.md +168 -0
- package/docs/tooling.md +236 -0
- package/package.json +15 -7
- package/prompts/flatink-core.md +49 -46
- package/prompts/flatink-lite.md +20 -17
- package/prompts/role-asset-creator.md +11 -11
- package/prompts/role-coder.md +27 -28
- package/prompts/role-motion-designer.md +12 -12
- package/dist/chunk-GAS27LSJ.js.map +0 -1
- package/dist/chunk-MRRRN2CS.js.map +0 -1
package/dist/cli/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/cli/render.ts
|
|
2
2
|
import { FlatPlayer } from "@flatkit/player";
|
|
3
|
+
import { isGroup, isInstance } from "@flatkit/engine/layers";
|
|
3
4
|
import { mkdtempSync, writeFileSync, rmSync } from "fs";
|
|
4
5
|
import { tmpdir } from "os";
|
|
5
6
|
import { join } from "path";
|
|
@@ -33,24 +34,25 @@ function registerFonts(doc, FontLibrary) {
|
|
|
33
34
|
}
|
|
34
35
|
return { dir, families };
|
|
35
36
|
}
|
|
36
|
-
async function
|
|
37
|
+
async function createRenderer(doc, opts = {}) {
|
|
37
38
|
const skiaPkg = "skia-canvas";
|
|
38
39
|
let skia;
|
|
39
40
|
try {
|
|
40
41
|
skia = await import(skiaPkg);
|
|
41
42
|
} catch {
|
|
42
43
|
throw new Error(
|
|
43
|
-
'skia-canvas is required for
|
|
44
|
+
'skia-canvas is required for rendering. Install it as a dev dependency: `npm i -D skia-canvas` (pnpm users: also allow its build script with `pnpm approve-builds` or add "skia-canvas" to pnpm.onlyBuiltDependencies, then reinstall). If the native binary is still missing afterwards, run `node node_modules/skia-canvas/lib/prebuild.mjs download`.'
|
|
44
45
|
);
|
|
45
46
|
}
|
|
46
47
|
const { Canvas, loadImage, Path2D, FontLibrary, DOMMatrix } = skia;
|
|
47
48
|
const scale = opts.scale && opts.scale > 0 ? opts.scale : 2;
|
|
48
49
|
const W = doc.width, H = doc.height;
|
|
49
|
-
const
|
|
50
|
+
const withParams = opts.params ? applyParams(doc, opts.params) : doc;
|
|
51
|
+
const { dir: fontDir, families } = registerFonts(withParams, FontLibrary);
|
|
50
52
|
if (families.length) process.stderr.write(`flatc: registered ${families.length} font famil${families.length > 1 ? "ies" : "y"}: ${families.join(", ")}
|
|
51
53
|
`);
|
|
52
54
|
const images = /* @__PURE__ */ new Map();
|
|
53
|
-
for (const a of
|
|
55
|
+
for (const a of withParams.assets ?? []) {
|
|
54
56
|
if (a.kind === "svg" || a.kind === "image" || /^data:image\//.test(a.data ?? "")) {
|
|
55
57
|
try {
|
|
56
58
|
images.set(a.id, await loadImage(a.data));
|
|
@@ -77,7 +79,9 @@ async function renderDocToPng(doc, opts = {}) {
|
|
|
77
79
|
set("cancelAnimationFrame", () => {
|
|
78
80
|
});
|
|
79
81
|
set("document", { createElement: (t) => t === "canvas" ? new Canvas(1, 1) : {} });
|
|
80
|
-
const
|
|
82
|
+
const pxW = Math.max(1, Math.round(W * scale));
|
|
83
|
+
const pxH = Math.max(1, Math.round(H * scale));
|
|
84
|
+
const canvas = new Canvas(pxW, pxH);
|
|
81
85
|
const el = canvas;
|
|
82
86
|
Object.assign(el, {
|
|
83
87
|
getBoundingClientRect: () => ({ width: W, height: H, left: 0, top: 0, right: W, bottom: H }),
|
|
@@ -87,31 +91,79 @@ async function renderDocToPng(doc, opts = {}) {
|
|
|
87
91
|
},
|
|
88
92
|
style: {}
|
|
89
93
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
94
|
+
let player = new FlatPlayer(el, withParams, { input: false, audio: false, padding: 0, image: (id) => images.get(id) ?? null });
|
|
95
|
+
return {
|
|
96
|
+
width: pxW,
|
|
97
|
+
height: pxH,
|
|
98
|
+
async frame(frame, frameOpts = {}) {
|
|
99
|
+
if (!player) throw new Error("renderer is closed");
|
|
100
|
+
if (frameOpts.vars) for (const [k, v] of Object.entries(frameOpts.vars)) player.setVar(k, v);
|
|
101
|
+
player.seek(frame);
|
|
102
|
+
const steps = frameOpts.steps ?? 0;
|
|
103
|
+
if (steps > 0) {
|
|
104
|
+
const n = Math.min(Math.floor(steps), MAX_RENDER_STEPS);
|
|
105
|
+
if (steps > MAX_RENDER_STEPS) process.stderr.write(`flatc: steps clamped to ${MAX_RENDER_STEPS} (was ${steps})
|
|
97
106
|
`);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
player.stepSim(n);
|
|
108
|
+
}
|
|
109
|
+
return el.toBuffer("png");
|
|
110
|
+
},
|
|
111
|
+
close() {
|
|
112
|
+
player?.destroy();
|
|
113
|
+
player = null;
|
|
114
|
+
for (const k in saved) {
|
|
115
|
+
if (saved[k] === void 0) delete g[k];
|
|
116
|
+
else g[k] = saved[k];
|
|
117
|
+
}
|
|
118
|
+
if (fontDir) try {
|
|
119
|
+
rmSync(fontDir, { recursive: true, force: true });
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
107
122
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function applyParams(doc, params) {
|
|
126
|
+
const entries = Object.entries(params);
|
|
127
|
+
if (!entries.length) return doc;
|
|
128
|
+
const exposed = /* @__PURE__ */ new Map();
|
|
129
|
+
for (const sym of doc.symbols) {
|
|
130
|
+
exposed.set(sym.id, /* @__PURE__ */ new Set([...(sym.states ?? []).map((st) => st.param), ...(sym.params ?? []).map((p) => p.name)]));
|
|
131
|
+
}
|
|
132
|
+
const out = structuredClone(doc);
|
|
133
|
+
const applied = /* @__PURE__ */ new Set();
|
|
134
|
+
const visit = (items) => {
|
|
135
|
+
for (const it of items) {
|
|
136
|
+
if (isInstance(it)) {
|
|
137
|
+
const known = exposed.get(it.symbolId);
|
|
138
|
+
if (known) {
|
|
139
|
+
for (const [k, v] of entries) {
|
|
140
|
+
if (!known.has(k)) continue;
|
|
141
|
+
(it.params ??= {})[k] = v;
|
|
142
|
+
applied.add(k);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (isGroup(it)) for (const l of it.layers) visit(l.items);
|
|
111
147
|
}
|
|
148
|
+
};
|
|
149
|
+
for (const l of out.layers) visit(l.items);
|
|
150
|
+
for (const sym of out.symbols) for (const l of sym.layers) visit(l.items);
|
|
151
|
+
for (const [k] of entries) {
|
|
152
|
+
if (!applied.has(k)) process.stderr.write(`flatc: no symbol exposes a param named "${k}"
|
|
153
|
+
`);
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
async function renderDocToPng(doc, opts = {}) {
|
|
158
|
+
const renderer = await createRenderer(doc, { scale: opts.scale, params: opts.params });
|
|
159
|
+
try {
|
|
160
|
+
return await renderer.frame(opts.frame ?? 0, { vars: opts.vars, steps: opts.steps });
|
|
161
|
+
} finally {
|
|
162
|
+
renderer.close();
|
|
112
163
|
}
|
|
113
164
|
}
|
|
114
165
|
export {
|
|
166
|
+
createRenderer,
|
|
115
167
|
renderDocToPng
|
|
116
168
|
};
|
|
117
169
|
//# sourceMappingURL=render.js.map
|
package/dist/cli/render.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/render.ts"],"sourcesContent":["// -----------------------------------------------------------------------------\n// render.ts -- HEADLESS PNG rendering of a Doc (skia-canvas backend), for `flatc --render`.\n//\n// Gives agents/CI an IMAGE of what they author (the \"blind positioning\" safeguard). We replay the\n// real `FlatPlayer` on a skia canvas: same paths, gradients, filters (glow/shadow via `ctx.filter`)\n// and SVG as the browser. skia-canvas is loaded through a DYNAMIC import with a NON-LITERAL specifier,\n// so it is never part of the public build graph -- install it on demand only when `--render` is used.\n// -----------------------------------------------------------------------------\nimport { FlatPlayer } from '@flatkit/player'\nimport type { Doc } from '@flatkit/types'\nimport { mkdtempSync, writeFileSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport type RenderOpts = { frame?: number; vars?: Record<string, number>; scale?: number; steps?: number }\n\n/** Cap on `--steps` (anti-DoS: an untrusted doc must not freeze the render host). One step = 1/60 s of sim. */\nconst MAX_RENDER_STEPS = 10_000\n\n/** Minimal structural view of the `skia-canvas` surface we use. Kept local so the public build needs\n * no native binary; the real module is resolved at runtime only when `--render` runs. */\ninterface SkiaCanvas {\n Canvas: new (w: number, h: number) => HTMLCanvasElement & { toBuffer(fmt: string): Promise<Uint8Array> }\n loadImage: (src: string) => Promise<unknown>\n Path2D: typeof Path2D\n /** Browser global under Node: the mask/clip path builder does `new DOMMatrix([...])` (drawScene). */\n DOMMatrix: typeof DOMMatrix\n /** Global font registry. `use(paths)` reads each file's intrinsic name-table family; the 2-arg\n * `use(family, paths)` form FORCES that alias (fixes variable-font statics whose name table lies). */\n FontLibrary: {\n use(fontPaths: readonly string[]): Array<{ family: string }>\n use(family: string, fontPaths: readonly string[]): Array<{ family: string }>\n }\n}\n\n/** Map a font asset's MIME (or its data-URI) to a file extension skia-canvas can parse. */\nconst FONT_EXT: Record<string, string> = {\n 'font/woff2': '.woff2', 'font/woff': '.woff', 'font/ttf': '.ttf', 'font/otf': '.otf',\n 'font/collection': '.ttc', 'application/font-woff': '.woff', 'application/x-font-ttf': '.ttf',\n}\n\n/** Registers every embedded `font` asset with skia's FontLibrary so headless text uses the authored\n * faces (not host fallbacks). Fonts are baked as base64 data-URIs by flatc; FontLibrary only reads\n * FILE paths, so we materialize each into a temp dir, register it, and return that dir for cleanup. */\nfunction registerFonts(doc: Doc, FontLibrary: SkiaCanvas['FontLibrary']): { dir: string | null; families: string[] } {\n const fonts = (doc.assets ?? []).filter((a) => a.kind === 'font' && /^data:/.test(a.data ?? ''))\n if (fonts.length === 0) return { dir: null, families: [] }\n const dir = mkdtempSync(join(tmpdir(), 'flatc-fonts-'))\n const families: string[] = []\n for (const a of fonts) {\n const b64 = a.data.slice(a.data.indexOf(',') + 1)\n const ext = FONT_EXT[a.mime] ?? `.${(a.mime.split('/')[1] || 'ttf').replace(/[^\\w]/g, '')}`\n const file = join(dir, a.id.replace(/[^\\w.-]/g, '_') + ext)\n try {\n writeFileSync(file, Buffer.from(b64, 'base64'))\n // `family` alias (from `asset … font \"Name\"`) forces the registered name, so the face matches the\n // text's `font \"Name\"` even when the file's own name table is wrong (variable-font static export).\n const used = a.family ? FontLibrary.use(a.family, [file]) : FontLibrary.use([file])\n for (const f of used) if (!families.includes(f.family)) families.push(f.family)\n } catch (e) {\n process.stderr.write(`flatc: skipped font asset \"${a.id}\" (${a.mime}): ${(e as Error).message}\\n`)\n }\n }\n return { dir, families }\n}\n\n/** Render `doc` to a PNG (Buffer). `frame` = target image; `vars` = state override (`--at`); `scale` = x-px (default 2). */\nexport async function renderDocToPng(doc: Doc, opts: RenderOpts = {}): Promise<Uint8Array> {\n // Non-literal specifier: tsc does not resolve it, so skia-canvas is not a build dependency.\n const skiaPkg: string = 'skia-canvas'\n let skia: SkiaCanvas\n try { skia = (await import(skiaPkg)) as unknown as SkiaCanvas }\n catch {\n throw new Error(\n 'skia-canvas is required for --render. Install it as a dev dependency: `npm i -D skia-canvas` ' +\n '(pnpm users: also allow its build script with `pnpm approve-builds` or add \"skia-canvas\" to ' +\n 'pnpm.onlyBuiltDependencies, then reinstall). If the native binary is still missing afterwards, ' +\n 'run `node node_modules/skia-canvas/lib/prebuild.mjs download`.',\n )\n }\n const { Canvas, loadImage, Path2D, FontLibrary, DOMMatrix } = skia\n const scale = opts.scale && opts.scale > 0 ? opts.scale : 2\n const W = doc.width, H = doc.height\n\n // Register embedded fonts up front so the player's `ctx.font` resolves to the authored faces.\n const { dir: fontDir, families } = registerFonts(doc, FontLibrary)\n if (families.length) process.stderr.write(`flatc: registered ${families.length} font famil${families.length > 1 ? 'ies' : 'y'}: ${families.join(', ')}\\n`)\n\n // Pre-decode the image assets (skia reads SVG/PNG/... from the data-URIs embedded by flatc).\n const images = new Map<string, CanvasImageSource>()\n for (const a of doc.assets ?? []) {\n if (a.kind === 'svg' || a.kind === 'image' || /^data:image\\//.test(a.data ?? '')) {\n try { images.set(a.id, (await loadImage(a.data)) as unknown as CanvasImageSource) } catch { /* unreadable asset: ignored (placeholder) */ }\n }\n }\n\n // Globals the player/rendering expects under Node (restored on exit).\n const g = globalThis as Record<string, unknown>\n const saved: Record<string, unknown> = {}\n const set = (k: string, v: unknown) => { saved[k] = g[k]; g[k] = v }\n set('Path2D', Path2D)\n set('DOMMatrix', DOMMatrix) // mask/clip layers build their clip path with `new DOMMatrix([...])` (no browser global under Node)\n set('window', { addEventListener() {}, removeEventListener() {}, devicePixelRatio: scale })\n set('addEventListener', () => {})\n set('removeEventListener', () => {})\n set('requestAnimationFrame', () => 0)\n set('cancelAnimationFrame', () => {})\n set('document', { createElement: (t: string) => (t === 'canvas' ? new Canvas(1, 1) : {}) }) // off-screen canvas (filters/tint)\n\n const canvas = new Canvas(Math.max(1, Math.round(W * scale)), Math.max(1, Math.round(H * scale)))\n const el = canvas as unknown as HTMLCanvasElement & { toBuffer(fmt: string): Promise<Uint8Array> }\n Object.assign(el, {\n getBoundingClientRect: () => ({ width: W, height: H, left: 0, top: 0, right: W, bottom: H }),\n addEventListener: () => {}, removeEventListener: () => {}, style: {},\n })\n\n try {\n const pl = new FlatPlayer(el, doc, { input: false, audio: false, padding: 0, image: (id) => images.get(id) ?? null })\n if (opts.vars) for (const [k, v] of Object.entries(opts.vars)) pl.setVar(k, v)\n pl.seek(opts.frame ?? 0) // applies frame + state\n if (opts.steps && opts.steps > 0) {\n const n = Math.min(Math.floor(opts.steps), MAX_RENDER_STEPS)\n if (opts.steps > MAX_RENDER_STEPS) process.stderr.write(`flatc: --steps clamped to ${MAX_RENDER_STEPS} (was ${opts.steps})\\n`)\n pl.stepSim(n) // run N fixed sim steps (onEnterFrame) so a stateful act unfolds before capture\n }\n const png = await el.toBuffer('png')\n pl.destroy()\n return png\n } finally {\n for (const k in saved) { if (saved[k] === undefined) delete g[k]; else g[k] = saved[k] }\n // skia keeps the parsed faces in memory after `use()`, so the temp files are safe to drop now.\n if (fontDir) try { rmSync(fontDir, { recursive: true, force: true }) } catch { /* best-effort cleanup */ }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/render.ts"],"sourcesContent":["// -----------------------------------------------------------------------------\n// render.ts -- HEADLESS PNG rendering of a Doc (skia-canvas backend), for `flatc --render`.\n//\n// Gives agents/CI an IMAGE of what they author (the \"blind positioning\" safeguard). We replay the\n// real `FlatPlayer` on a skia canvas: same paths, gradients, filters (glow/shadow via `ctx.filter`)\n// and SVG as the browser. skia-canvas is loaded through a DYNAMIC import with a NON-LITERAL specifier,\n// so it is never part of the public build graph -- install it on demand only when `--render` is used.\n// -----------------------------------------------------------------------------\nimport { FlatPlayer } from '@flatkit/player'\nimport type { Doc, Item } from '@flatkit/types'\nimport { isGroup, isInstance } from '@flatkit/engine/layers'\nimport { mkdtempSync, writeFileSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport type RenderOpts = { frame?: number; vars?: Record<string, number>; scale?: number; steps?: number; params?: Record<string, string> }\n\n/** Cap on `--steps` (anti-DoS: an untrusted doc must not freeze the render host). One step = 1/60 s of sim. */\nconst MAX_RENDER_STEPS = 10_000\n\n/** Minimal structural view of the `skia-canvas` surface we use. Kept local so the public build needs\n * no native binary; the real module is resolved at runtime only when `--render` runs. */\ninterface SkiaCanvas {\n Canvas: new (w: number, h: number) => HTMLCanvasElement & { toBuffer(fmt: string): Promise<Uint8Array> }\n loadImage: (src: string) => Promise<unknown>\n Path2D: typeof Path2D\n /** Browser global under Node: the mask/clip path builder does `new DOMMatrix([...])` (drawScene). */\n DOMMatrix: typeof DOMMatrix\n /** Global font registry. `use(paths)` reads each file's intrinsic name-table family; the 2-arg\n * `use(family, paths)` form FORCES that alias (fixes variable-font statics whose name table lies). */\n FontLibrary: {\n use(fontPaths: readonly string[]): Array<{ family: string }>\n use(family: string, fontPaths: readonly string[]): Array<{ family: string }>\n }\n}\n\n/** Map a font asset's MIME (or its data-URI) to a file extension skia-canvas can parse. */\nconst FONT_EXT: Record<string, string> = {\n 'font/woff2': '.woff2', 'font/woff': '.woff', 'font/ttf': '.ttf', 'font/otf': '.otf',\n 'font/collection': '.ttc', 'application/font-woff': '.woff', 'application/x-font-ttf': '.ttf',\n}\n\n/** Registers every embedded `font` asset with skia's FontLibrary so headless text uses the authored\n * faces (not host fallbacks). Fonts are baked as base64 data-URIs by flatc; FontLibrary only reads\n * FILE paths, so we materialize each into a temp dir, register it, and return that dir for cleanup. */\nfunction registerFonts(doc: Doc, FontLibrary: SkiaCanvas['FontLibrary']): { dir: string | null; families: string[] } {\n const fonts = (doc.assets ?? []).filter((a) => a.kind === 'font' && /^data:/.test(a.data ?? ''))\n if (fonts.length === 0) return { dir: null, families: [] }\n const dir = mkdtempSync(join(tmpdir(), 'flatc-fonts-'))\n const families: string[] = []\n for (const a of fonts) {\n const b64 = a.data.slice(a.data.indexOf(',') + 1)\n const ext = FONT_EXT[a.mime] ?? `.${(a.mime.split('/')[1] || 'ttf').replace(/[^\\w]/g, '')}`\n const file = join(dir, a.id.replace(/[^\\w.-]/g, '_') + ext)\n try {\n writeFileSync(file, Buffer.from(b64, 'base64'))\n // `family` alias (from `asset … font \"Name\"`) forces the registered name, so the face matches the\n // text's `font \"Name\"` even when the file's own name table is wrong (variable-font static export).\n const used = a.family ? FontLibrary.use(a.family, [file]) : FontLibrary.use([file])\n for (const f of used) if (!families.includes(f.family)) families.push(f.family)\n } catch (e) {\n process.stderr.write(`flatc: skipped font asset \"${a.id}\" (${a.mime}): ${(e as Error).message}\\n`)\n }\n }\n return { dir, families }\n}\n\n/**\n * A renderer held OPEN over a document: the expensive setup is paid once, then any number of frames.\n *\n * Every consumer that needed more than one image — a GIF, an MP4, a contact sheet, a loop check —\n * reimplemented this on top of the player, because the only thing on offer rendered a single frame and\n * paid the whole cost each time: the dynamic `skia-canvas` import, writing the embedded fonts to a temp\n * dir and registering them, decoding every image asset, building a `FlatPlayer`, installing ten globals.\n * Four such harnesses exist across two neighbouring repos, ~430 lines, each rediscovering the same\n * shims — including that `document` must exist or every `filter` and `tint` is dropped in SILENCE.\n */\nexport type Renderer = {\n /** PNG of one frame. `vars` overrides state for this frame; `steps` runs N sim steps before capture. */\n frame(frame: number, opts?: { vars?: Record<string, number>; steps?: number }): Promise<Uint8Array>\n /** The document's pixel size at the renderer's scale. */\n readonly width: number\n readonly height: number\n /** Restore the globals and drop the temp font files. Always call it — it is not optional. */\n close(): void\n}\n\n/**\n * Open a renderer over `doc`. The globals it installs (`document`, `Path2D`, `DOMMatrix`, …) stay in\n * place until `close()`, which is what makes a frame loop cheap; it also means one renderer at a time\n * per process. `params` sets a SYMBOL's exposed params (a state name or a number) before the first\n * frame — the reason a consumer had to write its own harness to preview anything with a state.\n */\nexport async function createRenderer(doc: Doc, opts: { scale?: number; params?: Record<string, string> } = {}): Promise<Renderer> {\n // Non-literal specifier: tsc does not resolve it, so skia-canvas is not a build dependency.\n const skiaPkg: string = 'skia-canvas'\n let skia: SkiaCanvas\n try { skia = (await import(skiaPkg)) as unknown as SkiaCanvas }\n catch {\n throw new Error(\n 'skia-canvas is required for rendering. Install it as a dev dependency: `npm i -D skia-canvas` ' +\n '(pnpm users: also allow its build script with `pnpm approve-builds` or add \"skia-canvas\" to ' +\n 'pnpm.onlyBuiltDependencies, then reinstall). If the native binary is still missing afterwards, ' +\n 'run `node node_modules/skia-canvas/lib/prebuild.mjs download`.',\n )\n }\n const { Canvas, loadImage, Path2D, FontLibrary, DOMMatrix } = skia\n const scale = opts.scale && opts.scale > 0 ? opts.scale : 2\n const W = doc.width, H = doc.height\n\n const withParams = opts.params ? applyParams(doc, opts.params) : doc\n\n // Register embedded fonts up front so the player's `ctx.font` resolves to the authored faces.\n const { dir: fontDir, families } = registerFonts(withParams, FontLibrary)\n if (families.length) process.stderr.write(`flatc: registered ${families.length} font famil${families.length > 1 ? 'ies' : 'y'}: ${families.join(', ')}\\n`)\n\n // Pre-decode the image assets (skia reads SVG/PNG/... from the data-URIs embedded by flatc).\n const images = new Map<string, CanvasImageSource>()\n for (const a of withParams.assets ?? []) {\n if (a.kind === 'svg' || a.kind === 'image' || /^data:image\\//.test(a.data ?? '')) {\n try { images.set(a.id, (await loadImage(a.data)) as unknown as CanvasImageSource) } catch { /* unreadable asset: ignored (placeholder) */ }\n }\n }\n\n // Globals the player/rendering expects under Node (restored by close()).\n const g = globalThis as Record<string, unknown>\n const saved: Record<string, unknown> = {}\n const set = (k: string, v: unknown) => { saved[k] = g[k]; g[k] = v }\n set('Path2D', Path2D)\n set('DOMMatrix', DOMMatrix) // mask/clip layers build their clip path with `new DOMMatrix([...])` (no browser global under Node)\n set('window', { addEventListener() {}, removeEventListener() {}, devicePixelRatio: scale })\n set('addEventListener', () => {})\n set('removeEventListener', () => {})\n set('requestAnimationFrame', () => 0)\n set('cancelAnimationFrame', () => {})\n set('document', { createElement: (t: string) => (t === 'canvas' ? new Canvas(1, 1) : {}) }) // off-screen canvas (filters/tint)\n\n const pxW = Math.max(1, Math.round(W * scale))\n const pxH = Math.max(1, Math.round(H * scale))\n const canvas = new Canvas(pxW, pxH)\n const el = canvas as unknown as HTMLCanvasElement & { toBuffer(fmt: string): Promise<Uint8Array> }\n Object.assign(el, {\n getBoundingClientRect: () => ({ width: W, height: H, left: 0, top: 0, right: W, bottom: H }),\n addEventListener: () => {}, removeEventListener: () => {}, style: {},\n })\n\n let player: FlatPlayer | null = new FlatPlayer(el, withParams, { input: false, audio: false, padding: 0, image: (id) => images.get(id) ?? null })\n\n return {\n width: pxW,\n height: pxH,\n async frame(frame, frameOpts = {}) {\n if (!player) throw new Error('renderer is closed')\n if (frameOpts.vars) for (const [k, v] of Object.entries(frameOpts.vars)) player.setVar(k, v)\n player.seek(frame) // applies frame + state\n const steps = frameOpts.steps ?? 0\n if (steps > 0) {\n const n = Math.min(Math.floor(steps), MAX_RENDER_STEPS)\n if (steps > MAX_RENDER_STEPS) process.stderr.write(`flatc: steps clamped to ${MAX_RENDER_STEPS} (was ${steps})\\n`)\n player.stepSim(n) // run N fixed sim steps (onEnterFrame) so a stateful act unfolds before capture\n }\n return el.toBuffer('png')\n },\n close() {\n player?.destroy()\n player = null\n for (const k in saved) { if (saved[k] === undefined) delete g[k]; else g[k] = saved[k] }\n // skia keeps the parsed faces in memory after `use()`, so the temp files are safe to drop now.\n if (fontDir) try { rmSync(fontDir, { recursive: true, force: true }) } catch { /* best-effort cleanup */ }\n },\n }\n}\n\n/**\n * A copy of `doc` with the scene's instances carrying `params` — a state NAME or a number, the same\n * spelling `--preview --set` accepts, resolved by the renderer per the symbol's ParamDef / state\n * machines. Rendering a program could only override document `var`s, so previewing a door in its `open`\n * state meant writing a harness that mutated the symbol by hand. That was one of four such harnesses.\n *\n * Set on the INSTANCE, not on the symbol's defaults: that is where a call-site value belongs, and it is\n * what `--preview` already does.\n */\nfunction applyParams(doc: Doc, params: Record<string, string>): Doc {\n const entries = Object.entries(params)\n if (!entries.length) return doc\n // Which names each symbol exposes: its typed params, plus the param each state machine drives.\n const exposed = new Map<string, Set<string>>()\n for (const sym of doc.symbols) {\n exposed.set(sym.id, new Set([...(sym.states ?? []).map((st) => st.param), ...(sym.params ?? []).map((p) => p.name)]))\n }\n const out = structuredClone(doc)\n const applied = new Set<string>()\n const visit = (items: Item[]): void => {\n for (const it of items) {\n if (isInstance(it)) {\n const known = exposed.get(it.symbolId)\n if (known) {\n for (const [k, v] of entries) {\n if (!known.has(k)) continue\n ;(it.params ??= {})[k] = v\n applied.add(k)\n }\n }\n }\n if (isGroup(it)) for (const l of it.layers) visit(l.items)\n }\n }\n for (const l of out.layers) visit(l.items)\n for (const sym of out.symbols) for (const l of sym.layers) visit(l.items)\n for (const [k] of entries) {\n if (!applied.has(k)) process.stderr.write(`flatc: no symbol exposes a param named \"${k}\"\\n`)\n }\n return out\n}\n\n/** Render `doc` to a PNG (Buffer). `frame` = target image; `vars` = state override; `scale` = x-px (default 2).\n * A one-shot convenience over `createRenderer` — reach for the renderer itself as soon as you want two. */\nexport async function renderDocToPng(doc: Doc, opts: RenderOpts = {}): Promise<Uint8Array> {\n const renderer = await createRenderer(doc, { scale: opts.scale, params: opts.params })\n try {\n return await renderer.frame(opts.frame ?? 0, { vars: opts.vars, steps: opts.steps })\n } finally {\n renderer.close()\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
run
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-S63MOX6G.js";
|
|
4
4
|
import {
|
|
5
5
|
allScopeVariables,
|
|
6
6
|
checkProgram,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
scopeProgram,
|
|
24
24
|
scopeRegions,
|
|
25
25
|
splitScopeProgram
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-IYPM3CU5.js";
|
|
27
27
|
import {
|
|
28
28
|
compileFlatpack,
|
|
29
29
|
packToJSON
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# FlatInk DSL — documentation
|
|
2
|
+
|
|
3
|
+
FlatInk is a small text language for **animations and interactive scenes** that compile to a single
|
|
4
|
+
self-contained `.flatpack` and play in any `<canvas>`. You can write it by hand, generate it from a
|
|
5
|
+
script or an LLM, or export it from the [FlatInk editor](http://flatink.zwyk-studio.com/).
|
|
6
|
+
|
|
7
|
+
## The mental model
|
|
8
|
+
|
|
9
|
+
A `.flatink` file has **two halves**:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
size 480 320
|
|
13
|
+
|
|
14
|
+
scene { ← THE SCENE: what you see (shapes, text, images, groups)
|
|
15
|
+
layer "game" {
|
|
16
|
+
group "Star" at 240,160 { layer "art" { circle 0 0 40 fill #ffcc00 } }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
object "Star" { ← THE BEHAVIOR: how it moves and reacts
|
|
21
|
+
when clicked { score = score + 1 }
|
|
22
|
+
rotation = clock * 30
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> Behavior attaches **by name**, and the name must be a **group** (or instance/text/image) — those alone
|
|
27
|
+
> carry a pose. A bare `circle` can be named, but never animated. `clock` is the monotone second count;
|
|
28
|
+
> `time` restarts on every timeline loop.
|
|
29
|
+
|
|
30
|
+
The **scene** is composition; the **behavior** (everything after `scene { … }`) is logic — events,
|
|
31
|
+
expressions, drag/drop, interactors. They don't share the same grammar.
|
|
32
|
+
|
|
33
|
+
## Guides
|
|
34
|
+
|
|
35
|
+
Read in order if you're new, or jump to a topic:
|
|
36
|
+
|
|
37
|
+
1. **[Getting started](getting-started.md)** — your first `.flatink`, compiled and played.
|
|
38
|
+
2. **[Scene & drawing](scene-and-drawing.md)** — layers, shapes, text, images, paints, filters, transforms.
|
|
39
|
+
3. **[Animating a symbol](animating-symbols.md)** — the timeline/cel/pose model, `rotate`/`scale`/`spin`, pivots, tweens, easing.
|
|
40
|
+
4. **[Behavior & interactions](behavior-and-interactions.md)** — events, actions, drag/drop, interactors, feedback.
|
|
41
|
+
5. **[Expressions & stdlib](expressions-and-stdlib.md)** — the expression language, math, `self`/`mouse`/`time`, and the `use "…"` packages.
|
|
42
|
+
6. **[Tooling](tooling.md)** — the `flatc` CLI: compile, render, headless play, gesture scripts, CI.
|
|
43
|
+
7. **[Host integration](host-integration.md)** — mounting `@flatkit/player` in an app: receive `send` events, drive the state variables, keyboard and teardown.
|
|
44
|
+
8. **[Embedding fonts](embedding-fonts.md)** — register the doc's embedded fonts (browser `FontFace` / skia `FontLibrary`) so text uses the authored faces.
|
|
45
|
+
|
|
46
|
+
**Appendix — [Gotchas & best practices](dsl-gotchas.md)**: hard-won pitfalls from real production. Skim
|
|
47
|
+
it once you know the basics.
|
|
48
|
+
|
|
49
|
+
## Two layers, one format
|
|
50
|
+
|
|
51
|
+
- **Layer A — composition & animation**: the timeline, tweens, motion paths, the scene tree.
|
|
52
|
+
- **Layer B — interaction & state**: `var`s, events, expressions, the declarative interactors.
|
|
53
|
+
|
|
54
|
+
You can use just Layer A (a pure animation) or both (an interactive activity). Either way the output is
|
|
55
|
+
one `.flatpack` that `@flatkit/player` plays.
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# Animating a symbol (`.flat`)
|
|
2
|
+
|
|
3
|
+
> How a `.flat` symbol moves over time: the **timeline / cel / pose** model (Flash-style), the `pose`
|
|
4
|
+
> keywords (`rotate`, `scale`, `opacity`, `spin`…), pivots, tweens, and how to preview the result.
|
|
5
|
+
> If you only need static composition, see [Scene & drawing](scene-and-drawing.md).
|
|
6
|
+
|
|
7
|
+
## The model in one paragraph
|
|
8
|
+
|
|
9
|
+
A symbol owns a **timeline** (`timeline <fps> <durationFrames>`). Each animated **layer** is a time
|
|
10
|
+
track: a sequence of **cels** (layer-wide keyframes). A cel lists the **poses** of the containers
|
|
11
|
+
present at that frame (a `pose` per roster item) and, optionally, the **matter** (static drawing) at
|
|
12
|
+
that key. Between two cels the layer either **holds** the last key or **tweens** toward the next one.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
symbol "Wheel" {
|
|
16
|
+
timeline 24 24 ← 24 fps, 24 frames (loops once per second)
|
|
17
|
+
layer "spin" {
|
|
18
|
+
group "Rim" at 100,100 pivot 0,0 { ← the roster: declared ONCE, posed by the cels below
|
|
19
|
+
layer "art" { circle 0 0 40 nofill stroke #333 8 }
|
|
20
|
+
}
|
|
21
|
+
cel 0 tween { pose "Rim" rotate 0 }
|
|
22
|
+
cel 24 { pose "Rim" rotate 360 } ← one full turn around the pivot, in DEGREES
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Preview it without authoring a wrapper:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
flatc --preview Wheel.flat --render -o wheel.png # a PNG (frame 0)
|
|
31
|
+
flatc --preview Wheel.flat -o wheel.flatpack # a playable .flatpack for the browser player
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## `pose` — the keyframe of a container
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pose "Name" [at <x>,<y>] [rotate <deg>] [scale <s> | scaleX <sx> scaleY <sy>]
|
|
38
|
+
[opacity <o>] [tint <#color> <amount>] [spin cw|ccw] [turns <n>] [filter …]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **`rotate <deg>` and `scale`/`scaleX`/`scaleY` are in human units** (degrees, multipliers) and apply
|
|
42
|
+
**around the group's `pivot`** — no matrices, no radians, no trigonometry. `rotate 90` is a quarter
|
|
43
|
+
turn; `scale 2` is double size.
|
|
44
|
+
- **`at <x>,<y>`** places the container's local **origin** in parent space. `rotate`/`scale` then turn
|
|
45
|
+
and scale around the `pivot` point (see below), keeping it anchored.
|
|
46
|
+
- **`matrix(a,b,c,d,e,f)`** is still accepted as an escape hatch, but you almost never need it.
|
|
47
|
+
|
|
48
|
+
### Patch semantics — a pose only overrides what it states
|
|
49
|
+
|
|
50
|
+
A pose **inherits** every channel it does not mention from the container's resting pose (its declaration
|
|
51
|
+
in the roster) — position, rotation, scale, opacity, tint, filters. So:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
pose "Boat" opacity 0.5 ← keeps the Boat's declared position/rotation/scale; only dims it
|
|
55
|
+
pose "Boat" rotate 3 ← keeps its position and scale; only tilts it 3°
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You do **not** re-state `at x,y` in every cel just to change opacity. (This is a change from older
|
|
59
|
+
builds where a partial pose snapped to `0,0`.)
|
|
60
|
+
|
|
61
|
+
## Pivot vs `at` — where things turn
|
|
62
|
+
|
|
63
|
+
- **`pivot <x>,<y>`** (set on the container in the roster, in its **local** coordinates) is the center
|
|
64
|
+
of rotation **and** scale **and** tween interpolation. Default is the local origin `0,0`.
|
|
65
|
+
- **`at <x>,<y>`** (on the pose) is where the local origin lands in the parent.
|
|
66
|
+
|
|
67
|
+
**Rule of thumb:** set the group's `pivot` to its visual center, then `rotate`/`scale`/`spin` turn it in
|
|
68
|
+
place. A wheel whose art is centered on its local origin needs no pivot; a wheel drawn off-origin must
|
|
69
|
+
set `pivot` to its hub, or it will **orbit** instead of spin.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
group "Hand" at 200,200 pivot 0,-60 { ← pivot at the clock center, 60px below the hand's tip
|
|
73
|
+
layer "art" { rect -4 -60 8 60 fill #111 }
|
|
74
|
+
}
|
|
75
|
+
…
|
|
76
|
+
cel 0 tween { pose "Hand" rotate 0 }
|
|
77
|
+
cel 60 { pose "Hand" rotate 360 } ← sweeps around the pivot, not its own middle
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Tweens, easing, spin
|
|
81
|
+
|
|
82
|
+
- **`cel N tween { … }`** interpolates this cel → the next for every container present in both. Without
|
|
83
|
+
`tween`, the cel **holds** until the next key.
|
|
84
|
+
- **`ease <curve>`** on the cel: `linear` · `easeIn` · `easeOut` · `easeInOut` · `cubic(a,b,c,d)`.
|
|
85
|
+
- **`spin cw|ccw`** + **`turns <n>`** force the rotation **direction** and add full turns across the
|
|
86
|
+
tween, so a 350° → 10° move can go the short way (`ccw`) or wind several times (`turns 2`). The spin
|
|
87
|
+
is **around the pivot**, like every other rotation.
|
|
88
|
+
- **`morph`** on a cel tweens the *shape* of the `matter` (drawing) toward the next key.
|
|
89
|
+
|
|
90
|
+
## Presence across cels — a cel is a full snapshot
|
|
91
|
+
|
|
92
|
+
Each `cel` is a **keyframe = the full set of containers present at that instant** (Flash style). A container
|
|
93
|
+
is shown only on the cels that **pose** it; one omitted from a cel **disappears** there. So a container
|
|
94
|
+
visible across a span must be posed on **each cel of that span** (it's per-*keyframe*, not per-frame — three
|
|
95
|
+
keyframes ⇒ three poses, not one per frame). This is also how a symbol **exits**: stop posing it.
|
|
96
|
+
|
|
97
|
+
Two ways to avoid re-typing an unchanged container:
|
|
98
|
+
|
|
99
|
+
- **Static element → its own layer WITHOUT cels.** A cel-less layer renders its items at every frame, so a
|
|
100
|
+
static base/background is declared **once** and never flickers. (Same idea as the render-order note below.)
|
|
101
|
+
- **`cel N hold { … }`** — carry the previous cel's poses forward for every container this cel does *not*
|
|
102
|
+
mention, then apply the stated overrides. Pure authoring sugar (the compiler expands it to full cels), so
|
|
103
|
+
you only write what changes:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
cel 0 tween { pose "Base" at 0,0 pose "Ring" scale 1 }
|
|
107
|
+
cel 30 hold tween { pose "Ring" scale 4 } # Base carried automatically
|
|
108
|
+
cel 60 hold { pose "Ring" scale 1 }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`hold` is opt-in per cel; without it the default (an omitted container is removed) is unchanged — so
|
|
112
|
+
exits still work.
|
|
113
|
+
|
|
114
|
+
## Frame-by-frame — a different DRAWING on each cel
|
|
115
|
+
|
|
116
|
+
A cel carries the poses **and** the layer's **matter**: the drawing at that key, written as a
|
|
117
|
+
`matter { … }` block. Change it from cel to cel and you get classic cel animation — one drawing per
|
|
118
|
+
frame, no tween:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
symbol "Blink" {
|
|
122
|
+
timeline 12 3
|
|
123
|
+
layer "draw" {
|
|
124
|
+
cel 0 { matter { circle 0 0 30 fill #e33 } }
|
|
125
|
+
cel 1 { matter { rect -30 -30 60 60 fill #3a3 } }
|
|
126
|
+
cel 2 { matter { path "M -30 30 L 0 -30 L 30 30 Z" fill #33e } }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- The matter is **held** until the next cel that defines one — a drawing kept over several frames is
|
|
132
|
+
written **once**, on the cel where it appears. An explicit `matter { }` (empty) blanks it.
|
|
133
|
+
- **`morph`** on the cel interpolates the *shape* toward the next cel's matter instead of cutting.
|
|
134
|
+
- A cel can carry **both**: a `matter { … }` for that frame's drawing *plus* `pose "…"` for the
|
|
135
|
+
containers animated on top of it (matter always draws **behind** the posed containers).
|
|
136
|
+
|
|
137
|
+
Two other ways to author the same thing, when the drawings already exist as objects:
|
|
138
|
+
|
|
139
|
+
- **Swap containers** — declare each drawing as a group in the roster, then pose only the one you want on
|
|
140
|
+
each cel (a container omitted from a cel disappears — see above).
|
|
141
|
+
- **Image sequence** — the same idiom with `image` items: `image "f1" 100 100 as "F1" at -50,-50` in the
|
|
142
|
+
roster, `cel 0 { pose "F1" }`. The media is declared in the `.flatink` (`asset "f1" "f1.png" image`).
|
|
143
|
+
|
|
144
|
+
> **The trap:** in a layer that **has cels**, a bare shape written directly in the layer is **never
|
|
145
|
+
> drawn** — such a layer renders the current cel's `matter` plus the containers that cel poses, and
|
|
146
|
+
> nothing else. Put the shape inside a `matter { … }`, or on a **cel-less layer** if it is static decor.
|
|
147
|
+
> `flatc --check` warns about it, and about the two sibling silent drops: a `pose "X"` naming no roster
|
|
148
|
+
> item, and a roster item that no cel ever poses.
|
|
149
|
+
|
|
150
|
+
## Driving a channel with an expression (`expr`)
|
|
151
|
+
|
|
152
|
+
Instead of keyframes you can bind a channel to an expression on the container itself:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
group "Fan" pivot 0,0 expr rotation "turns(time)" { … } ← one turn per second
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- The animatable channels are `x`, `y`, `scaleX`, `scaleY`, `rotation`, `opacity`.
|
|
159
|
+
- **`rotation` is in RADIANS** (like `sin`/`cos`/`atan2`). Use the helpers to stay in degrees:
|
|
160
|
+
- `rad(deg)` → radians, e.g. `expr rotation "rad(45)"`
|
|
161
|
+
- `turns(n)` → `n` full turns in radians, e.g. `expr rotation "turns(time)"` or `"turns(time * 0.5)"`
|
|
162
|
+
- `deg(rad)` → the inverse, for readouts.
|
|
163
|
+
|
|
164
|
+
## Stateful "feel": `spring` / `smooth`
|
|
165
|
+
|
|
166
|
+
`expr` is **pure** — it recomputes from `time`/params each frame, with no memory. When you want a channel to
|
|
167
|
+
*react over time* (lag behind a moving target, swing and settle), use a **modifier** instead. It carries
|
|
168
|
+
per-instance state that **integrates** toward a target each frame — so an asset's physical "feel" lives **in
|
|
169
|
+
the asset**, with no scene code:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
group "Suspente" spring rotation "crochetX" stiffness 0.08 damping 0.86 { … } # cable swings, then settles
|
|
173
|
+
group "Aiguille" smooth rotationDeg "valeur * 270" k 0.18 { … } # needle eases to its value
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- `smooth <channel> "<target>" k <0..1>` — 1st-order lag: each step `value += (target − value) * k`. Small
|
|
177
|
+
`k` = slow/heavy; `k = 1` = instant (no lag).
|
|
178
|
+
- `spring <channel> "<target>" stiffness <0..1> damping <0..1>` — 2nd-order spring: overshoots then settles.
|
|
179
|
+
Lower `damping` = more bounce. (Both params are per fixed 60 Hz step; out-of-range values are clamped.)
|
|
180
|
+
- `<target>` is an ordinary expression (params, `time`, `self.x`, …) — the resting value the channel chases.
|
|
181
|
+
Authoring sugar like `expr`: `rotate` = `rotation`; `rotationDeg` reads degrees (wraps the target in `rad()`).
|
|
182
|
+
- A modifier **wins** over a plain `expr` / keyframes on the same channel.
|
|
183
|
+
|
|
184
|
+
**React to MOVEMENT, not value — `velocity(expr)`.** Inside a modifier target (only there), `velocity(x)` is
|
|
185
|
+
the per-second rate of change of `x`. It's **0 at rest** and while scrubbing/rendering, non-zero only while `x`
|
|
186
|
+
is actually moving — perfect for a pendulum on a moving pivot (a crane cable that swings when the trolley moves,
|
|
187
|
+
then hangs vertical again, with no scene code):
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
group "Suspente" spring rotation "rad(-velocity(crochetX) * 40)" stiffness 0.06 damping 0.22 { … }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
At rest `velocity = 0` → target `0` → vertical, automatically; on a scrub/`--render` it's also `0` → snaps to
|
|
194
|
+
rest. Composable in any target (`rad(-velocity(crochetX)*40 + 2*sin(time))`). `velocity()` is **only** valid in a
|
|
195
|
+
modifier target — `flatc --check` flags it elsewhere.
|
|
196
|
+
|
|
197
|
+
**Per instance.** State is keyed per instance, so two cranes side by side swing **independently** (even when
|
|
198
|
+
the spring is on a group *inside* the symbol).
|
|
199
|
+
|
|
200
|
+
**Live vs. static.** The spring animates during **playback** (gallery autoplay, an activity). On **random
|
|
201
|
+
access** — a timeline scrub, `--render`, a contact sheet — there is no time to integrate, so the channel
|
|
202
|
+
**snaps to its target** (the rest pose). So tune a spring by *playing* the preview, not by scrubbing.
|
|
203
|
+
|
|
204
|
+
**Also scene-side.** The same modifier works in a `.flatink` `object` block — the target is then an ordinary
|
|
205
|
+
(unquoted) FlatInk expression — for a one-off spring on a scene object, when the feel isn't baked into a `.flat`:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
object "Hero" {
|
|
209
|
+
spring rotation = crochetX { stiffness 0.08 damping 0.86 }
|
|
210
|
+
smooth opacity = lit { k 0.18 }
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Looping & instancing
|
|
215
|
+
|
|
216
|
+
The timeline loops over `[0, durationFrames)`. An `instance` of a symbol chooses **how its own timeline
|
|
217
|
+
advances** (Flash's symbol-instance models), written after the instance's attributes:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
instance "Walk" as "legs" # synced (default)
|
|
221
|
+
instance "Walk" as "legs" loop # independent (MovieClip)
|
|
222
|
+
instance "Splash" as "fx" once # play once, then hold the last frame
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| mode | clock | behavior |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| *(default)* / `synced` | the parent's frame | **Graphic symbol**: scrubbed and *truncated* by the parent — if an ancestor's timeline is shorter than (or not a multiple of) the sub-loop, it snaps mid-cycle. Best for lip-sync, deterministic scrub. |
|
|
228
|
+
| `loop` (`independent`) | the runtime's monotone clock | **MovieClip**: loops on its *own* duration, immune to any ancestor's loop length. Use for state-loops and idles that must keep their phase across the parent's wrap. |
|
|
229
|
+
| `once` | the monotone clock, clamped | plays through **once**, then **holds** the last frame — a one-shot (a splash, an explosion, a pose that stays). |
|
|
230
|
+
| `singleFrame` | — | frozen on a fixed frame. |
|
|
231
|
+
|
|
232
|
+
A `loop`/`once` instance runs on the global heartbeat, so it never needs its parent padded to a common
|
|
233
|
+
multiple ("LCM") of its sub-loops. In the **editor** it shows frame 0 (MovieClip-style authoring); it plays
|
|
234
|
+
at runtime — edit its keyframes by opening the symbol itself. `--preview` sizes its window to show a nested
|
|
235
|
+
`loop`/`once` looping cleanly, without touching the previewed symbol's own authored duration.
|
|
236
|
+
|
|
237
|
+
## Exposed parameters (`params`)
|
|
238
|
+
|
|
239
|
+
A symbol can publish a small, named **interface** instead of exposing its internals — useful for restyling
|
|
240
|
+
an asset (hull/sail colors), tuning an animation (amplitude, speed), or toggling a detail, including
|
|
241
|
+
"after the fact" by a small model.
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
symbol "Boat" {
|
|
245
|
+
params {
|
|
246
|
+
color hull = #c0392b "Hull color"
|
|
247
|
+
color sail = #2980b9 "Sail color"
|
|
248
|
+
number wave = 1 range 0 2 "Bob amplitude"
|
|
249
|
+
bool flag = true "Show the pennant"
|
|
250
|
+
}
|
|
251
|
+
layer "body" {
|
|
252
|
+
path "…" fill hull // a color param used as a fill
|
|
253
|
+
group "Deck" expr y "sin(clock*3) * wave" { … } // a number param read in an expression
|
|
254
|
+
group "Flag" expr opacity "flag ? 1 : 0" { … } // a bool param as a toggle
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- `params { <type> <name> = <default> [range <min> <max>] ["doc"] … }` — `<type>` is `color`, `number`,
|
|
260
|
+
or `bool`. The default, range, and doc string make the interface self-describing.
|
|
261
|
+
- **`color` params** are used as a paint — `fill hull`, `stroke hull <width>`, a **gradient stop**
|
|
262
|
+
(`0:hull@0.8`, optional `@alpha`), or a **`tint hull <amount>`** (anywhere a `#color` literal goes).
|
|
263
|
+
Resolved per instance at render; *not* available in numeric expressions.
|
|
264
|
+
- **`number` / `bool` params** become **variables in the symbol's expressions** (`wave`, `flag`). `bool`
|
|
265
|
+
reads as `1`/`0`. (`flatc --check` knows them — reading a declared param in an `expr` is not an "unknown
|
|
266
|
+
variable".)
|
|
267
|
+
|
|
268
|
+
> The `timeline`, `params`, and `states` header blocks may appear in **any order** before the layers.
|
|
269
|
+
|
|
270
|
+
Set params at the instance **call-site** (literals), in `--preview`, or — for `number`/`bool` — at
|
|
271
|
+
runtime (`Boat.wave = 1.5`, see below):
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
instance "Boat" as "Hero" at center { hull = #1a5f3a, wave = 1.5, flag = false }
|
|
275
|
+
flatc --preview Boat.flat --render --set hull=#1a5f3a,wave=1.5 -o boat.png
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
> A `state` (below) is just another exposed param — same call-site/preview/runtime surface.
|
|
279
|
+
|
|
280
|
+
## Named states (`states`)
|
|
281
|
+
|
|
282
|
+
A symbol can expose **named states** — points on its own timeline — and let a consumer switch between
|
|
283
|
+
them. A door is the canonical case: the symbol animates from `closed` (frame 0) to `open` (frame 24),
|
|
284
|
+
and exposes that as a single param.
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
symbol "Door" {
|
|
288
|
+
timeline 24 24
|
|
289
|
+
states door { closed at 0 open at 24 initial closed transition 12 ease easeInOut }
|
|
290
|
+
layer "panel" {
|
|
291
|
+
group "Panel" at 60,10 pivot 0,0 { layer "art" { rect 0 0 40 80 fill #884422 } }
|
|
292
|
+
cel 0 tween { pose "Panel" rotate 0 } // closed
|
|
293
|
+
cel 24 { pose "Panel" rotate 80 } // open
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
- `states <param> { <name> at <frame> … }` declares the state machine. `<param>` is the exposed
|
|
299
|
+
variable (`door`); each `<name> at <frame>` anchors a state to a frame of the symbol's timeline.
|
|
300
|
+
- `initial <name>` is the resting state (default: the first). `transition <n> [ease <e>]` is the default
|
|
301
|
+
move between states.
|
|
302
|
+
- **The param drives the symbol's local playhead.** `door = 0` (or `closed`) → frame 0; `door = 1`
|
|
303
|
+
(or `open`) → frame 24; a fractional `door = 0.5` → frame 12, i.e. the authored in-between animation.
|
|
304
|
+
So **animating the variable from 0→1 plays the open animation** — states live inside the ordinary
|
|
305
|
+
variable system, no special runtime.
|
|
306
|
+
|
|
307
|
+
Select a state in a preview (a state name or a number):
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
flatc --preview Door.flat --render --set door=open -o open.png
|
|
311
|
+
flatc --preview Door.flat --render --set door=0.5 -o half.png # mid-transition
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Driving a state from a program (`set Name.param = state`)
|
|
315
|
+
|
|
316
|
+
In a `.flatink` program, address an instance **by name** and set its state — the player plays the
|
|
317
|
+
declared `transition` automatically. Each instance keeps its **own** state, so two doors are independent.
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
scene {
|
|
321
|
+
layer "stage" { instance "Door" as "FrontDoor" at 100,100 }
|
|
322
|
+
}
|
|
323
|
+
object "FrontDoor" {
|
|
324
|
+
when clicked { FrontDoor.door = open } // animates closed → open over `transition` frames
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The right-hand side is a **state name** (`open`) or an expression (`FrontDoor.door = score > 5 ? open : closed`
|
|
329
|
+
isn't valid — names aren't expressions; use a number there, e.g. `… ? 1 : 0`). `transition 0` snaps instantly.
|
|
330
|
+
|
|
331
|
+
> **Scope note:** the state value drives the instance's playhead and is visible to that instance's own
|
|
332
|
+
> expressions. Reading another object's state back by name (`FrontDoor.door` in an unrelated expression)
|
|
333
|
+
> and the broader typed `params {}` interface (colors/numbers/toggles, `fill hull`) are still to come.
|
|
334
|
+
|
|
335
|
+
## Render order (a real caveat)
|
|
336
|
+
|
|
337
|
+
Within **one animated layer**, the **matter (static drawing) always renders behind the posed
|
|
338
|
+
containers** — declaration order between a bare `path` and an animated `group` is **not** preserved,
|
|
339
|
+
because the cel model stores matter and the container roster separately.
|
|
340
|
+
|
|
341
|
+
**If a static shape must sit IN FRONT of an animated group**, give it its own group (so it becomes a
|
|
342
|
+
posed container too) or, simpler, **put it on its own layer** above. Layers always honor their stacking
|
|
343
|
+
order. This is the reliable way to control z-order around animation.
|
|
344
|
+
|
|
345
|
+
## Previewing without clipping
|
|
346
|
+
|
|
347
|
+
`flatc --preview` auto-sizes the stage to the symbol's bounds. By default (`--bbox all`) it measures the
|
|
348
|
+
**union over every frame** (sub-timelines unfrozen), so a part that drifts, rotates, or grows is **never
|
|
349
|
+
clipped**. Use `--bbox frame0` for the old frame-0-only measure, and `--pad N` to add a margin.
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
flatc --preview Boat.flat --render -o boat.png # union bbox (default) — full motion fits
|
|
353
|
+
flatc --preview Boat.flat --bbox frame0 --pad 40 -o b.png # frame-0 bounds + 40px margin
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
See also: [Tooling](tooling.md) for the full `flatc` reference, and
|
|
357
|
+
[Gotchas](dsl-gotchas.md) for sharp edges.
|