@flatkit/compiler 0.38.0 → 0.39.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/dist/cli/render.js +1 -1
- package/dist/cli/render.js.map +1 -1
- package/docs/behavior-and-interactions.md +40 -1
- package/docs/dsl-gotchas.md +5 -0
- package/docs/expressions-and-stdlib.md +34 -1
- package/package.json +4 -4
- package/prompts/flatink-core.md +3 -1
- package/prompts/flatink-lite.md +1 -1
- package/prompts/role-coder.md +1 -1
package/dist/cli/render.js
CHANGED
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, type SendEvent } from '@flatkit/player'\nimport { createReplayer, type Gesture, type Handlers, type Replayer } from '@flatkit/player/debug'\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>; script?: Gesture[]; settle?: number }\n\n/** What a replayed script reports: the `send`s it emitted, and the `expect` gestures that did not hold. */\nexport type PlayReport = { sends: SendEvent[]; expectFailures: string[]; warnings: 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; `script` replays gestures after the seek\n * (an `interactive` renderer only); `steps` then runs N sim steps before capture. */\n frame(frame: number, opts?: { vars?: Record<string, number>; steps?: number; script?: Gesture[] }): Promise<Uint8Array>\n /** Replays gestures on the live scene, as `flatc --play` does, and leaves it in the state they reach.\n * Successive calls continue the same session (one state, one `expect` window). Needs\n * `createRenderer(doc, { interactive: true })`. */\n play(gestures: Gesture[]): PlayReport\n /** PNG of the scene AS IT IS NOW — after a `play`, without seeking. */\n capture(): 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>; interactive?: boolean; settle?: number } = {}): 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 handlers: Handlers = {}\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 // The listeners the player registers are KEPT: a replayed script fires into them (`play`).\n addEventListener: (type: string, fn: Handlers[string]) => { handlers[type] = fn },\n removeEventListener: (type: string) => { delete handlers[type] },\n setPointerCapture: () => {}, releasePointerCapture: () => {}, style: {},\n })\n\n // `interactive`: the player listens to its canvas, so gestures can be replayed on it. Off by default —\n // a renderer that only draws frames registers nothing.\n const sends: SendEvent[] = []\n let player: FlatPlayer | null = new FlatPlayer(el, withParams, { input: !!opts.interactive, audio: false, padding: 0, seed: 1, image: (id) => images.get(id) ?? null, onEvent: (e) => sends.push(e) })\n let replayer: Replayer | null = null\n const play = (gestures: Gesture[]): PlayReport => {\n if (!player) throw new Error('renderer is closed')\n if (!opts.interactive) throw new Error('this renderer cannot replay gestures: open it with createRenderer(doc, { interactive: true })')\n replayer ??= createReplayer(player, withParams, handlers, sends, { settle: opts.settle })\n for (const g of gestures) replayer.apply(g)\n return { sends: [...sends], expectFailures: [...replayer.expectFailures], warnings: [...replayer.warnings] }\n }\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 if (frameOpts.script?.length) play(frameOpts.script)\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 player.render() // a gesture or a step paints only what it changed: draw the frame that is asked for\n return el.toBuffer('png')\n },\n play,\n async capture() {\n if (!player) throw new Error('renderer is closed')\n player.render()\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, interactive: !!opts.script?.length, settle: opts.settle })\n try {\n return await renderer.frame(opts.frame ?? 0, { vars: opts.vars, steps: opts.steps, script: opts.script })\n } finally {\n renderer.close()\n }\n}\n"],"mappings":";AAQA,SAAS,kBAAkC;AAC3C,SAAS,sBAAkE;AAE3E,SAAS,SAAS,kBAAkB;AACpC,SAAS,aAAa,eAAe,cAAc;AACnD,SAAS,cAAc;AACvB,SAAS,YAAY;AAQrB,IAAM,mBAAmB;AAmBzB,IAAM,WAAmC;AAAA,EACvC,cAAc;AAAA,EAAU,aAAa;AAAA,EAAS,YAAY;AAAA,EAAQ,YAAY;AAAA,EAC9E,mBAAmB;AAAA,EAAQ,yBAAyB;AAAA,EAAS,0BAA0B;AACzF;AAKA,SAAS,cAAc,KAAU,aAAoF;AACnH,QAAM,SAAS,IAAI,UAAU,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,UAAU,SAAS,KAAK,EAAE,QAAQ,EAAE,CAAC;AAC/F,MAAI,MAAM,WAAW,EAAG,QAAO,EAAE,KAAK,MAAM,UAAU,CAAC,EAAE;AACzD,QAAM,MAAM,YAAY,KAAK,OAAO,GAAG,cAAc,CAAC;AACtD,QAAM,WAAqB,CAAC;AAC5B,aAAW,KAAK,OAAO;AACrB,UAAM,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,GAAG,IAAI,CAAC;AAChD,UAAM,MAAM,SAAS,EAAE,IAAI,KAAK,KAAK,EAAE,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,OAAO,QAAQ,UAAU,EAAE,CAAC;AACzF,UAAM,OAAO,KAAK,KAAK,EAAE,GAAG,QAAQ,YAAY,GAAG,IAAI,GAAG;AAC1D,QAAI;AACF,oBAAc,MAAM,OAAO,KAAK,KAAK,QAAQ,CAAC;AAG9C,YAAM,OAAO,EAAE,SAAS,YAAY,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,YAAY,IAAI,CAAC,IAAI,CAAC;AAClF,iBAAW,KAAK,KAAM,KAAI,CAAC,SAAS,SAAS,EAAE,MAAM,EAAG,UAAS,KAAK,EAAE,MAAM;AAAA,IAChF,SAAS,GAAG;AACV,cAAQ,OAAO,MAAM,8BAA8B,EAAE,EAAE,MAAM,EAAE,IAAI,MAAO,EAAY,OAAO;AAAA,CAAI;AAAA,IACnG;AAAA,EACF;AACA,SAAO,EAAE,KAAK,SAAS;AACzB;AAmCA,eAAsB,eAAe,KAAU,OAAoG,CAAC,GAAsB;AAExK,QAAM,UAAkB;AACxB,MAAI;AACJ,MAAI;AAAE,WAAQ,MAAM,OAAO;AAAA,EAAmC,QACxD;AACJ,UAAM,IAAI;AAAA,MACR;AAAA,IAIF;AAAA,EACF;AACA,QAAM,EAAE,QAAQ,WAAW,QAAQ,aAAa,UAAU,IAAI;AAC9D,QAAM,QAAQ,KAAK,SAAS,KAAK,QAAQ,IAAI,KAAK,QAAQ;AAC1D,QAAM,IAAI,IAAI,OAAO,IAAI,IAAI;AAE7B,QAAM,aAAa,KAAK,SAAS,YAAY,KAAK,KAAK,MAAM,IAAI;AAGjE,QAAM,EAAE,KAAK,SAAS,SAAS,IAAI,cAAc,YAAY,WAAW;AACxE,MAAI,SAAS,OAAQ,SAAQ,OAAO,MAAM,qBAAqB,SAAS,MAAM,cAAc,SAAS,SAAS,IAAI,QAAQ,GAAG,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,CAAI;AAGzJ,QAAM,SAAS,oBAAI,IAA+B;AAClD,aAAW,KAAK,WAAW,UAAU,CAAC,GAAG;AACvC,QAAI,EAAE,SAAS,SAAS,EAAE,SAAS,WAAW,gBAAgB,KAAK,EAAE,QAAQ,EAAE,GAAG;AAChF,UAAI;AAAE,eAAO,IAAI,EAAE,IAAK,MAAM,UAAU,EAAE,IAAI,CAAkC;AAAA,MAAE,QAAQ;AAAA,MAAgD;AAAA,IAC5I;AAAA,EACF;AAGA,QAAM,IAAI;AACV,QAAM,QAAiC,CAAC;AACxC,QAAM,MAAM,CAAC,GAAW,MAAe;AAAE,UAAM,CAAC,IAAI,EAAE,CAAC;AAAG,MAAE,CAAC,IAAI;AAAA,EAAE;AACnE,MAAI,UAAU,MAAM;AACpB,MAAI,aAAa,SAAS;AAC1B,MAAI,UAAU,EAAE,mBAAmB;AAAA,EAAC,GAAG,sBAAsB;AAAA,EAAC,GAAG,kBAAkB,MAAM,CAAC;AAC1F,MAAI,oBAAoB,MAAM;AAAA,EAAC,CAAC;AAChC,MAAI,uBAAuB,MAAM;AAAA,EAAC,CAAC;AACnC,MAAI,yBAAyB,MAAM,CAAC;AACpC,MAAI,wBAAwB,MAAM;AAAA,EAAC,CAAC;AACpC,MAAI,YAAY,EAAE,eAAe,CAAC,MAAe,MAAM,WAAW,IAAI,OAAO,GAAG,CAAC,IAAI,CAAC,EAAG,CAAC;AAE1F,QAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,KAAK,CAAC;AAC7C,QAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,KAAK,CAAC;AAC7C,QAAM,WAAqB,CAAC;AAC5B,QAAM,SAAS,IAAI,OAAO,KAAK,GAAG;AAClC,QAAM,KAAK;AACX,SAAO,OAAO,IAAI;AAAA,IAChB,uBAAuB,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,EAAE;AAAA;AAAA,IAE1F,kBAAkB,CAAC,MAAc,OAAyB;AAAE,eAAS,IAAI,IAAI;AAAA,IAAG;AAAA,IAChF,qBAAqB,CAAC,SAAiB;AAAE,aAAO,SAAS,IAAI;AAAA,IAAE;AAAA,IAC/D,mBAAmB,MAAM;AAAA,IAAC;AAAA,IAAG,uBAAuB,MAAM;AAAA,IAAC;AAAA,IAAG,OAAO,CAAC;AAAA,EACxE,CAAC;AAID,QAAM,QAAqB,CAAC;AAC5B,MAAI,SAA4B,IAAI,WAAW,IAAI,YAAY,EAAE,OAAO,CAAC,CAAC,KAAK,aAAa,OAAO,OAAO,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,OAAO,IAAI,EAAE,KAAK,MAAM,SAAS,CAAC,MAAM,MAAM,KAAK,CAAC,EAAE,CAAC;AACrM,MAAI,WAA4B;AAChC,QAAM,OAAO,CAAC,aAAoC;AAChD,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AACjD,QAAI,CAAC,KAAK,YAAa,OAAM,IAAI,MAAM,+FAA+F;AACtI,iBAAa,eAAe,QAAQ,YAAY,UAAU,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC;AACxF,eAAWA,MAAK,SAAU,UAAS,MAAMA,EAAC;AAC1C,WAAO,EAAE,OAAO,CAAC,GAAG,KAAK,GAAG,gBAAgB,CAAC,GAAG,SAAS,cAAc,GAAG,UAAU,CAAC,GAAG,SAAS,QAAQ,EAAE;AAAA,EAC7G;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,MAAM,MAAM,OAAO,YAAY,CAAC,GAAG;AACjC,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AACjD,UAAI,UAAU,KAAM,YAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,UAAU,IAAI,EAAG,QAAO,OAAO,GAAG,CAAC;AAC3F,aAAO,KAAK,KAAK;AACjB,UAAI,UAAU,QAAQ,OAAQ,MAAK,UAAU,MAAM;AACnD,YAAM,QAAQ,UAAU,SAAS;AACjC,UAAI,QAAQ,GAAG;AACb,cAAM,IAAI,KAAK,IAAI,KAAK,MAAM,KAAK,GAAG,gBAAgB;AACtD,YAAI,QAAQ,iBAAkB,SAAQ,OAAO,MAAM,2BAA2B,gBAAgB,SAAS,KAAK;AAAA,CAAK;AACjH,eAAO,QAAQ,CAAC;AAAA,MAClB;AACA,aAAO,OAAO;AACd,aAAO,GAAG,SAAS,KAAK;AAAA,IAC1B;AAAA,IACA;AAAA,IACA,MAAM,UAAU;AACd,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AACjD,aAAO,OAAO;AACd,aAAO,GAAG,SAAS,KAAK;AAAA,IAC1B;AAAA,IACA,QAAQ;AACN,cAAQ,QAAQ;AAChB,eAAS;AACT,iBAAW,KAAK,OAAO;AAAE,YAAI,MAAM,CAAC,MAAM,OAAW,QAAO,EAAE,CAAC;AAAA,YAAQ,GAAE,CAAC,IAAI,MAAM,CAAC;AAAA,MAAE;AAEvF,UAAI,QAAS,KAAI;AAAE,eAAO,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MAAE,QAAQ;AAAA,MAA4B;AAAA,IAC3G;AAAA,EACF;AACF;AAWA,SAAS,YAAY,KAAU,QAAqC;AAClE,QAAM,UAAU,OAAO,QAAQ,MAAM;AACrC,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAE5B,QAAM,UAAU,oBAAI,IAAyB;AAC7C,aAAW,OAAO,IAAI,SAAS;AAC7B,YAAQ,IAAI,IAAI,IAAI,oBAAI,IAAI,CAAC,IAAI,IAAI,UAAU,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,KAAK,GAAG,IAAI,IAAI,UAAU,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;AAAA,EACtH;AACA,QAAM,MAAM,gBAAgB,GAAG;AAC/B,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,CAAC,UAAwB;AACrC,eAAW,MAAM,OAAO;AACtB,UAAI,WAAW,EAAE,GAAG;AAClB,cAAM,QAAQ,QAAQ,IAAI,GAAG,QAAQ;AACrC,YAAI,OAAO;AACT,qBAAW,CAAC,GAAG,CAAC,KAAK,SAAS;AAC5B,gBAAI,CAAC,MAAM,IAAI,CAAC,EAAG;AAClB,aAAC,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI;AACzB,oBAAQ,IAAI,CAAC;AAAA,UACf;AAAA,QACF;AAAA,MACF;AACA,UAAI,QAAQ,EAAE,EAAG,YAAW,KAAK,GAAG,OAAQ,OAAM,EAAE,KAAK;AAAA,IAC3D;AAAA,EACF;AACA,aAAW,KAAK,IAAI,OAAQ,OAAM,EAAE,KAAK;AACzC,aAAW,OAAO,IAAI,QAAS,YAAW,KAAK,IAAI,OAAQ,OAAM,EAAE,KAAK;AACxE,aAAW,CAAC,CAAC,KAAK,SAAS;AACzB,QAAI,CAAC,QAAQ,IAAI,CAAC,EAAG,SAAQ,OAAO,MAAM,2CAA2C,CAAC;AAAA,CAAK;AAAA,EAC7F;AACA,SAAO;AACT;AAIA,eAAsB,eAAe,KAAU,OAAmB,CAAC,GAAwB;AACzF,QAAM,WAAW,MAAM,eAAe,KAAK,EAAE,OAAO,KAAK,OAAO,QAAQ,KAAK,QAAQ,aAAa,CAAC,CAAC,KAAK,QAAQ,QAAQ,QAAQ,KAAK,OAAO,CAAC;AAC9I,MAAI;AACF,WAAO,MAAM,SAAS,MAAM,KAAK,SAAS,GAAG,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,OAAO,QAAQ,KAAK,OAAO,CAAC;AAAA,EAC1G,UAAE;AACA,aAAS,MAAM;AAAA,EACjB;AACF;","names":["g"]}
|
|
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, type SendEvent } from '@flatkit/player'\nimport { createReplayer, type Gesture, type Handlers, type Replayer } from '@flatkit/player/debug'\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>; script?: Gesture[]; settle?: number }\n\n/** What a replayed script reports: the `send`s it emitted, and the `expect` gestures that did not hold. */\nexport type PlayReport = { sends: SendEvent[]; expectFailures: string[]; warnings: 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; `script` replays gestures after the seek\n * (an `interactive` renderer only); `steps` then runs N sim steps before capture. */\n frame(frame: number, opts?: { vars?: Record<string, number>; steps?: number; script?: Gesture[] }): Promise<Uint8Array>\n /** Replays gestures on the live scene, as `flatc --play` does, and leaves it in the state they reach.\n * Successive calls continue the same session (one state, one `expect` window). Needs\n * `createRenderer(doc, { interactive: true })`. */\n play(gestures: Gesture[]): PlayReport\n /** PNG of the scene AS IT IS NOW — after a `play`, without seeking. */\n capture(): 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>; interactive?: boolean; settle?: number } = {}): 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 handlers: Handlers = {}\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 // The listeners the player registers are KEPT: a replayed script fires into them (`play`).\n addEventListener: (type: string, fn: Handlers[string]) => { handlers[type] = fn },\n removeEventListener: (type: string) => { delete handlers[type] },\n setPointerCapture: () => {}, releasePointerCapture: () => {}, style: {},\n })\n\n // `interactive`: the player listens to its canvas, so gestures can be replayed on it. Off by default —\n // a renderer that only draws frames registers nothing.\n const sends: SendEvent[] = []\n let player: FlatPlayer | null = new FlatPlayer(el, withParams, { input: !!opts.interactive, audio: false, padding: 0, seed: 1, image: (id) => images.get(id) ?? null, onEvent: (e) => sends.push(e) })\n let replayer: Replayer | null = null\n const play = (gestures: Gesture[]): PlayReport => {\n if (!player) throw new Error('renderer is closed')\n if (!opts.interactive) throw new Error('this renderer cannot replay gestures: open it with createRenderer(doc, { interactive: true })')\n replayer ??= createReplayer(player, withParams, handlers, sends, { settle: opts.settle })\n for (const g of gestures) replayer.apply(g)\n return { sends: [...sends], expectFailures: [...replayer.expectFailures], warnings: [...replayer.warnings] }\n }\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 if (frameOpts.script?.length) play(frameOpts.script)\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 // A replayed gesture paints only what it changed: draw again after a script. NOT otherwise — the seek\n // (and `stepSim`) have just drawn this frame, and a second draw goes through the filter cache, which\n // is not the same pixels to the bit as the first (one to three levels in 255 under a blur).\n if (frameOpts.script?.length) player.render()\n return el.toBuffer('png')\n },\n play,\n async capture() {\n if (!player) throw new Error('renderer is closed')\n player.render()\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, interactive: !!opts.script?.length, settle: opts.settle })\n try {\n return await renderer.frame(opts.frame ?? 0, { vars: opts.vars, steps: opts.steps, script: opts.script })\n } finally {\n renderer.close()\n }\n}\n"],"mappings":"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","names":["g"]}
|
|
@@ -27,7 +27,7 @@ Inside `object "Name" { … }`:
|
|
|
27
27
|
| `when held` | a long press |
|
|
28
28
|
| `when dropped on <Zone> [at pointer]` | released over a drop zone (see [drag & drop](#drag--drop)) |
|
|
29
29
|
|
|
30
|
-
Scene-wide: `when loaded { … }` (once), `every frame { … }` (each
|
|
30
|
+
Scene-wide: `when loaded { … }` (once), `every frame { … }` (each simulation step, 60 Hz — see [how a frame runs](#how-a-frame-runs)), `at frame <n> { … }`,
|
|
31
31
|
`label <frame> "name"`. These live at the TOP LEVEL of the program, outside any `object` block — inside
|
|
32
32
|
one they do nothing, and `--check` says so.
|
|
33
33
|
|
|
@@ -501,6 +501,45 @@ match Word1, Word2 onto Good, Bad {
|
|
|
501
501
|
It generates, per item, `<Item>_placed` / `<Item>_ok` / `<Item>_zone` state and the drag+drop handlers;
|
|
502
502
|
you keep the visual (`var <Item>_x`/`_y` + your channel expressions).
|
|
503
503
|
|
|
504
|
+
## How a frame runs
|
|
505
|
+
|
|
506
|
+
Three things happen, always in this order, and knowing it removes most "it reads the old value" surprises.
|
|
507
|
+
|
|
508
|
+
**1. Events, as they arrive.** A press, a move, a release or a key runs its handlers **at once**, between
|
|
509
|
+
two displays — not at the next step. At a release the order is: the gesture's outputs are written (drag
|
|
510
|
+
position, `link` target), then `when released`, then `when dropped on …` (in declaration order), then
|
|
511
|
+
`when clicked` if the press stayed a tap.
|
|
512
|
+
|
|
513
|
+
**2. Steps of the simulation.** `every frame { … }` runs at a **fixed 60 Hz**: one run is one step of
|
|
514
|
+
exactly 1/60 s, **whatever the `timeline` fps and whatever the display**. `timeline 30 …` only sets the
|
|
515
|
+
speed of the playhead: after 60 steps `clock` has advanced by 1 and `frame` by 30. The step has a name,
|
|
516
|
+
**`DT`** (= 1/60, in seconds), so an integration is written `v = v + a * DT` — never measure it from
|
|
517
|
+
`clock`. In a browser `clock`, `time` and `frame` follow REAL time, once per display, so two steps run in
|
|
518
|
+
the same display read the same `clock`; `clock - previous` is then the display's duration on the first and
|
|
519
|
+
`0` on the second. (`flatc --play` advances them by 1/60 per step, which is why a replay is exact.)
|
|
520
|
+
|
|
521
|
+
How many steps run before each display depends on the display: none or one at 120 Hz, one at 60 Hz, two at
|
|
522
|
+
30. When the display stalls (a tab in the background, a slow device) the player does **not** catch up: it
|
|
523
|
+
counts at most 0.25 s per display and runs at most 30 steps, dropping the rest. The simulation then runs
|
|
524
|
+
slower than the wall clock; it never jumps. Within a step, the scene's `every frame` runs first, then
|
|
525
|
+
those of the active symbols; `at frame <n>` scripts come after, in the same step.
|
|
526
|
+
|
|
527
|
+
**3. The picture.** Channel bindings (`x = px`, `opacity = lit`) are not statements that run: they are
|
|
528
|
+
read whenever something looks at the object — when it is drawn, when it is hit-tested, when a handler
|
|
529
|
+
reads `Target.x` — and always give the value of NOW. What is drawn between two steps is interpolated
|
|
530
|
+
between them, for smoothness; variables are never changed by that.
|
|
531
|
+
|
|
532
|
+
What follows from it:
|
|
533
|
+
|
|
534
|
+
- **A handler reads a derived value as the last step left it.** If `every frame { double = count * 2 }`
|
|
535
|
+
and a handler does `count = count + 1` then reads `double`, it reads the value from BEFORE its own
|
|
536
|
+
write. Two events between two steps: the second sees what the first *wrote*, not what `every frame`
|
|
537
|
+
derives from it. Derive in the handler what the handler needs, or make it a function (`fn`).
|
|
538
|
+
- **`flatc --play` gives every pointer event one step** (see [tooling](tooling.md#headless-play----play)),
|
|
539
|
+
as a real pointer does; `"settle": 0` replays two events with no step in between, which is the case above.
|
|
540
|
+
- **The step is guaranteed**; the number of steps per display is not. A rule that counts steps counts
|
|
541
|
+
sixtieths of a second of simulated time.
|
|
542
|
+
|
|
504
543
|
## See also
|
|
505
544
|
|
|
506
545
|
- The expression language and stdlib → **[Expressions & stdlib](expressions-and-stdlib.md)**
|
package/docs/dsl-gotchas.md
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
- A `.flatink` file splits in two: the **`scene { … }`** block (the VISUAL composition:
|
|
11
11
|
`path`/`circle`/`group`/`image`/`text`) and the **behavior** that follows (`object "Name"
|
|
12
12
|
{ … }`, `every frame`, timeline bindings). The two do NOT share the same grammar.
|
|
13
|
+
- **`every frame` is a 60 Hz simulation step, not a timeline frame.** One run is exactly 1/60 s —
|
|
14
|
+
the constant **`DT`** — whatever `timeline <fps>` says and whatever the display does; integrate with
|
|
15
|
+
`v = v + a * DT`, never with a `dt` measured from `clock`. Handlers run when their event arrives,
|
|
16
|
+
BEFORE the next step: a handler that reads a value derived in `every frame` reads the one of the last
|
|
17
|
+
step. The full order is in [how a frame runs](behavior-and-interactions.md#how-a-frame-runs).
|
|
13
18
|
|
|
14
19
|
## `object "X"` addresses an ANIMATABLE item — a group, instance, text or image
|
|
15
20
|
|
|
@@ -30,7 +30,8 @@ min max hypot clamp(x, lo, hi) lerp(a, b, t) mod(a, b) between(x, lo, hi)
|
|
|
30
30
|
rad(deg) deg(rad) turns(n)
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Constants: `PI`, `TAU` (2π), `E
|
|
33
|
+
Constants: `PI`, `TAU` (2π), `E`, and **`DT`** — the duration of one `every frame` step, in seconds: 1/60,
|
|
34
|
+
whatever the timeline's fps (`v = v + a * DT`). See [how a frame runs](behavior-and-interactions.md#how-a-frame-runs).
|
|
34
35
|
|
|
35
36
|
> **`lerp` is the exponential smoother.** `lerp(v, target, k)` = `v + (target - v) * k` — so
|
|
36
37
|
> `niv = lerp(niv, target, 0.1)` in `every frame` eases `niv` toward `target` (no new helper needed; the
|
|
@@ -70,6 +71,38 @@ its index are expressions, only the element picked is evaluated, and it indexes
|
|
|
70
71
|
index is rounded; outside the table it is `NaN`, so the binding keeps its fallback). It is not a value on
|
|
71
72
|
its own: `[1, 2, 3]` without an index is an error — to keep a table, declare `var t = [1, 2, 3]`.
|
|
72
73
|
|
|
74
|
+
## Determinism
|
|
75
|
+
|
|
76
|
+
For someone replaying a simulation elsewhere (a Python replica, a test that compares to the bit), what can
|
|
77
|
+
be counted on. Numbers are IEEE 754 doubles, and an expression is evaluated in the order it is written,
|
|
78
|
+
one rounded operation at a time (no fused multiply-add).
|
|
79
|
+
|
|
80
|
+
- **Exact** — the same bits on every engine: the operators `+ - * / %` and the comparisons, and `abs`
|
|
81
|
+
`floor` `ceil` `round` `sign` `min` `max` `sqrt` `clamp` `lerp` `mod` `between` `rad` `deg` `turns`.
|
|
82
|
+
(`sqrt` is the IEEE square root, correctly rounded on every engine in use, although ECMAScript does not
|
|
83
|
+
formally demand it. `rad`, `deg` and `turns` multiply by the double `PI`: exact, provided the replica
|
|
84
|
+
does the same operations in the same order — `rad(d)` is `d * PI / 180`.)
|
|
85
|
+
- **Engine-dependent** — ECMAScript leaves the last bits to the implementation: `sin` `cos` `tan` `asin`
|
|
86
|
+
`acos` `atan` `atan2` `pow` `exp` `log` `hypot`. Two browsers usually agree, and nothing guarantees
|
|
87
|
+
it; a Python or C library need not agree with either. Integrated over thousands of steps, one bit
|
|
88
|
+
becomes a visible gap. A replica that must match to the bit uses its own polynomial for these, on both
|
|
89
|
+
sides.
|
|
90
|
+
|
|
91
|
+
Three spellings that differ from other languages:
|
|
92
|
+
|
|
93
|
+
- **`%`** is the remainder of the truncated division: it takes the sign of the LEFT operand (`-1 % 3` is
|
|
94
|
+
`-1`), and works on decimals (`3.25 % 12` is `3.25`). It is C's `fmod`, Python's `math.fmod` — not
|
|
95
|
+
Python's `%`. **`mod(a, b)`** is the positive one (`mod(-1, 3)` is `2`), Python's `%` for `b > 0`.
|
|
96
|
+
- **`round`** sends a half UP, toward +∞: `round(2.5)` is `3`, `round(-2.5)` is `-2`. Python's `round`
|
|
97
|
+
sends it to the even neighbour. An array index is rounded the same way.
|
|
98
|
+
- **`random()`** draws from a seeded generator made of integer operations only: the same seed gives the
|
|
99
|
+
same sequence on every engine. A replay is always seeded (`flatc --play`, seed `1`, or `--seed N`); a
|
|
100
|
+
player in a page draws from the browser unless its host passes `seed`.
|
|
101
|
+
|
|
102
|
+
Time: one step of `every frame` is exactly **`DT`** = 1/60 s — integrate with it. `clock`, `time` and
|
|
103
|
+
`frame` follow real time in a browser and are exact only in a replay (see
|
|
104
|
+
[how a frame runs](behavior-and-interactions.md#how-a-frame-runs)).
|
|
105
|
+
|
|
73
106
|
## Functions (`fn`)
|
|
74
107
|
|
|
75
108
|
Define reusable helpers — a **value** function (an expression) or a **procedure** (actions):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flatkit/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"description": "The FlatInk language (parser + AST) and compiler (.flatink → .flatpack). Ships the flatc CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Zwyk Studio",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"docs"
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@flatkit/engine": "0.
|
|
61
|
-
"@flatkit/player": "0.
|
|
62
|
-
"@flatkit/types": "0.
|
|
60
|
+
"@flatkit/engine": "0.39.1",
|
|
61
|
+
"@flatkit/player": "0.39.1",
|
|
62
|
+
"@flatkit/types": "0.39.1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"skia-canvas": "^3.0.8 || ^4.0.0-rc7"
|
package/prompts/flatink-core.md
CHANGED
|
@@ -288,7 +288,9 @@ align top of "Bin" [offset dx,dy] // pin origin onto ano
|
|
|
288
288
|
Pure & numeric (no booleans: comparisons/logic yield `1`/`0`). Operators: `?: || && == != < > <= >=
|
|
289
289
|
+ - * / % - ! . [] fn()`. A lookup table can be written in place and indexed at once: `[10, 20, 30][i]`.
|
|
290
290
|
Built-ins: `sin cos tan asin acos atan atan2 abs sqrt pow exp log floor ceil round sign min max hypot
|
|
291
|
-
clamp(x,lo,hi) lerp(a,b,t) mod(a,b) between(x,lo,hi) rad(deg) deg(rad) turns(n)`. Constants `PI TAU E
|
|
291
|
+
clamp(x,lo,hi) lerp(a,b,t) mod(a,b) between(x,lo,hi) rad(deg) deg(rad) turns(n)`. Constants `PI TAU E`, and `DT` = 1/60 s: `every frame` is a
|
|
292
|
+
fixed 60 Hz step whatever the timeline's fps, so integrate with `v = v + a * DT`. Handlers run when their event
|
|
293
|
+
arrives, before the next step.
|
|
292
294
|
Reserved: `time` (seconds, **wraps** every `durationFrames`), `clock` (seconds, **monotone**), `frame`,
|
|
293
295
|
`value`, `mouse.x/y`, `keys.<Key>`, `self.*`, `<Name>.*`.
|
|
294
296
|
Packages: `use "collision" | "easing" | "gesture" | "feedback"`; functions are available bare and
|
package/prompts/flatink-lite.md
CHANGED
|
@@ -104,7 +104,7 @@ State/funcs: `var a = 0` · `var arr = fill(8,0)` (also as an ASSIGNMENT: `arr =
|
|
|
104
104
|
## Expressions
|
|
105
105
|
Pure numeric, no booleans (compare/logic → 1/0). Ops: `?: || && == != < > <= >= + - * / % - ! . [] fn()` · inline table `[10, 20, 30][i]`.
|
|
106
106
|
Funcs: `sin cos tan atan2 abs sqrt pow floor ceil round sign min max hypot clamp(x,lo,hi) lerp(a,b,t)
|
|
107
|
-
mod(a,b) between(x,lo,hi) rad(deg) deg(rad) turns(n)`. Const `PI TAU E
|
|
107
|
+
mod(a,b) between(x,lo,hi) rad(deg) deg(rad) turns(n)`. Const `PI TAU E` · `DT` = 1/60 s (the `every frame` step, fixed 60 Hz: `v = v + a * DT`).
|
|
108
108
|
Reserved: `time`(s, **wraps**) `clock`(s, **monotone**) `frame` `value` `mouse.x/y` `keys.<Key>` `self.*` `<Name>.*`.
|
|
109
109
|
Channels: `x y scaleX scaleY rotation opacity` (absolute) + `dx dy` (additive: `pos = at + (dx, dy)`).
|
|
110
110
|
Stateful easing: `spring <ch> "<target>" stiffness <0..1> damping <0..1>` · `smooth <ch> "<target>" k <0..1>`
|
package/prompts/role-coder.md
CHANGED
|
@@ -68,7 +68,7 @@ object "Dial" { spring rotation = aim { stiffness 0.08 damping 0.86 } } // smo
|
|
|
68
68
|
Pure numeric expressions (no booleans — logic/compares yield `1`/`0`). Operators `?: || && == != < >
|
|
69
69
|
<= >= + - * / % - ! . [] fn()`. Built-ins: `sin cos tan atan2 abs sqrt pow exp log floor ceil round
|
|
70
70
|
sign min max hypot clamp(x,lo,hi) lerp(a,b,t) mod(a,b) between(x,lo,hi) rad deg turns`. Constants
|
|
71
|
-
`PI TAU E
|
|
71
|
+
`PI TAU E`, `DT` (= 1/60 s: `every frame` is a fixed 60 Hz step, integrate with `v = v + a * DT`). Reserved: `time` (s), `frame`, `value`, `mouse.x/y`, `keys.<Key>`, `self.*`, `<Name>.*`.
|
|
72
72
|
|
|
73
73
|
Functions: `fn dist(ax,ay,bx,by) = hypot(ax-bx, ay-by)` (value) · `fn reset() { score = 0 }` (procedure).
|
|
74
74
|
|