@flatkit/compiler 0.31.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/manifest.ts","../src/languageCard.ts","../src/drawingCard.ts","../src/index.ts"],"sourcesContent":["// ─────────────────────────────────────────────────────────────────────────────\n// manifest.ts — \"map of the scene\" for an LLM (and tooling introspection).\n//\n// The language fits in < 2k tokens (see languageCard); the real context cost for an LLM\n// is knowing WHAT IT CAN NAME in THIS scene (objects, assets, variables, functions).\n// `docToManifest` derives this compact block from a Doc — the exact counterpart of references\n// by name (sceneRefs): the model then references only REAL names, and the linter catches the rest.\n//\n// Pure, derived (never stored). ~a few hundred tokens for a real scene.\n// ─────────────────────────────────────────────────────────────────────────────\nimport type { Doc, Group, Image, Instance, ItemEvent, Layer, Text } from '@flatkit/types'\nimport { EXPR_CHANNELS, OFFSET_CHANNELS } from '@flatkit/engine/timeline'\nimport { getSymbol, isGroup, isImage, isInstance, isPoseable, isText } from '@flatkit/engine/layers'\nimport { analyzeExpr } from '@flatkit/engine/expr'\nimport { forEachAction, forEachActionExpression, forEachItemExpression } from './docWalk'\nimport { languageCard } from './languageCard'\nimport { drawingCard } from './drawingCard'\n\n/** Type label of a poseable (hence named) item. */\nfunction kindLabel(doc: Doc, it: Group | Instance | Text | Image): string {\n if (isInstance(it)) { const s = getSymbol(doc, it.symbolId); return s ? `Instance:${s.name}` : 'Instance' }\n if (isText(it)) return 'Text'\n if (isImage(it)) return 'Image'\n return 'Symbol' // Group (\"symbol\" in the UI)\n}\n\n/**\n * A named scene object and the CONTRACT the logic places on it — what a host must honour to reskin the\n * scene without touching its behavior. Deliberately free of coordinates: the names and the roles are the\n * contract, the composition stays entirely the skin's business. (A contract that carried positions would\n * hand every reskin the same layout, which is exactly the failure it exists to prevent.)\n */\nexport type ManifestObject = {\n name: string\n kind: string\n /** Item events the logic handles on it (`click`, `drop`…) — the skin must keep them reachable. */\n events: ItemEvent[]\n /** A `drag`/`turn` interactor targets it: the pointer writes its position, so the skin must not pin it. */\n dragged: boolean\n /** A placement target: something drops on it, or it carries a `hitbox`. Keep the name and a generous box. */\n zone: boolean\n /** Channels the logic drives (bindings and modifier targets) — a skin that sets these fights the logic. */\n channels: string[]\n /** Document variables it reads: the game state a skin can hang its own visuals on. */\n reads: string[]\n}\n\n/** The program's STATE: every variable a skin could meaningfully bind to. Wider than `doc.variables`,\n * which only holds the `var`-declared ones — a variable first assigned in a handler, or written by a\n * `drag` interactor, is just as real at runtime and is often the most interesting one to bind. */\nfunction stateVariables(doc: Doc): Set<string> {\n const out = new Set(Object.keys(doc.variables ?? {}))\n forEachAction(doc, (a) => {\n if (a.do === 'setVar' || a.do === 'setIndex') out.add(a.name)\n else if (a.do === 'repeatRange') out.add(a.var)\n })\n for (const i of doc.interactors ?? []) for (const v of [i.varX, i.varY, i.varT]) if (v) out.add(v)\n return out\n}\n\n/** State variables referenced by an expression — parsed, not grepped, so `open` never matches `reopen`\n * and a name nested inside a call still counts. An unparseable expression yields nothing (the linter\n * reports it; the manifest stays quiet). Runtime scalars (`time`, `mouse.x`) are not state and never\n * appear: they are not something a host can set. */\nfunction readsOf(expr: string, state: Set<string>, into: Set<string>): void {\n const a = analyzeExpr(expr)\n if (!a.ok) return\n for (const id of a.refs.ids) if (state.has(id)) into.add(id)\n}\n\n/** Named scene objects (groups included, library symbols excluded) with their binding contract; first\n * name wins, and nested groups are walked. */\nexport function manifestObjects(doc: Doc): ManifestObject[] {\n const out: ManifestObject[] = []\n const seen = new Set<string>()\n const state = stateVariables(doc)\n const dropZones = new Set((doc.interactions ?? []).filter((i) => i.event === 'drop' && i.over).map((i) => i.over as string))\n const walk = (layers: Layer[]) => {\n for (const l of layers) for (const it of l.items) {\n if (isPoseable(it) && it.name && !seen.has(it.name)) {\n seen.add(it.name)\n const mine = (doc.interactions ?? []).filter((i) => i.targetId === it.id)\n const channels: string[] = []\n const reads = new Set<string>()\n forEachItemExpression(it, (expr, channel) => { channels.push(channel); readsOf(expr, state, reads) })\n for (const i of mine) forEachActionExpression(i.actions, (expr) => readsOf(expr, state, reads))\n out.push({\n name: it.name,\n kind: kindLabel(doc, it),\n events: [...new Set(mine.map((i) => i.event))],\n dragged: !!doc.interactors?.some((i) => i.targetId === it.id),\n zone: dropZones.has(it.name) || (isGroup(it) && !!it.hitbox),\n channels,\n reads: [...reads],\n })\n }\n if (isGroup(it)) walk(it.layers)\n }\n }\n walk(doc.layers)\n return out\n}\n\n/** The events the program EMITS to its host (`send \"…\"`), in order of first appearance, deduped. The\n * other half of the contract: the moments a host — or a skin's motion pass — can react to. */\nexport function manifestEvents(doc: Doc): string[] {\n const events = new Set<string>()\n forEachAction(doc, (a) => { if (a.do === 'send') events.add(a.event) })\n return [...events]\n}\n\n/** One object's contract, as readable clauses. Empty = pure decor the skin owns outright. */\nfunction contractParts(o: ManifestObject): string[] {\n const parts: string[] = []\n if (o.dragged) parts.push('drag')\n if (o.zone) parts.push('zone')\n if (o.events.length) parts.push(`on ${o.events.join('/')}`)\n if (o.channels.length) parts.push(`driven: ${o.channels.join(' ')}`)\n if (o.reads.length) parts.push(`reads: ${o.reads.join(' ')}`)\n return parts\n}\n\n/** Variables → `name=value` (scalar) or `name[len]` (array). */\nfunction manifestVars(doc: Doc): string[] {\n return Object.entries(doc.variables ?? {}).map(([k, v]) => (Array.isArray(v) ? `${k}[${v.length}]` : `${k}=${v}`))\n}\n\n/**\n * Compact map of the scene (objects/assets/variables/functions/packages) — injectable in a prompt.\n * Only non-empty sections appear. The names are the ones the code can reference.\n */\nexport function docToManifest(doc: Doc): string {\n const objs = manifestObjects(doc)\n const assets = (doc.assets ?? []).map((a) => `${a.kind}:${a.id}`)\n const vars = manifestVars(doc)\n const funcs = (doc.functions ?? []).map((f) => `${f.name}(${f.params.join(', ')})`)\n const lines = ['# SCENE', `size: ${doc.width}x${doc.height}`]\n if (objs.length) lines.push(`objects: ${objs.map((o) => `${o.name}(${o.kind})`).join(', ')}`)\n if (assets.length) lines.push(`assets: ${assets.join(', ')}`)\n if (vars.length) lines.push(`vars: ${vars.join(', ')}`)\n if (funcs.length) lines.push(`funcs: ${funcs.join(', ')}`)\n if (doc.imports?.length) lines.push(`packages: ${doc.imports.join(', ')}`)\n // The binding contract, for the objects that carry one — what a skin must honour. No positions: the\n // composition is the skin's to invent, which is precisely what keeps two scenes from looking alike.\n const contract = objs.map((o) => ({ o, parts: contractParts(o) })).filter(({ parts }) => parts.length)\n if (contract.length) {\n lines.push('contract (honour these; the layout is yours):')\n for (const { o, parts } of contract) lines.push(` ${o.name} - ${parts.join(', ')}`)\n }\n const events = manifestEvents(doc)\n if (events.length) lines.push(`events: ${events.join(', ')}`)\n lines.push(`channels: ${EXPR_CHANNELS.join(' ')} (additive offsets: ${OFFSET_CHANNELS.join(' ')} -> pos = at + d)`)\n return lines.join('\\n')\n}\n\n/**\n * EVERYTHING a model needs to write a whole program: both language references (static) + the scene map\n * derived from the Doc. It is the bundle, not the map — if you only want the names this scene can\n * reference (because you already inject the references yourself), that is **`docToManifest(doc)`**, and\n * calling this instead ships the cards a second time.\n *\n * The DRAWING card is included by default. Handed the behavior card alone, a model asked for decor\n * invents a shapes grammar — and what it invents does not compile. Pass `{ drawing: false }` when the\n * model only edits behavior and the prompt budget is tight.\n */\nexport function llmContext(doc: Doc, opts: { drawing?: boolean } = {}): string {\n const cards = opts.drawing === false ? languageCard() : `${languageCard()}\\n\\n${drawingCard()}`\n return `${cards}\\n\\n${docToManifest(doc)}`\n}\n","// ─────────────────────────────────────────────────────────────────────────────\n// languageCard.ts — TERSE reference of FlatInk Script, \"system-prompt\" sized.\n//\n// Single source of truth to steer an LLM (or a human in a hurry): condensed\n// grammar + built-ins, ~1k tokens. The function/constant/channel lists are\n// INTERPOLATED from expr/timeline/stdlib → never out of sync with the real engine.\n// To be paired with `docToManifest` (manifest.ts) for the names specific to a scene.\n// ─────────────────────────────────────────────────────────────────────────────\nimport { STD_CONSTANTS, STD_FUNCTIONS } from '@flatkit/engine/expr'\nimport { EXPR_CHANNELS } from '@flatkit/engine/timeline'\nimport { PACKAGES } from '@flatkit/engine/stdlib'\n\n/** Language reference card (static, kept in sync with the engine via STD_*). */\nexport function languageCard(): string {\n return `# FlatInk Script — reference\nNumbers only (0 = false, anything else = true). No string type (only event/label/asset names in quotes). Comment: // to end of line.\nOne statement per line — a newline ends the statement (write \\`x = 1\\` then \\`y = 2\\` on separate lines, not \\`x = 1 y = 2\\`).\n\n## Objects\nobject \"Name\" { … } attaches behavior BY NAME. The name must be a group / instance / text / image — the only things that carry a pose.\nA SHAPE (\\`rect … as \"N\"\\`) or a LAYER cannot be animated: wrap the shape in \\`group \"N\" { layer \"art\" { … } }\\`. (Naming one is a compile error.)\nOpacities MULTIPLY down the tree — do not set \\`opacity 0\\` on the shape to hide it at rest, or the group's fade-in stays invisible.\n\n## Events (attach to an object or to the scene)\nwhen loaded { } // once, on start\nevery frame { } // every frame\nat frame N { } // when the playhead reaches frame N\nwhen clicked|hovered|unhovered|pressed|dragged|released|held { }\n\n## Actions\nplay · pause · go to frame N [and play|and pause] · go to \"label\"\nname = expr // set a variable\narr[i] = expr // write an array slot\nif cond { } else { } · repeat N times { } · repeat i from A to B { }\nmyProc() · send \"event\"[, expr | text(\"id\") | { a = expr, b … }] · sound \"assetId\"\n\n## Expressions (drive a channel, or compute in an action)\nchannel = expr channels: ${EXPR_CHANNELS.join(' ')} (expression wins over keyframes)\ndx = expr · dy = expr // ADDITIVE offset: final pos = at + (dx, dy) — oscillate AROUND the anchor (dx = 30*sin(time)); absolute x/y REPLACE at\nrotationDeg = expr // sugar for rotation = rad(expr) — author angles in DEGREES (rotation & sin/cos/atan2 are RADIANS)\noperators: + - * / % < > <= >= == != && || ! cond ? a : b\ncontext: time frame clock value · mouse.x mouse.y mouse.dx mouse.dy · keys.Space keys.ArrowLeft … (keys are 1/0, use directly: keys.Space ? … : …)\n time WRAPS every durationFrames (2.5 s by default) — clock is MONOTONE. Timestamps you capture and compare later MUST use clock\n (\\`when wrong { shown = clock }\\` + \\`opacity = pulse(shown, 4)\\`), or the ramp replays on every loop.\nName.x Name.y Name.rotation Name.scaleX Name.scaleY Name.opacity // any named object (identifier name), live on-screen value (read-only)\nself.x self.y self.rotation self.scaleX self.scaleY self.opacity // the object's own channels, in its bindings (no mirror variable)\n\n## Spaces (local vs world)\nself & channels (x, y, rotation…) = LOCAL (relative to parent — what x = … sets). Name.x, mouse.x = WORLD (the stage).\nAt the scene root, local = world (the common case → nothing to think about). If an object is NESTED in a group and you\nrelate it to a world position, convert: toLocalX(x, y) toLocalY(x, y) (world → your space) · toGlobalX/Y (your space → world).\nconstants: ${STD_CONSTANTS.join(' ')}\nfunctions: ${STD_FUNCTIONS.join(' ')}\n\n## Direct manipulation (interactors)\ndrag x, y // object follows the pointer while held → writes vars x, y (bind them: x = vx, y = vy)\n{ enabled <expr> } gates the GESTURE only — when/pressed/released/clicked STILL fire. Guard the body: when released { if done == 0 { … } }\ndragX vx · dragY vy // single-axis\ndrag x, y { confine to Zone snap 10 } // bound to a named object's box · grid snap\nturn a around cx,cy { snap 15 } // dial/knob → a = pivot→cursor angle in RADIANS (pair: rotation = a)\nturnDeg a around cx,cy { snap 15 } // same in DEGREES (pair: rotationDeg = a) · snap is degrees on both\nwhen dropped on Zone { … } // fires on release when the object's center is inside the named zone\n\n## Declarations\nlet name = 0 · let arr = fill(n, v) · let arr = [a, b, c]\nfn name(a, b) = expr // value function (use in expressions)\nfn name() { … } // procedure (block of actions)\neach \"Symbol\" as i { opacity = data[i] } // bind every instance of a symbol (i = index)\nuse \"package\" packages: ${PACKAGES.join(' ')} // OPTIONAL: calling a package function imports it automatically\n\n## Example\nlet score = 0\nevery frame { score = score + 1 }\nobject \"Ball\" {\n when clicked { score = score + 1 }\n rotation = atan2(Target.y - self.y, Target.x - self.x) // aim at another object by name\n}`\n}\n","// ─────────────────────────────────────────────────────────────────────────────\n// drawingCard.ts — TERSE reference of the DRAWING half of FlatInk, \"system-prompt\" sized.\n//\n// `languageCard` covers BEHAVIOR (events, channels, expressions) and not one word of the composition:\n// no shapes, no paints, no filters. An integrator handing that card to a model and asking for decor was\n// handing it a reference with nothing about drawing — so the model guessed, and what a model guesses in\n// a DSL does not compile. The two cards are complements: pair them (plus `docToManifest` for the names\n// of a particular scene) whenever a model has to produce a whole `.flatink`.\n//\n// Every ```flatink example below is COMPILED by drawingCard.test.ts. A reference that is copied drifts;\n// one that is compiled cannot. When the grammar moves, that test goes red rather than this card going\n// quietly wrong.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Drawing reference card (composition: shapes, paints, filters, text, clipping). */\nexport function drawingCard(): string {\n return `# FlatInk — drawing (the \\`scene { … }\\` half)\n\nThe unit is the LAYER: \\`layer \"name\" { … }\\`, stacked bottom to top. A layer holds shapes, text, images\nand groups (which nest their own layers). Coordinates are PIXELS in the parent's frame — never percentages.\n\n## Shapes\ncircle <cx> <cy> <r>\nellipse <cx> <cy> <rx> <ry>\nrect <x> <y> <w> <h> [<r>] // r = rounded corners (or <rx> <ry> for distinct)\npath \"M0 0 C40 -20 80 20 120 0 Z\" // raw SVG path data — total freedom\ncircle 100 100 40 as \"Ring\" // name it (right after the geometry) → addressable\n\n## Paint\nfill #rrggbb | #rrggbbaa · nofill\nfill linear(<angle>, 0:#…, 1:#…) // angle: 0 = →, 90 = ↓ ; stops are offset:color\nfill radial(<cx>, <cy>, <r>, 0:#…, 1:#…) // cx/cy/r as 0..1 of the box\nstroke <paint> <width> [cap butt|round|square] [join miter|round|bevel] [miter <n>] [dash a,b]\nopacity <0..1> // opacities MULTIPLY down the tree\n\n## Filters — on any item, no wrapper group needed\nfilter glow <blur> <color> · filter shadow <dx> <dy> <blur> <color>\nfilter blur <radius> · filter adjust <brightness> <contrast> <saturate> <hue>\n\n## Text\ntext \"…\" at <x>,<y> box <w> <h> font \"sans-serif\" size <n> align center line 1.2 color #… [bold] [italic] [wrap]\ntext \"…\" along \"<shapeId>\" align center start 0.5 // laid along a named shape's curve\nWord-wrap is OPT-IN: without \\`wrap\\`, only an explicit \\\\n breaks a line.\n\n## Images\nimage \"<assetId>\" <w> <h> at <x>,<y> // the asset is declared at the top: asset \"logo\" \"logo.svg\" image\n\n## Clipping\ngroup \"Name\" at x,y clip <x> <y> <w> <h> { … } // rectangular cut, in the group's LOCAL coordinates\nmask layer \"Name\" { <shapes> layer \"c\" { … } } // arbitrary shape: the mask's matter clips its child layers\n\n## The five rules that decide whether it compiles\n1. WORD ORDER IS FIXED: content → \\`as \"…\"\\` → \\`at …\\` → style. So \\`text \"Hi\" at 10,10 box 200 40\\`,\n NEVER \\`text \"Hi\" box 200 40 at 10,10\\`. A shape names itself right after its geometry.\n Stroke options (\\`cap\\`/\\`join\\`/\\`miter\\`/\\`dash\\`) belong to the STROKE and follow it directly:\n \\`nofill stroke #888 2 dash 6,5\\`, not \\`stroke #888 2 nofill dash 6,5\\`.\n2. A COMMENT STARTS WITH \\`//\\`. A \\`#\\` opens a COLOR.\n3. ONE STATEMENT PER LINE.\n4. Anything you intend to ANIMATE must be a \\`group \"Name\" at x,y { layer \"c\" { … } }\\`. An \\`object\\`\n block aimed at a shape or a layer is a compile ERROR — shapes are baked material and carry no pose.\n5. A group's children are positioned RELATIVE to it. Draw them around 0,0 and place the group.\n\n## Examples\n\nA sky and a ground, in two gradients:\n\n\\`\\`\\`flatink\nsize 960 540\nscene {\n layer \"sky\" {\n rect 0 0 960 540 fill linear(90, 0:#1b2a4a, 1:#4a6fa5)\n ellipse 780 90 70 70 fill radial(0.5, 0.5, 0.5, 0:#ffe9a8, 1:#ffe9a800)\n }\n layer \"ground\" {\n path \"M0 430 C240 400 420 460 960 415 L960 540 L0 540 Z\" fill #2e4b34 opacity 0.85\n }\n}\n\\`\\`\\`\n\nA line-art silhouette and a drop shadow:\n\n\\`\\`\\`flatink\nsize 200 400\nscene {\n layer \"tree\" {\n path \"M60 300 L60 180 M60 220 L20 170 M60 230 L100 180\" nofill stroke #23301f 8 cap round\n circle 60 150 60 fill #3f6b3a filter shadow 0 6 12 #00000055\n }\n}\n\\`\\`\\`\n\nA group — the only shape a behavior block can animate:\n\n\\`\\`\\`flatink\nsize 480 320\nscene {\n layer \"life\" {\n group \"Cloud\" at 240,120 {\n layer \"c\" {\n ellipse 0 0 70 26 fill #ffffff opacity 0.22\n ellipse 46 -10 44 22 fill #ffffff opacity 0.18\n }\n }\n }\n}\n\nobject \"Cloud\" {\n dx = 30 * sin(clock)\n}\n\\`\\`\\`\n\nA panel, text, and a clipped porthole:\n\n\\`\\`\\`flatink\nsize 960 540\nscene {\n layer \"panel\" {\n rect 40 40 300 90 24 fill #10141c99 stroke #ffffff33 2\n text \"Workshop\" at 60,66 box 260 40 font \"sans-serif\" size 26 color #f2f6ff\n group \"Porthole\" at 700,300 clip -60 -60 120 120 {\n layer \"c\" { circle 0 0 90 fill linear(0, 0:#0b3d5c, 1:#0f6f92) }\n }\n }\n}\n\\`\\`\\``\n}\n","// @flatkit/compiler -- the FlatInk language and compiler.\n//\n// Parses FlatInk Script (the .flatink DSL) and compiles a program plus its assets into a .flatpack:\n// a playable `Doc` (JSON, with the material already baked). Also ships the `flatc` CLI, via its `bin` -- NOT from this entry, which stays free of Node builtins.\n//\n// The language layer (DSL parser/printer, .flat format) lives in @flatkit/engine and is re-exported\n// here so the compiler package is a single coherent entry point.\n\n// --- Compile a program (+ assets) into a playable .flatpack Doc ---------------\nexport { compileFlatpack, packToJSON, type MediaMap } from './compile'\n\n// --- Check a program the way the CLI does (source in, diagnostics out) --------\nexport { checkProgram, programDiagnostics, formatDiagnostics, applyFixes, repairLoop, type CheckDiagnostic, type CheckOptions, type CheckResult } from './check'\nexport type { TextEdit } from '@flatkit/engine/dsl'\n\n// --- Static analysis: lint a program / a whole Doc ----------------------------\nexport { lint, lintReport, localVariables, type LintContext } from './lint'\nexport {\n lintDoc, lintDocReport, docHasErrors, docLintContext, allScopeVariables,\n scopeProgram, docStructureWarnings, docLayoutWarnings,\n} from './programDoc'\n\n// --- Manifest / LLM context for a Doc -----------------------------------------\nexport { manifestObjects, manifestEvents, docToManifest, llmContext, type ManifestObject } from './manifest'\n\n// --- The reference cards: behavior (languageCard) and composition (drawingCard) -\nexport { languageCard } from './languageCard'\nexport { drawingCard } from './drawingCard'\n\n// --- Scope-program helpers (split/join the per-object behavior blocks) ---------\nexport { splitScopeProgram, scopeRegions, formatObjectBlock, joinScopeProgram } from './scopeProgram'\n\n// --- The flatc CLI entry point (also wired as the `flatc` bin) -----------------\n// `run` (the CLI entry) is deliberately NOT re-exported. It lives in a module that imports `fs`/`path` at\n// the top, so re-exporting it put Node builtins in the root's chunk graph -- and a browser bundle then\n// fails at NAME RESOLUTION (`\"extname\" is not exported by \"__vite-browser-external\"`), before tree-shaking\n// can drop the unused symbol. The root's job is the pure helpers a service or a browser needs\n// (`checkProgram`, `languageCard`, `drawingCard`, `docToManifest`); the CLI's job is the `bin`, which\n// imports `./cli/flatc` directly. `scripts/check-pack.mjs` walks the built chunk graph to keep it that way.\n\n// --- The language layer, re-exported from the engine for convenience ----------\nexport { parseUnits, printUnits } from '@flatkit/engine/dsl'\nexport {\n parseProgram, printProgram, parseProgramFull, printProgramFull,\n parseFlat, parseFlatLib, exportFlatProject,\n} from '@flatkit/engine/flatFormat'\n"],"mappings":"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","names":["EXPR_CHANNELS","EXPR_CHANNELS"]}
1
+ {"version":3,"sources":["../src/manifest.ts","../src/languageCard.ts","../src/drawingCard.ts","../src/index.ts"],"sourcesContent":["// ─────────────────────────────────────────────────────────────────────────────\n// manifest.ts — \"map of the scene\" for an LLM (and tooling introspection).\n//\n// The language fits in < 2k tokens (see languageCard); the real context cost for an LLM\n// is knowing WHAT IT CAN NAME in THIS scene (objects, assets, variables, functions).\n// `docToManifest` derives this compact block from a Doc — the exact counterpart of references\n// by name (sceneRefs): the model then references only REAL names, and the linter catches the rest.\n//\n// Pure, derived (never stored). ~a few hundred tokens for a real scene.\n// ─────────────────────────────────────────────────────────────────────────────\nimport type { Doc, Group, Image, Instance, ItemEvent, Layer, Text } from '@flatkit/types'\nimport { EXPR_CHANNELS, OFFSET_CHANNELS } from '@flatkit/engine/timeline'\nimport { getSymbol, isGroup, isImage, isInstance, isPoseable, isText } from '@flatkit/engine/layers'\nimport { analyzeExpr } from '@flatkit/engine/expr'\nimport { forEachAction, forEachActionExpression, forEachItemExpression } from './docWalk'\nimport { languageCard } from './languageCard'\nimport { drawingCard } from './drawingCard'\n\n/** Type label of a poseable (hence named) item. */\nfunction kindLabel(doc: Doc, it: Group | Instance | Text | Image): string {\n if (isInstance(it)) { const s = getSymbol(doc, it.symbolId); return s ? `Instance:${s.name}` : 'Instance' }\n if (isText(it)) return 'Text'\n if (isImage(it)) return 'Image'\n return 'Symbol' // Group (\"symbol\" in the UI)\n}\n\n/**\n * A named scene object and the CONTRACT the logic places on it — what a host must honour to reskin the\n * scene without touching its behavior. Deliberately free of coordinates: the names and the roles are the\n * contract, the composition stays entirely the skin's business. (A contract that carried positions would\n * hand every reskin the same layout, which is exactly the failure it exists to prevent.)\n */\nexport type ManifestObject = {\n name: string\n kind: string\n /** Item events the logic handles on it (`click`, `drop`…) — the skin must keep them reachable. */\n events: ItemEvent[]\n /** A `drag`/`turn` interactor targets it: the pointer writes its position, so the skin must not pin it. */\n dragged: boolean\n /** A placement target: something drops on it, or it carries a `hitbox`. Keep the name and a generous box. */\n zone: boolean\n /** Channels the logic drives (bindings and modifier targets) — a skin that sets these fights the logic. */\n channels: string[]\n /** Document variables it reads: the game state a skin can hang its own visuals on. */\n reads: string[]\n}\n\n/** The program's STATE: every variable a skin could meaningfully bind to. Wider than `doc.variables`,\n * which only holds the `var`-declared ones — a variable first assigned in a handler, or written by a\n * `drag` interactor, is just as real at runtime and is often the most interesting one to bind. */\nfunction stateVariables(doc: Doc): Set<string> {\n const out = new Set(Object.keys(doc.variables ?? {}))\n forEachAction(doc, (a) => {\n if (a.do === 'setVar' || a.do === 'setIndex') out.add(a.name)\n else if (a.do === 'repeatRange') out.add(a.var)\n })\n for (const i of doc.interactors ?? []) for (const v of [i.varX, i.varY, i.varT]) if (v) out.add(v)\n return out\n}\n\n/** State variables referenced by an expression — parsed, not grepped, so `open` never matches `reopen`\n * and a name nested inside a call still counts. An unparseable expression yields nothing (the linter\n * reports it; the manifest stays quiet). Runtime scalars (`time`, `mouse.x`) are not state and never\n * appear: they are not something a host can set. */\nfunction readsOf(expr: string, state: Set<string>, into: Set<string>): void {\n const a = analyzeExpr(expr)\n if (!a.ok) return\n for (const id of a.refs.ids) if (state.has(id)) into.add(id)\n}\n\n/** Named scene objects (groups included, library symbols excluded) with their binding contract; first\n * name wins, and nested groups are walked. */\nexport function manifestObjects(doc: Doc): ManifestObject[] {\n const out: ManifestObject[] = []\n const seen = new Set<string>()\n const state = stateVariables(doc)\n const dropZones = new Set((doc.interactions ?? []).filter((i) => i.event === 'drop' && i.over).map((i) => i.over as string))\n const walk = (layers: Layer[]) => {\n for (const l of layers) for (const it of l.items) {\n if (isPoseable(it) && it.name && !seen.has(it.name)) {\n seen.add(it.name)\n const mine = (doc.interactions ?? []).filter((i) => i.targetId === it.id)\n const channels: string[] = []\n const reads = new Set<string>()\n forEachItemExpression(it, (expr, channel) => { channels.push(channel); readsOf(expr, state, reads) })\n for (const i of mine) forEachActionExpression(i.actions, (expr) => readsOf(expr, state, reads))\n out.push({\n name: it.name,\n kind: kindLabel(doc, it),\n events: [...new Set(mine.map((i) => i.event))],\n dragged: !!doc.interactors?.some((i) => i.targetId === it.id),\n zone: dropZones.has(it.name) || (isGroup(it) && !!it.hitbox),\n channels,\n reads: [...reads],\n })\n }\n if (isGroup(it)) walk(it.layers)\n }\n }\n walk(doc.layers)\n return out\n}\n\n/** The events the program EMITS to its host (`send \"…\"`), in order of first appearance, deduped. The\n * other half of the contract: the moments a host — or a skin's motion pass — can react to. */\nexport function manifestEvents(doc: Doc): string[] {\n const events = new Set<string>()\n forEachAction(doc, (a) => { if (a.do === 'send') events.add(a.event) })\n return [...events]\n}\n\n/** One object's contract, as readable clauses. Empty = pure decor the skin owns outright. */\nfunction contractParts(o: ManifestObject): string[] {\n const parts: string[] = []\n if (o.dragged) parts.push('drag')\n if (o.zone) parts.push('zone')\n if (o.events.length) parts.push(`on ${o.events.join('/')}`)\n if (o.channels.length) parts.push(`driven: ${o.channels.join(' ')}`)\n if (o.reads.length) parts.push(`reads: ${o.reads.join(' ')}`)\n return parts\n}\n\n/** Variables → `name=value` (scalar) or `name[len]` (array). */\nfunction manifestVars(doc: Doc): string[] {\n return Object.entries(doc.variables ?? {}).map(([k, v]) => (Array.isArray(v) ? `${k}[${v.length}]` : `${k}=${v}`))\n}\n\n/**\n * Compact map of the scene (objects/assets/variables/functions/packages) — injectable in a prompt.\n * Only non-empty sections appear. The names are the ones the code can reference.\n */\nexport function docToManifest(doc: Doc): string {\n const objs = manifestObjects(doc)\n const assets = (doc.assets ?? []).map((a) => `${a.kind}:${a.id}`)\n const vars = manifestVars(doc)\n const funcs = (doc.functions ?? []).map((f) => `${f.name}(${f.params.join(', ')})`)\n const lines = ['# SCENE', `size: ${doc.width}x${doc.height}`]\n if (objs.length) lines.push(`objects: ${objs.map((o) => `${o.name}(${o.kind})`).join(', ')}`)\n if (assets.length) lines.push(`assets: ${assets.join(', ')}`)\n if (vars.length) lines.push(`vars: ${vars.join(', ')}`)\n if (funcs.length) lines.push(`funcs: ${funcs.join(', ')}`)\n if (doc.imports?.length) lines.push(`packages: ${doc.imports.join(', ')}`)\n // The binding contract, for the objects that carry one — what a skin must honour. No positions: the\n // composition is the skin's to invent, which is precisely what keeps two scenes from looking alike.\n const contract = objs.map((o) => ({ o, parts: contractParts(o) })).filter(({ parts }) => parts.length)\n if (contract.length) {\n lines.push('contract (honour these; the layout is yours):')\n for (const { o, parts } of contract) lines.push(` ${o.name} - ${parts.join(', ')}`)\n }\n const events = manifestEvents(doc)\n if (events.length) lines.push(`events: ${events.join(', ')}`)\n lines.push(`channels: ${EXPR_CHANNELS.join(' ')} (additive offsets: ${OFFSET_CHANNELS.join(' ')} -> pos = at + d)`)\n return lines.join('\\n')\n}\n\n/**\n * EVERYTHING a model needs to write a whole program: both language references (static) + the scene map\n * derived from the Doc. It is the bundle, not the map — if you only want the names this scene can\n * reference (because you already inject the references yourself), that is **`docToManifest(doc)`**, and\n * calling this instead ships the cards a second time.\n *\n * The DRAWING card is included by default. Handed the behavior card alone, a model asked for decor\n * invents a shapes grammar — and what it invents does not compile. Pass `{ drawing: false }` when the\n * model only edits behavior and the prompt budget is tight.\n */\nexport function llmContext(doc: Doc, opts: { drawing?: boolean } = {}): string {\n const cards = opts.drawing === false ? languageCard() : `${languageCard()}\\n\\n${drawingCard()}`\n return `${cards}\\n\\n${docToManifest(doc)}`\n}\n","// ─────────────────────────────────────────────────────────────────────────────\n// languageCard.ts — TERSE reference of FlatInk Script, \"system-prompt\" sized.\n//\n// Single source of truth to steer an LLM (or a human in a hurry): condensed\n// grammar + built-ins, ~1k tokens. The function/constant/channel lists are\n// INTERPOLATED from expr/timeline/stdlib → never out of sync with the real engine.\n// To be paired with `docToManifest` (manifest.ts) for the names specific to a scene.\n// ─────────────────────────────────────────────────────────────────────────────\nimport { STD_CONSTANTS, STD_FUNCTIONS } from '@flatkit/engine/expr'\nimport { EXPR_CHANNELS } from '@flatkit/engine/timeline'\nimport { PACKAGES } from '@flatkit/engine/stdlib'\n\n/** Language reference card (static, kept in sync with the engine via STD_*). */\nexport function languageCard(): string {\n return `# FlatInk Script — reference\nNumbers only (0 = false, anything else = true). No string type (only event/label/asset names in quotes). Comment: // to end of line.\nOne statement per line — a newline ends the statement (write \\`x = 1\\` then \\`y = 2\\` on separate lines, not \\`x = 1 y = 2\\`).\n\n## Objects\nobject \"Name\" { … } attaches behavior BY NAME. The name must be a group / instance / text / image — the only things that carry a pose.\nA SHAPE (\\`rect … as \"N\"\\`) or a LAYER cannot be animated: wrap the shape in \\`group \"N\" { layer \"art\" { … } }\\`. (Naming one is a compile error.)\nOpacities MULTIPLY down the tree — do not set \\`opacity 0\\` on the shape to hide it at rest, or the group's fade-in stays invisible.\n\n## Events (attach to an object or to the scene)\nwhen loaded { } // once, on start\nevery frame { } // every frame\nat frame N { } // when the playhead reaches frame N\nwhen clicked|hovered|unhovered|pressed|dragged|released|held { }\n\n## Actions\nplay · pause · go to frame N [and play|and pause] · go to \"label\"\nname = expr // set a variable\narr[i] = expr // write an array slot\nif cond { } else { } · repeat N times { } · repeat i from A to B { }\nmyProc() · send \"event\"[, expr | text(\"id\") | { a = expr, b … }] · sound \"assetId\"\n\n## Expressions (drive a channel, or compute in an action)\nchannel = expr channels: ${EXPR_CHANNELS.join(' ')} (expression wins over keyframes)\ndx = expr · dy = expr // ADDITIVE offset: final pos = at + (dx, dy) — oscillate AROUND the anchor (dx = 30*sin(time)); absolute x/y REPLACE at\nrotationDeg = expr // sugar for rotation = rad(expr) — author angles in DEGREES (rotation & sin/cos/atan2 are RADIANS)\noperators: + - * / % < > <= >= == != && || ! cond ? a : b\ncontext: time frame clock value · mouse.x mouse.y mouse.dx mouse.dy · keys.Space keys.ArrowLeft … (keys are 1/0, use directly: keys.Space ? … : …)\n time WRAPS every durationFrames (2.5 s by default) — clock is MONOTONE. Timestamps you capture and compare later MUST use clock\n (\\`when wrong { shown = clock }\\` + \\`opacity = pulse(shown, 4)\\`), or the ramp replays on every loop.\nName.x Name.y Name.rotation Name.scaleX Name.scaleY Name.opacity // any named object (identifier name), live on-screen value (read-only)\nself.x self.y self.rotation self.scaleX self.scaleY self.opacity // the object's own channels, in its bindings (no mirror variable)\n\n## Spaces (local vs world)\nself & channels (x, y, rotation…) = LOCAL (relative to parent — what x = … sets). Name.x, mouse.x = WORLD (the stage).\nAt the scene root, local = world (the common case → nothing to think about). If an object is NESTED in a group and you\nrelate it to a world position, convert: toLocalX(x, y) toLocalY(x, y) (world → your space) · toGlobalX/Y (your space → world).\nconstants: ${STD_CONSTANTS.join(' ')}\nfunctions: ${STD_FUNCTIONS.join(' ')}\n\n## Direct manipulation (interactors)\ndrag x, y // object follows the pointer while held → writes vars x, y (bind them: x = vx, y = vy)\n{ enabled <expr> } gates the GESTURE only — when/pressed/released/clicked STILL fire. Guard the body: when released { if done == 0 { … } }\ndragX vx · dragY vy // single-axis\ndrag x, y { confine to Zone snap 10 } // bound to a named object's box · grid snap\nturn a around cx,cy { snap 15 } // dial/knob → a = pivot→cursor angle in RADIANS (pair: rotation = a)\nturnDeg a around cx,cy { snap 15 } // same in DEGREES (pair: rotationDeg = a) · snap is degrees on both\ntrace p along Route { tolerance 30 } // follow a named guide with the finger → p = 0..1 by ARC LENGTH (pair: draw \"p\" on the SAME geometry)\nreveal c { brush 32 erase cells grid } // scratch → c = cleared fraction; erase = the runtime rubs the target out where scratched; grid[row*cols+col] = 1 says WHERE\nwhen dropped on Zone { … } // fires on release when the object's center is inside the named zone\n\n## Declarations\nlet name = 0 · let arr = fill(n, v) · let arr = [a, b, c]\nfn name(a, b) = expr // value function (use in expressions)\nfn name() { … } // procedure (block of actions)\neach \"Symbol\" as i { opacity = data[i] } // bind every instance of a symbol (i = index)\nuse \"package\" packages: ${PACKAGES.join(' ')} // OPTIONAL: calling a package function imports it automatically\n\n## Example\nlet score = 0\nevery frame { score = score + 1 }\nobject \"Ball\" {\n when clicked { score = score + 1 }\n rotation = atan2(Target.y - self.y, Target.x - self.x) // aim at another object by name\n}`\n}\n","// ─────────────────────────────────────────────────────────────────────────────\n// drawingCard.ts — TERSE reference of the DRAWING half of FlatInk, \"system-prompt\" sized.\n//\n// `languageCard` covers BEHAVIOR (events, channels, expressions) and not one word of the composition:\n// no shapes, no paints, no filters. An integrator handing that card to a model and asking for decor was\n// handing it a reference with nothing about drawing — so the model guessed, and what a model guesses in\n// a DSL does not compile. The two cards are complements: pair them (plus `docToManifest` for the names\n// of a particular scene) whenever a model has to produce a whole `.flatink`.\n//\n// Every ```flatink example below is COMPILED by drawingCard.test.ts. A reference that is copied drifts;\n// one that is compiled cannot. When the grammar moves, that test goes red rather than this card going\n// quietly wrong.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Drawing reference card (composition: shapes, paints, filters, text, clipping). */\nexport function drawingCard(): string {\n return `# FlatInk — drawing (the \\`scene { … }\\` half)\n\nThe unit is the LAYER: \\`layer \"name\" { … }\\`, stacked bottom to top. A layer holds shapes, text, images\nand groups (which nest their own layers). Coordinates are PIXELS in the parent's frame — never percentages.\n\n## Shapes\ncircle <cx> <cy> <r>\nellipse <cx> <cy> <rx> <ry>\nrect <x> <y> <w> <h> [<r>] // r = rounded corners (or <rx> <ry> for distinct)\npath \"M0 0 C40 -20 80 20 120 0 Z\" // raw SVG path data — total freedom\ncircle 100 100 40 as \"Ring\" // name it (right after the geometry) → addressable\n\n## Paint\nfill #rrggbb | #rrggbbaa · nofill\nfill linear(<angle>, 0:#…, 1:#…) // angle: 0 = →, 90 = ↓ ; stops are offset:color\nfill radial(<cx>, <cy>, <r>, 0:#…, 1:#…) // cx/cy/r as 0..1 of the box\nstroke <paint> <width> [cap butt|round|square] [join miter|round|bevel] [miter <n>] [dash a,b]\ndraw <0..1> [from <0..1>] // how much of the OUTLINE is stroked, by ARC LENGTH; \"quoted\" = per-frame expression\nopacity <0..1> // opacities MULTIPLY down the tree\n\n## Filters — on any item, no wrapper group needed\nfilter glow <blur> <color> · filter shadow <dx> <dy> <blur> <color>\nfilter blur <radius> · filter adjust <brightness> <contrast> <saturate> <hue>\n\n## Text\ntext \"…\" at <x>,<y> box <w> <h> font \"sans-serif\" size <n> align center line 1.2 color #… [bold] [italic] [wrap]\ntext \"…\" along \"<shapeId>\" align center start 0.5 // laid along a named shape's curve\nWord-wrap is OPT-IN: without \\`wrap\\`, only an explicit \\\\n breaks a line.\n\n## Images\nimage \"<assetId>\" <w> <h> at <x>,<y> // the asset is declared at the top: asset \"logo\" \"logo.svg\" image\n\n## Clipping\ngroup \"Name\" at x,y clip <x> <y> <w> <h> { … } // rectangular cut, in the group's LOCAL coordinates\nmask layer \"Name\" { <shapes> layer \"c\" { … } } // arbitrary shape: the mask's matter clips its child layers\n\n## The five rules that decide whether it compiles\n1. WORD ORDER IS FIXED: content → \\`as \"…\"\\` → \\`at …\\` → style. So \\`text \"Hi\" at 10,10 box 200 40\\`,\n NEVER \\`text \"Hi\" box 200 40 at 10,10\\`. A shape names itself right after its geometry.\n Stroke options (\\`cap\\`/\\`join\\`/\\`miter\\`/\\`dash\\`) belong to the STROKE and follow it directly:\n \\`nofill stroke #888 2 dash 6,5\\`, not \\`stroke #888 2 nofill dash 6,5\\`.\n2. A COMMENT STARTS WITH \\`//\\`. A \\`#\\` opens a COLOR.\n3. ONE STATEMENT PER LINE.\n4. Anything you intend to ANIMATE must be a \\`group \"Name\" at x,y { layer \"c\" { … } }\\`. An \\`object\\`\n block aimed at a shape or a layer is a compile ERROR — shapes are baked material and carry no pose.\n5. A group's children are positioned RELATIVE to it. Draw them around 0,0 and place the group.\n\n## Examples\n\nA sky and a ground, in two gradients:\n\n\\`\\`\\`flatink\nsize 960 540\nscene {\n layer \"sky\" {\n rect 0 0 960 540 fill linear(90, 0:#1b2a4a, 1:#4a6fa5)\n ellipse 780 90 70 70 fill radial(0.5, 0.5, 0.5, 0:#ffe9a8, 1:#ffe9a800)\n }\n layer \"ground\" {\n path \"M0 430 C240 400 420 460 960 415 L960 540 L0 540 Z\" fill #2e4b34 opacity 0.85\n }\n}\n\\`\\`\\`\n\nA line-art silhouette and a drop shadow:\n\n\\`\\`\\`flatink\nsize 200 400\nscene {\n layer \"tree\" {\n path \"M60 300 L60 180 M60 220 L20 170 M60 230 L100 180\" nofill stroke #23301f 8 cap round\n circle 60 150 60 fill #3f6b3a filter shadow 0 6 12 #00000055\n }\n}\n\\`\\`\\`\n\nA group — the only shape a behavior block can animate:\n\n\\`\\`\\`flatink\nsize 480 320\nscene {\n layer \"life\" {\n group \"Cloud\" at 240,120 {\n layer \"c\" {\n ellipse 0 0 70 26 fill #ffffff opacity 0.22\n ellipse 46 -10 44 22 fill #ffffff opacity 0.18\n }\n }\n }\n}\n\nobject \"Cloud\" {\n dx = 30 * sin(clock)\n}\n\\`\\`\\`\n\nA panel, text, and a clipped porthole:\n\n\\`\\`\\`flatink\nsize 960 540\nscene {\n layer \"panel\" {\n rect 40 40 300 90 24 fill #10141c99 stroke #ffffff33 2\n text \"Workshop\" at 60,66 box 260 40 font \"sans-serif\" size 26 color #f2f6ff\n group \"Porthole\" at 700,300 clip -60 -60 120 120 {\n layer \"c\" { circle 0 0 90 fill linear(0, 0:#0b3d5c, 1:#0f6f92) }\n }\n }\n}\n\\`\\`\\``\n}\n","// @flatkit/compiler -- the FlatInk language and compiler.\n//\n// Parses FlatInk Script (the .flatink DSL) and compiles a program plus its assets into a .flatpack:\n// a playable `Doc` (JSON, with the material already baked). Also ships the `flatc` CLI, via its `bin` -- NOT from this entry, which stays free of Node builtins.\n//\n// The language layer (DSL parser/printer, .flat format) lives in @flatkit/engine and is re-exported\n// here so the compiler package is a single coherent entry point.\n\n// --- Compile a program (+ assets) into a playable .flatpack Doc ---------------\nexport { compileFlatpack, packToJSON, type MediaMap } from './compile'\n\n// --- Check a program the way the CLI does (source in, diagnostics out) --------\nexport { checkProgram, programDiagnostics, formatDiagnostics, applyFixes, repairLoop, type CheckDiagnostic, type CheckOptions, type CheckResult } from './check'\nexport type { TextEdit } from '@flatkit/engine/dsl'\n\n// --- Static analysis: lint a program / a whole Doc ----------------------------\nexport { lint, lintReport, localVariables, type LintContext } from './lint'\nexport {\n lintDoc, lintDocReport, docHasErrors, docLintContext, allScopeVariables,\n scopeProgram, docStructureWarnings, docLayoutWarnings,\n} from './programDoc'\n\n// --- Manifest / LLM context for a Doc -----------------------------------------\nexport { manifestObjects, manifestEvents, docToManifest, llmContext, type ManifestObject } from './manifest'\n\n// --- The reference cards: behavior (languageCard) and composition (drawingCard) -\nexport { languageCard } from './languageCard'\nexport { drawingCard } from './drawingCard'\n\n// --- Scope-program helpers (split/join the per-object behavior blocks) ---------\nexport { splitScopeProgram, scopeRegions, formatObjectBlock, joinScopeProgram } from './scopeProgram'\n\n// --- The flatc CLI entry point (also wired as the `flatc` bin) -----------------\n// `run` (the CLI entry) is deliberately NOT re-exported. It lives in a module that imports `fs`/`path` at\n// the top, so re-exporting it put Node builtins in the root's chunk graph -- and a browser bundle then\n// fails at NAME RESOLUTION (`\"extname\" is not exported by \"__vite-browser-external\"`), before tree-shaking\n// can drop the unused symbol. The root's job is the pure helpers a service or a browser needs\n// (`checkProgram`, `languageCard`, `drawingCard`, `docToManifest`); the CLI's job is the `bin`, which\n// imports `./cli/flatc` directly. `scripts/check-pack.mjs` walks the built chunk graph to keep it that way.\n\n// --- The language layer, re-exported from the engine for convenience ----------\nexport { parseUnits, printUnits } from '@flatkit/engine/dsl'\nexport {\n parseProgram, printProgram, parseProgramFull, printProgramFull,\n parseFlat, parseFlatLib, exportFlatProject,\n} from '@flatkit/engine/flatFormat'\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWA,SAAS,iBAAAA,gBAAe,uBAAuB;AAC/C,SAAS,WAAW,SAAS,SAAS,YAAY,YAAY,cAAc;AAC5E,SAAS,mBAAmB;;;ACL5B,SAAS,eAAe,qBAAqB;AAC7C,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AAGlB,SAAS,eAAuB;AACrC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCAuB0B,cAAc,KAAK,GAAG,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,aAc7C,cAAc,KAAK,GAAG,CAAC;AAAA,aACvB,cAAc,KAAK,GAAG,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCAkBD,SAAS,KAAK,GAAG,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASrD;;;AChEO,SAAS,cAAsB;AACpC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8GT;;;AF3GA,SAAS,UAAU,KAAU,IAA6C;AACxE,MAAI,WAAW,EAAE,GAAG;AAAE,UAAM,IAAI,UAAU,KAAK,GAAG,QAAQ;AAAG,WAAO,IAAI,YAAY,EAAE,IAAI,KAAK;AAAA,EAAW;AAC1G,MAAI,OAAO,EAAE,EAAG,QAAO;AACvB,MAAI,QAAQ,EAAE,EAAG,QAAO;AACxB,SAAO;AACT;AA0BA,SAAS,eAAe,KAAuB;AAC7C,QAAM,MAAM,IAAI,IAAI,OAAO,KAAK,IAAI,aAAa,CAAC,CAAC,CAAC;AACpD,gBAAc,KAAK,CAAC,MAAM;AACxB,QAAI,EAAE,OAAO,YAAY,EAAE,OAAO,WAAY,KAAI,IAAI,EAAE,IAAI;AAAA,aACnD,EAAE,OAAO,cAAe,KAAI,IAAI,EAAE,GAAG;AAAA,EAChD,CAAC;AACD,aAAW,KAAK,IAAI,eAAe,CAAC,EAAG,YAAW,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAG,KAAI,EAAG,KAAI,IAAI,CAAC;AACjG,SAAO;AACT;AAMA,SAAS,QAAQ,MAAc,OAAoB,MAAyB;AAC1E,QAAM,IAAI,YAAY,IAAI;AAC1B,MAAI,CAAC,EAAE,GAAI;AACX,aAAW,MAAM,EAAE,KAAK,IAAK,KAAI,MAAM,IAAI,EAAE,EAAG,MAAK,IAAI,EAAE;AAC7D;AAIO,SAAS,gBAAgB,KAA4B;AAC1D,QAAM,MAAwB,CAAC;AAC/B,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,QAAQ,eAAe,GAAG;AAChC,QAAM,YAAY,IAAI,KAAK,IAAI,gBAAgB,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,UAAU,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAc,CAAC;AAC3H,QAAM,OAAO,CAAC,WAAoB;AAChC,eAAW,KAAK,OAAQ,YAAW,MAAM,EAAE,OAAO;AAChD,UAAI,WAAW,EAAE,KAAK,GAAG,QAAQ,CAAC,KAAK,IAAI,GAAG,IAAI,GAAG;AACnD,aAAK,IAAI,GAAG,IAAI;AAChB,cAAM,QAAQ,IAAI,gBAAgB,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,EAAE;AACxE,cAAM,WAAqB,CAAC;AAC5B,cAAM,QAAQ,oBAAI,IAAY;AAC9B,8BAAsB,IAAI,CAAC,MAAM,YAAY;AAAE,mBAAS,KAAK,OAAO;AAAG,kBAAQ,MAAM,OAAO,KAAK;AAAA,QAAE,CAAC;AACpG,mBAAW,KAAK,KAAM,yBAAwB,EAAE,SAAS,CAAC,SAAS,QAAQ,MAAM,OAAO,KAAK,CAAC;AAC9F,YAAI,KAAK;AAAA,UACP,MAAM,GAAG;AAAA,UACT,MAAM,UAAU,KAAK,EAAE;AAAA,UACvB,QAAQ,CAAC,GAAG,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAAA,UAC7C,SAAS,CAAC,CAAC,IAAI,aAAa,KAAK,CAAC,MAAM,EAAE,aAAa,GAAG,EAAE;AAAA,UAC5D,MAAM,UAAU,IAAI,GAAG,IAAI,KAAM,QAAQ,EAAE,KAAK,CAAC,CAAC,GAAG;AAAA,UACrD;AAAA,UACA,OAAO,CAAC,GAAG,KAAK;AAAA,QAClB,CAAC;AAAA,MACH;AACA,UAAI,QAAQ,EAAE,EAAG,MAAK,GAAG,MAAM;AAAA,IACjC;AAAA,EACF;AACA,OAAK,IAAI,MAAM;AACf,SAAO;AACT;AAIO,SAAS,eAAe,KAAoB;AACjD,QAAM,SAAS,oBAAI,IAAY;AAC/B,gBAAc,KAAK,CAAC,MAAM;AAAE,QAAI,EAAE,OAAO,OAAQ,QAAO,IAAI,EAAE,KAAK;AAAA,EAAE,CAAC;AACtE,SAAO,CAAC,GAAG,MAAM;AACnB;AAGA,SAAS,cAAc,GAA6B;AAClD,QAAM,QAAkB,CAAC;AACzB,MAAI,EAAE,QAAS,OAAM,KAAK,MAAM;AAChC,MAAI,EAAE,KAAM,OAAM,KAAK,MAAM;AAC7B,MAAI,EAAE,OAAO,OAAQ,OAAM,KAAK,MAAM,EAAE,OAAO,KAAK,GAAG,CAAC,EAAE;AAC1D,MAAI,EAAE,SAAS,OAAQ,OAAM,KAAK,WAAW,EAAE,SAAS,KAAK,GAAG,CAAC,EAAE;AACnE,MAAI,EAAE,MAAM,OAAQ,OAAM,KAAK,UAAU,EAAE,MAAM,KAAK,GAAG,CAAC,EAAE;AAC5D,SAAO;AACT;AAGA,SAAS,aAAa,KAAoB;AACxC,SAAO,OAAO,QAAQ,IAAI,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAO,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,EAAG;AACnH;AAMO,SAAS,cAAc,KAAkB;AAC9C,QAAM,OAAO,gBAAgB,GAAG;AAChC,QAAM,UAAU,IAAI,UAAU,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,IAAI,EAAE,EAAE,EAAE;AAChE,QAAM,OAAO,aAAa,GAAG;AAC7B,QAAM,SAAS,IAAI,aAAa,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG;AAClF,QAAM,QAAQ,CAAC,WAAW,SAAS,IAAI,KAAK,IAAI,IAAI,MAAM,EAAE;AAC5D,MAAI,KAAK,OAAQ,OAAM,KAAK,YAAY,KAAK,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,IAAI,EAAE,IAAI,GAAG,EAAE,KAAK,IAAI,CAAC,EAAE;AAC5F,MAAI,OAAO,OAAQ,OAAM,KAAK,WAAW,OAAO,KAAK,IAAI,CAAC,EAAE;AAC5D,MAAI,KAAK,OAAQ,OAAM,KAAK,SAAS,KAAK,KAAK,IAAI,CAAC,EAAE;AACtD,MAAI,MAAM,OAAQ,OAAM,KAAK,UAAU,MAAM,KAAK,IAAI,CAAC,EAAE;AACzD,MAAI,IAAI,SAAS,OAAQ,OAAM,KAAK,aAAa,IAAI,QAAQ,KAAK,IAAI,CAAC,EAAE;AAGzE,QAAM,WAAW,KAAK,IAAI,CAAC,OAAO,EAAE,GAAG,OAAO,cAAc,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM,MAAM;AACrG,MAAI,SAAS,QAAQ;AACnB,UAAM,KAAK,+CAA+C;AAC1D,eAAW,EAAE,GAAG,MAAM,KAAK,SAAU,OAAM,KAAK,KAAK,EAAE,IAAI,MAAM,MAAM,KAAK,IAAI,CAAC,EAAE;AAAA,EACrF;AACA,QAAM,SAAS,eAAe,GAAG;AACjC,MAAI,OAAO,OAAQ,OAAM,KAAK,WAAW,OAAO,KAAK,IAAI,CAAC,EAAE;AAC5D,QAAM,KAAK,aAAaC,eAAc,KAAK,GAAG,CAAC,wBAAwB,gBAAgB,KAAK,GAAG,CAAC,mBAAmB;AACnH,SAAO,MAAM,KAAK,IAAI;AACxB;AAYO,SAAS,WAAW,KAAU,OAA8B,CAAC,GAAW;AAC7E,QAAM,QAAQ,KAAK,YAAY,QAAQ,aAAa,IAAI,GAAG,aAAa,CAAC;AAAA;AAAA,EAAO,YAAY,CAAC;AAC7F,SAAO,GAAG,KAAK;AAAA;AAAA,EAAO,cAAc,GAAG,CAAC;AAC1C;;;AG/HA,SAAS,YAAY,kBAAkB;AACvC;AAAA,EACE;AAAA,EAAc;AAAA,EAAc;AAAA,EAAkB;AAAA,EAC9C;AAAA,EAAW;AAAA,EAAc;AAAA,OACpB;","names":["EXPR_CHANNELS","EXPR_CHANNELS"]}
@@ -163,13 +163,99 @@ Higher-level pointer behaviors (each writes into your variables; all accept `{ e
163
163
  turn <angle> around <x>,<y> [{ snap <deg> }] # dial / clock hand → angle in RADIANS → rotation = angle
164
164
  turnDeg <angle> around <x>,<y> [{ snap <deg> }] # …in DEGREES → rotationDeg = angle (rotationDeg = sugar for rotation = rad(…))
165
165
  trace <progress> along <Group> [{ tolerance <px> }]# follow a path → progress 0..1 (monotone)
166
- reveal <progress> [{ brush <px> }] # scratch/wipe the grabbed area → fraction 0..1 (cumulative across grabs)
166
+ reveal <progress> [{ brush <px> · erase · cells <array> }] # scratch/wipe the grabbed area → fraction 0..1 (cumulative across grabs)
167
167
  link <endX>, <endY>, <target> to <Group> # pull a thread → end follows the pointer; <target> = hit index 1..n on release (0 = none)
168
168
  ```
169
169
 
170
170
  Each output also accepts an **array element** (`drag hx[i], hy[i]`, `reveal seen[2]`) — the natural form
171
171
  under `each` (see below).
172
172
 
173
+ ### Drawing what a `trace` traced
174
+
175
+ `trace` gives you **how far** along the path the finger got, in arc length. Feed it to a shape's
176
+ [`draw`](scene-and-drawing.md#drawing-a-stroke-progressively-draw) — the same path, the same measure — and
177
+ the ink appears exactly under the finger:
178
+
179
+ ```
180
+ scene { layer "Ink" {
181
+ group "Route" { layer "c" { path "M60 300C220 120 340 480 500 300" nofill stroke #dddddd 18 cap round } }
182
+ path "M60 300C220 120 340 480 500 300" nofill stroke #2255ff 18 cap round draw "progress" nohit
183
+ } }
184
+
185
+ object "Route" { trace progress along Route { tolerance 30 } }
186
+ ```
187
+
188
+ The guide and the ink carry the **same path data**: `trace` measures on it, `draw` cuts on it. (Driving a
189
+ rectangular mask by `scaleX` instead ties the ink to *screen x*, which drifts from path length wherever the
190
+ curve is steep.)
191
+
192
+ ### Rubbing a veil out (`reveal … erase`)
193
+
194
+ `reveal <p>` reports how MUCH of the zone was cleared. Add **`erase`** and the runtime *shows* it: the
195
+ grabbed object is drawn minus what the finger rubbed out. A scratch card is then a grey rectangle:
196
+
197
+ ```
198
+ scene { layer "Jeu" {
199
+ text "BRAVO" at 90,120 font "sans-serif" size 64 align left line 1.2 color #10141c
200
+ group "Veil" at 200,150 { layer "c" { rect -180 -120 360 240 fill #8a94a6 } }
201
+ } }
202
+
203
+ object "Veil" {
204
+ reveal cleared {
205
+ brush 28
206
+ erase // the veil disappears under the finger — no cell artwork to author
207
+ }
208
+ }
209
+ ```
210
+
211
+ Nothing else to write: no grid of tiles, no `each`, no array. What disappears is exactly what the fraction
212
+ counts (one disc per cleared cell, sized to merge with its neighbours), so `cleared` and the picture always
213
+ agree. Erasing is **visual only** — the zone stays grabbable where it has been cleared, which is what lets
214
+ the scratching continue there.
215
+
216
+ > **Why not a `mask` layer?** Because a mask is an even-odd **clipping path**: two overlapping brush stamps
217
+ > *cancel* instead of accumulating, and nothing in the language creates a stamp at the pointer anyway
218
+ > (the scene's geometry is fixed; only channels move). `erase` does the accumulation in the runtime, where
219
+ > the gesture already keeps the state.
220
+
221
+ ### Seeing WHERE it was scratched (`reveal … cells`)
222
+
223
+ `erase` rubs the veil out for you. `cells <array>` is the other half: it hands you the grid behind the
224
+ number — **where** — for when the scene has to *react* to the uncovered area rather than just show it
225
+ (score a region, light up the object underneath, drive your own cell artwork):
226
+
227
+ ```
228
+ var cleared = 0
229
+ var scratched = fill(551, 0) # one slot per cell; --check tells you the count
230
+
231
+ object "Veil" {
232
+ reveal cleared {
233
+ brush 32
234
+ cells scratched # scratched[i] = 1 once cell i is cleared
235
+ }
236
+ }
237
+
238
+ each "Cell" as i { opacity = 1 - scratched[i] } # …and the veil disappears where it was rubbed
239
+ ```
240
+
241
+ The grid is derived from the zone, so it is reproducible on paper: it covers the object's **world bbox**,
242
+ each cell is a **`brush` × `brush`** square, `cols = ceil(width / brush)`, `rows = ceil(height / brush)`,
243
+ and **`i = row * cols + col`** (cell `(col,row)` is centred at `minX + (col + 0.5) * brush`,
244
+ `minY + (row + 0.5) * brush`). A cell is cleared once its **centre** falls within `brush` of the pointer —
245
+ so a single touch clears a small plus-shape, not one square. Cells are written **once**, never back to 0:
246
+ the grid is as monotone as the fraction, and both agree. (The array is yours to read *and* write, but the
247
+ coverage behind it has no reset — so each new grab re-syncs the array from the interactor's own state,
248
+ rather than letting a scene show an intact cell over a zone counted as cleared.)
249
+
250
+ Declare the array at exactly `cols * rows` — `flatc --check` states the geometry and the exact
251
+ `var … = fill(N, 0)` to write whenever the sizes disagree, because a short array drops the writes past its
252
+ end in silence.
253
+
254
+ > A `reveal` target is grabbable **over its whole zone**, whatever its content currently looks like — under
255
+ > `erase`, and equally when the cells you fade to `opacity 0` stop being hittable (invisible things let the
256
+ > pointer through). Without that rule the scratching would work on the first stroke and then stall on the
257
+ > cleared area — invisible in a static render, visible only in a replayed `down/move/up`.
258
+
173
259
  ### Drawing the thread of a `link`
174
260
 
175
261
  `link` gives you the end position and the target index; **the visible wire is yours to draw**. The idiom:
@@ -62,6 +62,16 @@
62
62
  coords) — e.g. hide the "feet" of an emerging shape: `group "Arc" clip -100 -60 200 60 { … }`. For an
63
63
  arbitrary clip shape, use a `mask` layer instead. **Render-only**: hit-testing and the preview/auto-size
64
64
  bbox ignore it (clipped-away area stays clickable / counts toward the framing) — it's a visual cut.
65
+ - **A `mask` layer is a CLIPPING PATH in even-odd, not an alpha mask.** Its material contributes its
66
+ *outline*, nothing else: an alpha in the paint is ignored (`fill radial(…)` → hard edge, no falloff), a
67
+ `filter blur` on the matter changes nothing, and **two overlapping shapes cancel** where they overlap (a
68
+ third brings it back). So "stamp a soft brush repeatedly into a mask" does not accumulate — it flickers
69
+ holes in and out. For a scratch/wipe, do not build a mask at all: `reveal … { erase }` accumulates in the
70
+ runtime. For a progressive line, trim a stroke with
71
+ [`draw`](scene-and-drawing.md#drawing-a-stroke-progressively-draw); for a sliding window, pilot a GROUP
72
+ used as the mask's matter (a rectangle whose `scaleX` follows the progress).
73
+ - **A partially drawn stroke (`draw`) still hits over the WHOLE path**: the trim is visual (like `clip`).
74
+ Ink meant to be untouchable takes `nohit`.
65
75
 
66
76
  ## Text
67
77
 
@@ -174,12 +184,26 @@ Inside an `object "Name" { … }`, besides `drag x, y` / `dragX` / `dragY`:
174
184
  path with the finger. While the pointer stays within `tolerance` of the trace (the regions
175
185
  of the named group), `<progress>` rises from 0 to 1 (monotone, never goes back down).
176
186
  Great for tracing a letter, a border, a constellation. (`tolerance` defaults to 24 px.)
177
- - **`reveal <progress> [{ brush <px> · enabled <expr> }]`**: scratch / wipe. The grabbed
178
- object IS the area to reveal; rubbing it ticks the cells of an internal grid (cell side =
187
+ - **`reveal <progress> [{ brush <px> · erase · cells <array> · enabled <expr> }]`**: scratch / wipe. The grabbed
188
+ object IS the area to reveal; rubbing it ticks the cells of a grid (cell side =
179
189
  `brush`) and `<progress>` rises from 0 to 1 (monotone, and **cumulative across separate grabs**
180
190
  — a child rubbing in several short strokes keeps adding coverage, it does not reset). Drive a
181
191
  cover's opacity with `opacity = 1 - <progress>`. Great for scratch cards, fogged glass, digging.
182
192
  (`brush` defaults to 24 px; coverage model, no pixel mask.)
193
+ - **`erase`** makes the RUNTIME rub the target out where it was scratched (one disc per cleared cell,
194
+ accumulating) — a scratch card is then a grey rectangle and nothing else. A `mask` layer cannot do
195
+ this: its matter is an even-odd clip path, so two overlapping stamps cancel, and no construct creates
196
+ a stamp at the pointer.
197
+ - **`cells <array>`** hands you that grid — **where** it was scratched, not only how much:
198
+ `grille[i] = 1` once cell `i` is cleared, `i = row * cols + col` over the object's world bbox,
199
+ `cols = ceil(w / brush)`. One `each "Grain" as i { opacity = 1 - grille[i] }` then erases the veil
200
+ under the finger. Declare the array at exactly `cols * rows` — `--check` states the number.
201
+ See [Behavior](behavior-and-interactions.md#seeing-where-it-was-scratched-reveal--cells).
202
+ - A `reveal` target is grabbable **over its whole zone**, whatever its content looks like — so a veil
203
+ stays scratchable where its cells have already gone to `opacity 0`. (Everywhere else, an item at
204
+ `opacity 0` lets the pointer through; a group whose children are ALL invisible is not hit, and
205
+ `hitbox` does not change that — it is the drop-zone rectangle, not a hit surface. Only a replayed
206
+ `down/move/up` script shows this class of bug: a static render looks perfect.)
183
207
  - **`link <endX>, <endY>, <target> to <TargetsGroup> [{ enabled <expr> }]`**: pull an elastic
184
208
  thread toward a target. During the drag, `<endX>`/`<endY>` = pointer position (DRAW the
185
209
  thread yourself with expressions, e.g. a region connecting the object to `endX,endY`). On
@@ -35,6 +35,31 @@ path "…" nofill stroke #888 2 # outline only
35
35
  rect 0 0 40 40 fill #00aaff opacity 0.5 # 0..1 (8-digit hex alpha also works)
36
36
  ```
37
37
 
38
+ ### Drawing a stroke progressively (`draw`)
39
+
40
+ `draw` sets **how much of an outline is stroked**, as a fraction of its **arc length** — ink that appears
41
+ behind a finger, a signature that writes itself, a route that grows:
42
+
43
+ ```
44
+ path "…" nofill stroke #fff 18 cap round draw 0.35 # the first 35 % of the LINE's length
45
+ path "…" nofill stroke #fff 18 cap round draw "progress" # …driven by a variable, every frame
46
+ path "…" nofill stroke #fff 18 draw "p" from "p - 0.15" # a WINDOW: a comet trail chasing `p`
47
+ ```
48
+
49
+ - `draw <to>` = the end of the drawn window (`1` = whole, the default), `from <start>` its beginning (`0`
50
+ by default). A **quoted** value is an expression re-evaluated per frame; a bare number is fixed. Both are
51
+ clamped to 0..1, and `from > to` draws nothing.
52
+ - The measure is **arc length**, the same one [`trace`](behavior-and-interactions.md#interactors) reports
53
+ and text-on-path's `start` uses. So `draw = <the trace's progress>` puts the ink exactly under the
54
+ finger — on a steep slope, where an x-driven mask drifts by whole stroke-widths.
55
+ - Several subpaths are traversed **in order**: the first is drawn whole before the next starts (a letter's
56
+ stem, then its dot).
57
+ - It trims the **stroke only**. The fill, the gradient box, the bbox and the hit shape stay those of the
58
+ whole path — so a half-drawn outline over a full disc is what you asked for, and an ink trail that must
59
+ not catch the pointer takes `nohit`.
60
+ - `draw` on a shape **without a stroke** has nothing to trim; `flatc --check` says so rather than letting
61
+ you animate an invisible thing.
62
+
38
63
  ### Paints (gradients)
39
64
 
40
65
  ```
@@ -82,6 +107,11 @@ mask layer "Window" { circle 60 60 50 fill #fff layer "c" { … } } // arbitra
82
107
  framing) — it's a visual cut, not a hit/layout change.
83
108
  - For an **arbitrary** clip shape, use a **`mask` layer**: its material (the shapes drawn directly in it)
84
109
  clips its **child layers**. See the [gotchas](dsl-gotchas.md) for the clip/mask details.
110
+ - ⚠️ A mask is a **clipping path, evaluated even-odd — not an alpha mask.** Only the *outline* of its
111
+ material counts: a `fill radial(…)` in it gives a hard edge (no soft falloff), a `filter blur` on it
112
+ changes nothing, and two overlapping shapes **cancel** where they overlap (a third brings it back).
113
+ A soft or accumulating reveal is not a mask — animate the matter itself (a piloted group as the mask's
114
+ material, or `draw` on a stroke).
85
115
 
86
116
  ## Text
87
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkit/compiler",
3
- "version": "0.31.1",
3
+ "version": "0.32.0",
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.31.1",
61
- "@flatkit/types": "0.31.1",
62
- "@flatkit/player": "0.31.1"
60
+ "@flatkit/engine": "0.32.0",
61
+ "@flatkit/player": "0.32.0",
62
+ "@flatkit/types": "0.32.0"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "skia-canvas": "^3.0.8"
@@ -64,8 +64,14 @@ fill radial(0.5, 0.5, 0.5, 0:#fff, 1:#000) // cx, cy, r (0..1), then sto
64
64
  filter glow <blur> <color> | shadow <dx> <dy> <blur> <color> | blur <r> | adjust <b> <c> <s> <h>
65
65
  tint <color> <amount(0..1)> // Flash-style tint
66
66
  nohit // drawn but ignored by hit-test
67
+ draw <to> [from <start>] // stroke extent by ARC LENGTH (0..1); quoted = expression
67
68
  ```
68
69
 
70
+ `draw` is how a line DRAWS ITSELF: `path "…" nofill stroke #fff 18 cap round draw "avance"` strokes the
71
+ first `avance` of the path's LENGTH — the same measure `trace` reports, so ink lands under the finger even
72
+ on a steep curve (an x-driven mask does not). `from` opens a window (comet trail). It trims the stroke
73
+ only: fill, bbox and hit shape stay whole.
74
+
69
75
  ## Animation — timeline / cel / pose
70
76
 
71
77
  Cels are how anything moves on a timeline, and they work in BOTH halves: inside a `symbol` (a reusable
@@ -176,11 +182,24 @@ drag x, y [{ confine to <Zone>
176
182
  enabled <expr> }] // ONE OPTION PER LINE. dragX / dragY too
177
183
  turn <angle> around <x>,<y> [{ snap <deg> }] // → <angle> in RADIANS → rotation = <angle> directly
178
184
  turnDeg <angle> around <x>,<y> [{ snap <deg> }] // → <angle> in DEGREES → pair with rotationDeg = <angle>
179
- trace <progress> along <Group> [{ tolerance <px> }]// follow a path → 0..1 monotone
180
- reveal <progress> [{ brush <px> }] // scratch/wipe → 0..1 cumulative
185
+ trace <progress> along <Group> [{ tolerance <px> }]// follow a path → 0..1 monotone (pairs with `draw`)
186
+ reveal <progress> [{ brush <px>
187
+ erase // …and the runtime RUBS THE TARGET OUT where it was scratched
188
+ cells <array> }] // scratch/wipe → 0..1 cumulative; `cells` = WHERE (1 per cleared cell)
181
189
  link <endX>,<endY>,<target> to <Group> // elastic thread → target = hit index 1..n (0=none)
182
190
  ```
183
191
 
192
+ **`trace` + `draw` is the tracing exercise**: give the guide shape and the ink shape the SAME path data,
193
+ `trace avance along Chemin` on one, `draw "avance"` on the other — the ink follows the finger by arc
194
+ length. **A scratch card is `reveal cleared { brush 28 · erase }` on a grey rectangle** — nothing else: `erase`
195
+ makes the runtime rub the veil out under the finger (a `mask` layer CANNOT do it, its matter is an even-odd
196
+ clip path where two overlapping stamps cancel). **`reveal … cells grille`** is the other half, for a scene
197
+ that must REACT to the uncovered area: it writes `grille[i] = 1` for each cleared cell (`i = row * cols + col`,
198
+ `cols = ceil(zone_width / brush)` over the object's world bbox), so `each "Grain" as i {
199
+ opacity = 1 - grille[i] }` erases the veil WHERE it was rubbed. Declare `var grille = fill(cols*rows, 0)` —
200
+ `--check` states the exact number. A `reveal` target stays grabbable over its whole zone even once its
201
+ cells are invisible.
202
+
184
203
  **`link` gives you the end point and the target index -- it does NOT draw the thread.** Nobody writes
185
204
  anything but these two lines, so here they are, as a program that compiles:
186
205
 
@@ -38,6 +38,8 @@ group "Name" at x,y pivot px,py { layer "c" { … } } // nests its own laye
38
38
  instance "Symbol" as "Name" at x,y // place a symbol from a .flat
39
39
  ```
40
40
  Style: `fill #rrggbb | nofill` · `stroke #rgb <w> [cap round][join round][dash a,b]` · `opacity 0..1` ·
41
+ `draw <0..1> [from <0..1>]` (stroke extent by ARC LENGTH; quoted = expression: `draw "avance"` = ink drawn
42
+ behind a finger, the measure `trace` reports) ·
41
43
  `fill linear(90, 0:#a, 1:#b)` (0=→,90=↓) · `fill radial(0.5,0.5,0.5, 0:#fff,1:#000)` ·
42
44
  `filter glow <blur> <color> | shadow <dx> <dy> <blur> <color> | blur <r>` · `tint <color> <amt>` · `nohit`.
43
45
 
@@ -73,7 +75,10 @@ Drag/interactors (write into your vars; all take `{ enabled <expr> }`):
73
75
  ```
74
76
  drag x, y [{ confine to <Zone>
75
77
  snap <grid> }] // ONE OPTION PER LINE. Then USE them: x = px, y = py
76
- turn <angle> around x,y · trace <progress> along <Group> · reveal <progress>
78
+ turn <angle> around x,y · trace <progress> along <Group> // pair with `draw "<progress>"` on the SAME path data
79
+ reveal <progress> [{ brush <px>
80
+ erase // the runtime rubs the target out where scratched (a scratch card = a grey rect + this)
81
+ cells <array> }] // fraction + WHERE: cells[row*cols+col] = 1, cols = ceil(zone_w/brush)
77
82
  link endX,endY,target to <Group> // target = hit index 1..n (0=none), WORLD coords
78
83
  // link draws NO thread. A bar drawn from its own origin, then:
79
84
  // rotation = angle(srcX, srcY, endX, endY) scaleX = dist(srcX, srcY, endX, endY) / <drawn length>