# Jabbertoon, in full Jabbertoon (https://jabbertoon.com) is a free cartoon studio that runs in the web browser. This file holds, in plain text, the public documentation (the block language, the pack format, the link formats and the JSON Schemas) followed by every pack Jabbertoon ships: its id, name, description and program. Every shipped pack is CC0. The short overview is https://jabbertoon.com/llms.txt. From code or an AI assistant, today, the free tools list the moves, characters and looks, give a move's program and its readable text, check a move, a character or a cartoon, and make links that open a move (in the block editor, where it plays) or a character (in the studio or the character creator); links that play a whole cartoon, and turning a script into a video in code, are coming in the next release, and the link formats below say what each link opens today. ================================================================ # Jabbertoon docs https://jabbertoon.com/docs/ These pages describe what a Jabbertoon move, character, scene and cartoon are, as data, so that people, programs and AI assistants can read and write them. Everything here is open: the code is Apache-2.0 and every shipped pack is CC0. To get started quickly, the [developers' page](/developers/) has a five-minute quickstart with no key, and the [page for AI assistants](/for-ai/) lists the free tools. ================================================================ # The Jabbertoon block language https://jabbertoon.com/docs/language/ Every move, scene and cartoon on Jabbertoon is a program in this language: JSON that people see as blocks, as text or as a timeline. This is the public edition of the specification. The JavaScript engine in the browser is the reference implementation, and a Python port follows it to within 1e-9 on every recorded case. ## Which hats fire today A script starts with a hat, the block that says when it runs. These hats run today: **start** (when the move starts), **talk-start** and **talk-end** (when the character starts and stops speaking a line), and **scene** (when a scene of a cartoon starts). In the [block editor](/blocks/), **key** and **click** hats also fire, for the one character on its stage. Four more hats, **touch**, **beat**, **loud** and **every**, are part of the language: programs that use them load, validate and save without losing anything, but nothing makes them fire yet, so a script under one of them never runs. They are named future features. ## The specification ## 0. Non-negotiables - **Exact time.** A timed block that starts at t and lasts d ends at exactly t+d, never at "the next frame". The runtime is a discrete-event scheduler: `stepTo(T)` processes every block event with time ≤ T in time order, then the pose is read at T. So a pose depends only on T, not on how the clock was stepped (24 fps, sample jumps, one big jump all agree bit-for-bit; tested). - **The only step-dependent things are polls:** `while`/`until` conditions are checked once per step, and a loop iteration that took zero time waits for the next step (no infinite loops). Behaviours meant to be exact use timed blocks (`for`, `wait until`), not polls. - **Deterministic.** No wall clock. `random` is a seeded PRNG per runtime (mulberry32). - **Two clocks.** `timeline` (video: the renderer calls `stepTo(frame/fps)`) and `live` (play: the page steps by the real time elapsed, `step(dt)`, and sends input events). Every block works under both; no block can read the step size (§5: there is no `dt` name). - **Units.** Positions in 1080p pixels (renderers scale by min(W,H)/1080). Angles in degrees, **positive = counter-clockwise as the viewer sees it** (the original studio's PIL convention; canvas renderers draw −angle). Scales are factors. Time in seconds. A scene's **placement** of a character is in stage fractions (x of the width, y and h of the height; §7.2); behaviour offsets add to it. ## 1. Program JSON ``` program := { "v": 1, "kind": "behaviour"|"scene"|"cartoon", "name"?, "params"?: {name: value}, "vars"?: {name: number}, "scripts": [script] } // scene/cartoon add "cast", "places" (§7.1) script := { "hat"?: hat, "body": [block] } // no hat = { "on": "start" } hat := {"on":"start"} | {"on":"talk-start"} | {"on":"talk-end"} | {"on":"key","key"?} | {"on":"click"} | {"on":"scene","name"?,"place"?,"mood"?,"at"?} | {"on":"beat"} | {"on":"loud"} | {"on":"every"} | {"on":"touch"} block := { "op": string, ...args } // "note" and "id" are allowed on any block value := number | "=expression" // see §5 ``` `params` are defaults; the caller's params override them. Defaults are evaluated at start, in declaration order, and may use `dur` and earlier params (e.g. `"t_in": "=min(max(dur*0.6, 0.5), 3.0)"`). A variable starts at a finite number: a formula, text, `true`, `null`, a list or an object is a validation error (`vars.x: a variable starts at a number, not "=1"`); `setVar` gives a variable any value as the script runs. `vars` that is not an object adds no variables (`null`, `5`, `[]`); a list or a string adds its entries by index, which are checked the same way (a string's entries are its UTF-16 code units, as `Object.entries` gives them: an emoji is two, `vars.0` and `vars.1`). ## 2. The pose model A character runs behaviour **instances** (`startBehaviour(char, program, {dur, params})`). Each instance owns a layer with two kinds of state: - **Base channels** — absolute values written by `set` and `ramp`. One value per channel per instance; the last writer wins; a ramp starts from the channel's current value and then **holds** the target. - **Motions** — `wave`, `hop`, `gait`, `drive`. Continuous functions of time. A motion block does not wait: it starts the motion and the script continues. A motion lives until its **scope** ends. **Scopes.** The script's top level is the instance's root scope: motions started there live until the instance is stopped (even after the script's last block). `for` and `while` open a scope; when the `for`'s time is up or the `while` exits, the motions started inside stop. `together` branches start motions in the enclosing scope. `if`, `repeat` and `forever` bodies do not open scopes. **Start-or-keep.** Running a motion block whose motion is already running in the same scope does nothing (it does not restart or stack). `forever { bob; wait 1 }` bobs once, continuously. **Combining.** The character's pose = rest ⊕ every instance's base ⊕ every motion, where `dx dy rot` and part channels add, `sx sy` multiply, `mouth blink` take the max, `mood` takes the latest write. Rest is 0 (1 for `sx sy`). The skeleton adds two pose keys that are not channels (§8): `reach` (per hand or foot, the latest `reach`/`release` wins) and `view` (the latest `view` wins, like `mood`). **Channels.** `dx dy rot sx sy mouth blink mood`, and part channels `part:[:][#]` (angle, deg) / `lift:[:][#]` (px, +up). side is −1 left / 1 right; the index's meaning depends on the role: on legs (`leg shin foot`) it numbers the legs back to front (quadruped: #0 back-left, #1 back-right, #2 front-left, #3 front-right); on a **chain** (a tail, the spine, ears, hair; §8.2) it numbers the segments from the root (`tail#0` is next to the body; the skeleton's blocks write segment 0 on the plain channel, `part:tail`, §8.2). A drawn node with role R, side S, index I reads the **sum** of `R`, `R:S`, `R#I`, `R:S#I`, so "both arms" and "left arm" combine — except a chain follower (segment i ≥ 1 of a chain the template draws), which reads only `R#i` and `R:S#i` (§8.1; `docs/SHAPES.md` §3.2). Roles: head neck torso hips arm forearm hand leg shin foot tail ear wing fin claw snout nose hair hat eye brow (`engine/parts.js` `ROLES`, the same list as the roles `ui/shapes.js` draws; no template draws a neck yet, so a block that moves the neck is a warning, v0.4.1, §8.6). Validation: a channel that does not have this form (`part:Arm`, `lift:arm:2`, `part:__proto__`) is an error; a well-formed part channel of any other lower-case role (`part:elbow`) is a channel like the others (it sums, `rt.pose` reports it), but nothing draws it, so it is a warning. **Renderer conventions (part synonyms).** The engine computes channels; it does not know which body a character has. So that every character answers the same moves, the shape renderer (`ui/shapes.js` `applyPose`) also lets some drawn parts read other roles' channels, added to their own: - a **wing** reads `arm` plus half of `forearm` at its root, so "lift arms" flaps the wings and "wave" waves a wing; - a **hand** reads `claw` (the original crab moves wiggle a person's hands); - on a **four-legged** body the front legs (`leg` #2, #3) read 40 % of `arm` and their shins (`shin` #2, #3) 40 % of `forearm`; in profile the left side's angle is negated (a profile body has no outward side, so "lift arms" raises both front legs forward: a dog "waves" a front paw); seen head on it is used as it is (outward, like a person's arm); - (v0.4.1) the part channels name the limb ends a reach names (§8.4): `part:hand` (`hand:S`) moves a four-legged body's front paws (which read the foot channels only by their leg number, `part:foot#2`), a bird's wing tip (the outer half of its wing) and a monster's claw; `part:foot` moves a four-legged body's back paws; - (v0.4.1) a part a template draws with fewer segments than a chain block bends (a one-piece torso, tail or ear) shows the whole bend: its last drawn segment also reads the index channels of the segments it does not draw. These are drawing rules, not engine semantics: the pose (`rt.pose`) never contains them, and another renderer may choose its own (the Python studio adapter does not apply them). ## 3. Primitive blocks (the runtime executes only these) Timing args: `over` (seconds; 0 = instant), `ease` ∈ `linear | smooth (smoothstep, default) | in (u²) | out (sin(uπ/2)) | inout (=smooth) | snappy (overshoot)`. | op | args | waits? | meaning | |---|---|---|---| | `set` | `to: {ch: value}` | no | base channels jump to values | | `ramp` | `to` and/or `by: {ch: value}`, `over`, `ease` | `over` s | glide base channels; a glide cut off by a scope end or a stop freezes where it is | | `wait` | `s` or `until` (behaviour time) | yes | | | `for` | `s` or `until`, `body` | until the time is up | runs body; aborts it at the end; ends its scope | | `while` | `cond`, `body` | polls | scope; condition checked each iteration/step | | `until` | `cond` | polls | wait until cond | | `repeat` | `n`, `body` | — | | | `forever` | `body` | — | | | `together` | `branches: [[block]]` | until all end | branches run as parallel threads | | `if` | `cond`, `body`, `else` | — | | | `stop` / `stopAll` | — | — | end this script / every script of this instance | | `wave` | `ch` (or list), `shape`, one of `w`/`hz`/`every`, `phase`, `amp`, `offset`, `pow`, `decay`, `clip`, `sync` | no | value = offset + amp·shape(w·t + phase) | | `hop` | `amp`, `hz` or `every`, `sync` | no | the studio hop curve on dy, sx, sy | | `gait` | `every`, `stride`, `give`, `bend`, `thighScale`, `shinScale`, `armSwing`, `bob: [base, amp] \| null`, `roll`, `sync` | no | walk cycle on every leg of the body plan | | `drive` | `ch` (or list), `value` | no | channel follows a live expression | | `mood` | `mood` | no | | | `setVar` / `changeVar` | `var`, `value` / `by` | no | instance variables (score, lives…) | | `chain` | `ch`, `shape`, `amp`, one of `w`/`hz`/`every`, `phase`, `decay`, `lag`, `falloff`, `segments`, `sync` | no | a wave travelling down `ch`, `ch#1` … `ch#(n−1)` (§8.2) | | `follow` | `ch` (or list), `source`, `gain`, `hz`, `damping` | no | `ch` lags `source` like a damped spring (§8.3) | | `reach` / `release` | `part`, `to: {x, y}` or `at` (reach), `over`, `ease` | `over` s | a hand's or foot's reach weight glides to 1 / 0 (§8.4) | | `view` | `to` | no | the drawing to show: `front`, `side`, `back` (§8.5) | **wave.** `shape` ∈ `sin cos abssin possin dampsin square saw tri`. `w` is rad/s; `hz` gives w = 2π·hz; `every` gives w = 2π/every. `offset` defaults to the channel's rest (0, or 1 for sx/sy), so on a scale channel the wave value *is* the factor. `pow` raises the shaped value; `dampsin` multiplies by e^(−decay·t); `clip: "pos"|"neg"` clamps after shaping. `amp` and `offset` are **live** (evaluated every frame — e.g. `"amp": "=-20*loud"` makes the bob follow the voice); `w`, `phase`, `pow`, `decay` are fixed when the motion starts. **sync.** A motion's clock t is `local` (time since the motion started; default; no pop at start) or `start` (time since the behaviour started; keeps phase across phases of a behaviour). **gait.** ph = (t mod every)/every. Each leg gets `walkleg(ph + offset)` → (thigh, shin) where stance (p < ½) is thigh = stride − 2·stride·s, shin = give·sin(sπ), and swing is thigh = −stride + 2·stride·s, shin = bend·sin(sπ). Two legs: offsets 0, ½ on `part:leg:∓1`/`part:shin:∓1`, arms counter-swing `part:arm:S = −thigh_S·armSwing`. Four legs: offsets [0, ½, ½, 0] on `part:leg#i`. Six: tripod. `bob` adds dy = base − amp·|sin(2π·ph)|; `roll` adds rot = sin(2π·ph)·roll. **hop.** cyc = (t·hz) mod 1. cyc < 0.12: crouch (dy = 6k, sx = 1+0.08k, sy = 1−0.08k, k = cyc/0.12). Else c = (cyc−0.12)/0.88, h = sin(cπ): dy = −amp·h, sx = 1−0.10h, sy = 1+0.12h; landing (c > 0.86): sx = 1+0.11k, sy = 1−0.11k with k = (c−0.86)/0.14. `hopair(cyc)` in expressions gives h (0 on the ground). ## 4. Friendly blocks (desugared before running) `move {dx, dy, over}` → ramp by · `moveTo {x, y, over}` → ramp to dx/dy · `turn/turnTo {deg}` → rot · `scaleTo {s | sx, sy}` (each a number or `=formula`) · `squash {k}` → sx 1+k, sy 1−k · `setPart/turnPart {part, deg}` · `liftPart {part, px}` · `bob {amp}` → wave dy abssin −amp · `sway {deg}` → wave rot · `wobble {px}` → wave dx · `shake {px, deg}` → wave dx (and rot), 6 times a second unless it gives `w`, `hz` or `every` (the wiggles default to every 1 s) · `swingPart {part, deg}` · `window {from, to, body}` → `wait until from` + `for until to` (negative times count back from `dur`) · the skeleton's `swish flop bend bow letFollow turnView` and `reach {hold}` (§8). Part names accept channels (`part:arm:-1`), legacy names (`arm_l`, `thigh_r`) and words ("left arm", "arms", "tail", "front right leg"; v0.4.1: "front paws" / "front left paw" / "front feet" are `part:hand`(`:S`), the front paws being a four-legged body's hands, "back paws" / "hind paws" `part:foot`; a paw named without front or back is a front paw: "paws" is `part:hand`, "left paw" `part:hand:-1`, on two legs the hands). "front legs" / "back legs" without a side is the pair's left leg's channel (`part:leg#2`, `part:leg#0`); a friendly block moves both legs of the pair (`part:leg:-1#2` and `part:leg:1#3`, outward as for any pair; a lift `lift:leg#2` and `lift:leg#3`; v0.4.1, before which "swing front legs" moved the front-left leg alone). A friendly block names a part, so a raw channel there must be well-formed and of a role in the vocabulary (§2): `part:elbow` or `part:constructor` is an unknown part (an error), not the warning a primitive block gets. **Friendly part angles mean "lift outward"** on either side: a left-side part's raw angle is negated, and a side-less paired part ("arms", "ears", "legs") becomes both sides mirrored, so `setPart arms 80` raises both arms and `swingPart arms 30` flaps them together. Unpaired parts (head, tail, neck) pass through raw. Primitive blocks always use raw channel angles (the skeleton's `reach`/`release` name a hand or foot like a friendly block, and a reach point's x is outward, §8.4). ## 5. Expressions A value is a number or a string starting with `=`. Grammar, low → high precedence: `||` · `&&` · `== != < <= > >=` · `+ -` · `* / %` · unary `- + !` · `^` (right-assoc) · atom (number | name | name(args) | (expr)). `%` is floored modulo (Python semantics). Comparisons give 1/0. Names: `tau` (behaviour time), `local` (motion time), `t` (runtime time), `dur`, `loud` (0..1 voice level), `talking` (0/1), `random`, `pi`, `e`, then params, then vars, then the page's globals (`rt.vars`). Functions: sin cos tan abs exp sqrt floor ceil round(=floor(x+½)) sign pow min max mod clamp smoothstep hopair lerp step. Validation knows exactly these built-in names (`validate.js` `BUILTIN_NAMES`); any other name must be a param, a var or a page global. Validation cannot see page globals (the page sets `rt.vars` at run time), so it warns about any name that is not a built-in, a param or a var; when the expression runs, a name that none of them holds (page globals included) is an error. - **There is no `dt`.** Under exact time (§0) a value never depends on how the clock was stepped, and a step size is exactly that, so the runtime does not provide it (v0.3 validation listed it, and such a program failed when it ran). Use `tau`/`local` and timed blocks; a param or var may be called `dt`. - **Names are data.** Function names, ops, params, vars, page globals, part words, channel keys, place names, cast ids and hat names are looked up as own properties (`hasOwnProperty`, null-prototype objects for params and vars), so `constructor`, `toString`, `__proto__` are ordinary names where a name is free (a param, a var, a place, a cast id) and unknown where the vocabulary is fixed (no function, op or part has those names). This is also how the Python port behaves. ## 6. Interpreter contract (JS `engine/runtime.js`; Python `py/c4c_runtime/`) The Python port (`py/c4c_runtime/`, `docs/PYTHON_PORT.md`) is the same contract in snake_case, behaviours, scenes and cartoons (§7) alike (`create_runtime`, `add_character`, `start_behaviour`, `step_to`, `pose`, `start_cartoon`, `frame`, `validate`, …), held to the JS engine by `fixtures/parity_js.json` (poses, frames, timelines, warnings and errors within 1e-9). ``` rt = createRuntime({ clock: "timeline"|"live", seed, speech?: (text, {who, voice, mood, speed, pitch}) => seconds, // §7.4 packs?: (id, version) => program | pack, // §7.5, synchronous fps = 24, aspect = 16/9, maxDuration = 3600 }) // §7.2, §7.7 rt.addCharacter(id, { legs: 2 }) inst = rt.startBehaviour(id, program, { dur, params }) // runs start-hat scripts at rt.t rt.stopBehaviour(inst) rt.setInputs(id, { loud, talking }) rt.emit(event, { charId, key, name }) // talk-start/-end set talking; hats restart rt.stepTo(T) rt.step(dt) // time only moves forward rt.pose(id) → { dx, dy, sx, sy, rot, parts: {"arm:-1": deg, …}, lift: {…}, mouth, blink, mood, reach: {"hand:1": {x, y, w} | {spot, w} | {spot, dx, dy, w}, …}, view: "front" | "side" | "back" | null } // §8 rt.startCartoon(program | {cast, places, program}, { params }) → { duration, timeline } // §7; one at a time rt.frame() → { t, scene, place, background, grade, caption, characters: { id: { x, y, h, facing, visible, pose, talking } }, order: [id] } // §7.8 rt.timeline rt.duration rt.warnings // §7.7 validate(program) → { ok, errors[], warnings[] } // unknown ops/channels/parts = errors; unknown names, roles nothing draws = warnings ``` These are all the options: anything else passed to `createRuntime` is ignored (the plan's twin runtime, §7.7, is marked internally, not by an option). `startCartoon` either returns with the cartoon running or throws (an invalid program, a scene expression the plan cannot evaluate, a behaviour the cartoon starts failing at once); after a throw no cartoon is running: the runtime has the cartoon it had before (none, or the last one that ended), whatever the failed cartoon had started is stopped, and the next `startCartoon` is accepted. When the plan failed nothing else changed either: no cast was added, nothing was scheduled, and the speech answers and warnings the plan collected (it asks this runtime's speech provider, §7.4) are dropped with it. When the run failed once begun, the cast it added stays, and so do the plan's warnings and speech answers. `engine/index.js` `poseAt(program, {dur, params, legs, inputs}, tau)`: one behaviour's pose at `tau`, the same bits as a fresh runtime started at 0 and stepped once to `tau` (so a renderer can scrub). It keeps the runtimes of recent calls warm (per program object and settings, a few per program) and steps one on when the next call asks for the same or a later time, so playing forward costs the new stretch of time, not the whole history (a follow-through ticks 240 times a second from its start, §8.3). It reuses a runtime only while that gives the same bits as from scratch: no thread has yielded to a step (a poll, §0; `rt.polled`), no pose has drawn `random`, and the inputs are this call's or no block or tick has read them yet (`rt.inputsRead`; a pose reading them does not count). Otherwise, for an earlier time, or for settings that cannot be compared by value, it starts from scratch. A program object must not change once it has run (the engine prepares each one once). The Python port's `pose_at` keeps runtimes warm in the same way (the same answers; `docs/PYTHON_PORT.md`). `engine/parts.js`: `partChannel(name)`, `partAngle(pose, role, side, index)`, `legacyPose(pose)`. `engine/director.js`: `cartoonProgram(content)`, `scenesOf(program)`, `estimateSpeech(text, {speed})`. ## 7. Scenes and cartoons (built in v0.3: `engine/director.js`, `tests/scene.test.mjs`) A scene or a cartoon is a program like a behaviour, run by the same scheduler (§0 holds: exact time, deterministic, poll-only step dependence). Its scripts are threads of a **director** instance that is never drawn. The director owns the stage (place, grade, caption), each character's **placement**, the **lines** being spoken, and the behaviour instances a scene starts with `does`. ### 7.1 Programs and the one design: scene hats ``` scene := { "v":1, "kind":"scene", "name"?, "cast"?: {id: entry}, "places"?: {name: place}, "params"?, "vars"?, "scripts" } cartoon := { "v":1, "kind":"cartoon", "name"?, "cast": {id: entry}, "places"?: {name: place}, "params"?, "vars"?, "scripts" } entry := "role description" | { "name"?, "legs"?: 2, "at"?: {x, y, h, facing}, "voice"?, "pack"?, "look"?, "note"? } pack := "c4c:char:sunny" | { "id": "c4c:char:sunny", "version"?: "1.0.0" } // a character pack reference place := "background name" | { "background"?, "grade"? } ``` A cast entry is an **inline descriptor** (`name legs at voice look`, what the example cartoon uses) or, once character packs exist, a **pack reference** (`pack`, optionally with `name`/`at`/`voice` to override the pack's own); both forms are validated by `validate.js` (unknown fields are warnings, a `pack` must be a pack id or `{id, version}`). `docs/PACK_FORMAT.md` describes the same two forms. - **A cartoon is an ordered list of scenes, written as scene hats.** Every cartoon script starts with `{"on":"scene","name":…}`; scripts with the same name are one scene and run in parallel (like two actors' parts); scenes run in the order their names first appear. A hat may carry `place` (the scene's place, as if it began with `cut {place}`), `mood` (its grade) and `at` (§7.6). Two scripts of one scene must not disagree about these (validation error). - **A scene program** (kind `scene`, a scene template pack) is exactly one scene: its scripts have no hat or a scene hat; the scene's name is the first hat name, else the program's `name`. Its `cast` usually lists roles (`{"A": "any", "B": "any"}`) that the page fills with `addCharacter` first. - `cast` ids become characters on `startCartoon` (`addCharacter(id, {legs})`, existing characters are kept). `name`, `voice` (the default voice of the character's lines), `pack` (a character pack reference) and `look` (the character's design, for the renderer: `{template, style, shape, colors: {body, skin, acc}, stretch, options, facing}`, `docs/PACK_FORMAT.md`, what `ui/shapes.js` `makeCharacter` takes as it is) are data the runtime passes through; validation checks the look's form (not an object is an error, anything inside it a renderer cannot use is a warning). A cartoon **pack**'s content is `{cast?, places?, program}` (`docs/PACK_FORMAT.md`: `program` required, nothing else allowed); `startCartoon` accepts it directly and merges the outer cast and places into the program (`cartoonProgram`). - A scene's scripts may use the control blocks — `wait for while until repeat forever together if stop stopAll setVar changeVar window` — and the scene blocks below. Blocks that move a character (`set ramp wave hop gait drive` and the friendly moves) are errors in a scene: a scene says `does {who, behaviour}`. Scene blocks are errors in a behaviour. - `stop` ends the script (or the `together` branch) it is in. `stopAll` ends every script of the scene, wherever it runs (also inside a branch), so the scene ends then — or holds its stage until a pinned next scene (§7.6). What they cut off ends as in §7.2 and §7.4, and is reported (§7.6). - In scene expressions: `tau` is time since the scene started (so `wait {until: 2}` means 2 s into the scene), `t` is runtime time, `dur` is the scene's length when the next scene is pinned with `at` (so `window {from: -1}` works) and Infinity otherwise, `random` is the director's own seeded stream (separate from the behaviours', so the plan in §7.7 matches the run), params and vars as usual. `loud`/`talking` belong to characters; a scene reads 0 (warning). - **Held scripts.** A scene script that can only wake when its scene ends — a `window` without `to`, `wait {until: "=dur"}`, any wait or `for` until Infinity — is *held*: it does not keep the scene going, and it ends with the scene (its `for` scope too, so what it started stops then). So `window {from: 1, body}` means "from 1 s to the end of the scene" whether or not the end is pinned; alone in its scene, the scene ends when the window's body has run. Counting back from an end that is not pinned (`window {from: -1}`, `"=dur - 1"`) never arrives: validation warns, and the blocks never reached are reported in `rt.warnings` (§7.6). `window {from, to}` takes times in seconds (`to` too). - **Screenplay conversion.** The structure is a screenplay already: a scene hat is a scene heading, `say` is a line with a parenthetical, `does`/`enter`/`exit` are action lines, `cut` is a transition, parallel scripts are simultaneous action. `toText` prints it (§7.9). ### 7.2 Placement (owned by the scene) Each character has a placement `{x, y, h, facing, visible}`, separate from its behaviours; behaviours only **offset** it (pose `dx dy` etc. are added on top by the renderer). - **Units:** `x` is a fraction of the stage width (0 = left edge, 1 = right edge) at the character's root; `y` is the feet line as a fraction of the stage height (0 = top, 1 = bottom; the ground is **0.92**); `h` is the character's height as a fraction of the stage height (default **0.5**); `facing` is the way the character faces on screen: +1 right, −1 left (a glide through 0 turns it round). Pose offsets stay in 1080p pixels, scaled by min(W,H)/1080. These are the studio page's own units (its scene table's `ax`, `ah`, baseline 0.92·H). - **Facing, defined as a product.** A character's art has its own facing `A`: the way the art faces as designed. The shape templates are drawn facing right (dog, cat) or facing the front (person, kid, bird, monster), `A` = +1; the studio's "face the other way" switch makes `A` = −1, and so does a bitmap drawn facing left. The renderer mirrors the designed art by **`facing × A`** (its `sx`, and the pose's `dx` and `rot` with it: `ui/shapes.js` `drawCharacter` mirrors "the whole performance"), so on screen the character faces `facing` whichever way its art was designed: a design turned to face left still enters from the left facing right, never walking backwards. In `ui/shapes.js` terms, whose `drawCharacter({facing})` mirrors the template itself, a cartoon renderer passes the placement's `facing` for a shape character (the design's own switch is overridden by the scene), and a page without a cartoon passes the design's own facing, as the studio does today. `A` is the renderer's to know (from the character design or a cast entry's `look`); the engine only carries the placement's `facing`. - **Validation of placement values** (numbers in `place`, `enter`, `exit` and a cast entry's `at`; a `"=formula"` is known only when it runs): `facing` must be 1 or −1 (error), `h` must be more than 0 (error), a `y` outside 0..1 (the feet out of frame) is a warning. - **Defaults:** characters are spread in cast order, x = (i+1)/(n+1), y = 0.92, h = 0.5, facing +1. **Nobody is visible until placed**: by the cast entry's `at` (visible from the start), `place`, or `enter`. After `exit` finishes, the character is not visible. - **Persistence:** placement persists across behaviours, `does` replacements and scenes, until a scene block changes it. - **The spot on stage survives an exit.** `exit` remembers where the character stood when it began (on the axis it leaves by: x for left/right, y for top/bottom). `enter` without `x`/`y` comes back to that spot, per axis, else to its current placement; so exit right → enter from the left lands where it stood, and exit through the bottom → enter from a side lands with its feet on the ground. `place` with `x`/`y` and `enter {x, y}` set a new spot. - **Glides** (`place … over`, `enter`, `exit`) use the same segments and eases as `ramp`: the script waits exactly `over` seconds; a glide cut off by a scope end (`for`), a `stop`, or its scene ending **freezes** where it is (and an interrupted `exit` leaves the character visible); cut off at the very instant it lands (a `for` or a pinned scene ending then), it has landed (an `exit` hides). Two glides of one value: the last writer wins (an `exit` whose value was taken over by a later `place`/`enter` does not hide the character when it ends). - **Off stage** for `enter`/`exit` with `aspect = W/H` (createRuntime option, default 16/9): left x = −(0.02 + h/aspect), right x = 1 + 0.02 + h/aspect, top y = −0.02, bottom y = 1 + h + 0.02 — far enough for a character as wide as it is tall. ### 7.3 Scene blocks | op | args | waits? | meaning | |---|---|---|---| | `say` | `who, text, voice?, mood?, speed?, pitch?` | the line's length | §7.4 | | `does` | `who, behaviour, for?, params?, also?` | no | §7.5 | | `caption` | `text, for?` | no | shows `text` for `for` s (or until its scope/scene ends) | | `cut` | `place?, mood?` (at least one) | no | changes the place and/or the grade (§7.6) | | `place` | `who, x?, y?, h?, facing?, over=0, ease=smooth` | `over` | sets or glides the placement; makes the character visible | | `enter` | `who, from=left\|right\|top\|bottom, over=1, ease=linear, x?, y?, h?, facing?` | `over` | jumps off stage on that side, glides to the x/y given, else its spot from before its last exit, else its placement (§7.2; a given h is set at once); facing defaults to the way it moves (from left: +1, from right: −1) | | `exit` | `who, to=right\|left\|top\|bottom, over=1, ease=linear, facing?` | `over` | glides off stage on that side (facing the way it moves), then is not visible | | `mood` | `who, mood` | no | the character's face mood (in a behaviour, `mood {mood}` is the instance's own) | ### 7.4 `say` - Its length comes from `createRuntime({ speech: (text, opts) => seconds })`, `opts = {who, voice, mood, speed, pitch}` (only the given ones; `voice` defaults to the cast entry's `voice`). Without a provider, or when it throws or returns something that is not a finite number ≥ 0 (a Promise included; warning): **0.06 s per character + 0.25 s, at least 0.6 s**, divided by `speed`. The provider must be synchronous and pure (the same answer for the same line; measure the audio before `startCartoon`); the runtime asks once per distinct line and reuses the answer. - The block waits exactly that long. From its start to its end the speaker's `talking` input is 1; `talk-start` hats fire at the start and `talk-end` hats at the end, at the exact times (overlapping lines by one speaker: one talk-start at the first start, one talk-end at the last end). The mouth is the renderer's (from the audio's loudness via `setInputs(id, {loud})`, or a flap while `talking`) or a behaviour's (`drive mouth`); the director does not set `talking` by any other means, so pages should not set it themselves while a cartoon runs. - The line is the caption while it lasts. `mood` applies to the speaker from the line's start and **holds** until another mood is written (a later line's mood, `mood {who}`, or a behaviour's `mood`; the latest write wins; at equal times the scene's beats the behaviour's). - Cut off (scope end, `stop`, the scene ending), the line ends then: talk-end fires, the caption ends, and the record's `dur` becomes what was spoken. - Record: `{t, dur, kind: "say", who, text, voice, mood}` (+ `speed`, `pitch` when given) — exactly what an audio stage needs to synthesise and place the line. ### 7.5 `does` - Starts a behaviour instance on `who` at the current time: `behaviour` is a pack id (`"c4c:beh:bounce"`), `{id, version}`, or an inline program (kind `behaviour`). Ids resolve through `createRuntime({ packs })`, which may return the program or the whole pack (its `content` is used); the engine does no I/O (a page passes its catalog: `const cat = await loadPacks(); createRuntime({ packs: cat.get })`). A missing or invalid pack never stops the cartoon: it falls back to `c4c:beh:idle` (or nothing if that is missing too) and is reported in `rt.warnings`. - The instance's `dur` is `for` when given, else the time until the earliest known end of its scope: an enclosing `for` block, else the scene's end (from the plan, §7.7), else Infinity. - `params` are **scene** expressions, like every other value in a scene block: they are evaluated when the `does` runs, with the scene's names (its params, vars, `tau`, `random`, …), in the plan as in the run, and the behaviour receives the numbers (its own defaults fill the rest, evaluated in the behaviour). A name the scene does not know is a validation warning and an error at `startCartoon` (the plan evaluates it), never part-way through a render. A param the behaviour does not declare is a warning (validation for an inline behaviour, `rt.warnings` for a pack) and is passed anyway. A behaviour that cannot start (e.g. a default naming something unknown) is skipped with a warning. - It ends at `for` (exact time), when its scope ends (a `for`/`while` block in the scene, or the scene), or when **replaced**: a `does` on the same character stops every behaviour the scene started on that character, unless it has `also: true`, which adds it alongside. Behaviours started by the page with `startBehaviour` are never replaced by a scene. A stopped behaviour's offsets disappear at once (a new shot). - It does not wait: `does A bounce` then `say A "…"` bounces while talking. Use `wait` to hold. ### 7.6 Scenes, cuts, captions, the stage - A scene **starts** when the previous one ends, or at its pinned `at` (seconds from the cartoon's start, normally a song time). It **ends** when all its scripts have ended (held ones aside, §7.1) — unless the next scene is pinned, in which case it holds its stage until that time, and anything still running is cut off then (glides freeze, lines end). **The pin wins**: a pin that has already passed when the scene before it starts (an earlier line ran longer than planned, e.g. TTS) cuts that scene off at once — it runs 0 s, its lines are recorded with `dur` 0 — and the pinned scene starts then, late. Validation warns when a pinned scene is pinned before an earlier pinned one. - **Nothing a scene loses goes unreported.** When a scene ends, `rt.warnings` gets one message for it (with the scene, the time and the pin) if it lost anything: squeezed to 0 s by a pin that had passed (and how late the pinned scene starts); a line cut off part-way by a pin (how much was spoken); a caption whose `for` ran past the scene's end (how much was shown — a caption does not wait); a `does … for` whose time ran past the scene's end (how much of it ran — a `does` does not wait either); a glide (`place … over`, `enter`, `exit`) cut off part-way by a pin (how much of it ran; a cut-off `exit` leaves the character on stage, and every cut-off glide leaves it where it froze for the next scene); blocks a script had not reached (cut off by a pin, or after a wait for an end that never comes). A `forever`/`while` loop cut off by a pin and a hold at the end (a `wait` with nothing after it) are expected and not reported — but a line or a glide cut off part-way inside a loop is. Something that ends exactly when the scene ends (a glide landing, a `for` running out) lost nothing, and "exactly" allows for rounding: what was due to end at most a microsecond after the scene (0.1 + 0.2 is 0.30000000000000004, not 0.3) is not a loss. The one exception is an `exit`, which either has landed or has not: one that rounding put a hair after the scene's end has not landed, so its character stays on stage, just out of frame, and the message says so ("lands 5.6e-17 s after the scene ends (rounding)"). Amounts are printed to 3 decimals, more when 3 would print two different amounts alike ("0.3 of 0.3001 s done"). A `does` without `for` (or with `for` Infinity) and a caption without `for` end with their scene by definition. A scene ended by `stopAll` says so ("ended at 1 s by "stop all"") instead of advising a `wait`, and lists what the `stopAll` cut off in the other scene scripts as well: a line or a glide cut off part-way (an `exit` frozen on stage), blocks they had not reached. The script that runs the `stopAll` means to stop there, so the blocks after it (in it, or in the other branches of its own script) are not losses, but a line or a glide cut off in its other branches is. Ending a script also ends its inner scopes (a `for`, a `while`) and what was started in them, so a caption or a `does … for` started there whose `for` had not run out is cut off by the `stopAll` itself, in any script (the stopping one too, as the same caption at scene scope is cut off by the scene's end), and reported with the amount as of the `stopAll`; what the scene itself started stays until the scene ends and is reported then. When the next scene is pinned, the stage holds after the `stopAll` until the pin: the `stopAll`'s cut-offs get a message of their own ("was stopped at 1 s by "stop all" and held its stage until the pinned start of "b" at 3 s: …"), and the scene's end reports what the pin cut off, as usual. The plan (§7.7) finds all of these, so they are in `rt.warnings` as soon as `startCartoon` returns, and a run that follows the plan repeats exactly the same messages, which are not added a second time. A run follows the plan unless the scene scripts poll or loop without waiting and the renderer steps off the plan's frames (§7.7); such a run can cut things off at other times than the plan did and then adds its own messages beside the plan's. - When a scene starts: its place/mood apply, behaviours already running hear `{"on":"scene","name"}` hats (a behaviour started by the scene itself starts with its start hats), and its scripts start. - When a scene ends, everything it started ends with it: its `does` behaviours, its captions, its lines. Placement, place and grade persist into the next scene. - `cut {place, mood}`: the place changes; the grade becomes `mood`, else the place entry's `grade`, else "normal". `cut {mood}` changes only the grade. The scene's hat `place`/`mood` work the same way. - Captions: the frame's caption is the **latest-started** caption still showing (a `caption` block or a line). When it ends, an earlier one still showing returns. A newer caption block of the same scope that ends no earlier hides an older one for good (only their scope's end cuts caption blocks short, and it cuts both at once), so the director forgets the older one (§7.7). ### 7.7 The plan: `rt.timeline`, `rt.duration` `startCartoon` first runs the director alone on a twin runtime (no behaviours: the director cannot read characters, so its timing does not depend on them), stepping on the cartoon's own frames `t0 + k/fps` (t0 = the time `startCartoon` was called, k = 1, 2, …) up to `maxDuration`. So before the first frame: - `rt.timeline` is the complete record list in time order: `scene {name, place, grade}`, `say`, `does {who, behaviour, also?}`, `caption {text}`, `cut {place, grade}` (dur = until the next cut or the scene end), `place {who, x?, y?, h?, facing?}`, `enter {who, from}`, `exit {who, to}`, `mood {who, mood}` — each with `t` (runtime seconds) and `dur` (seconds actually run). - `rt.duration` is the cartoon's length (null if it does not end within `maxDuration`). - `does` without `for` learns the rest of its scene from the plan. The plan equals the run exactly when the scene scripts do not poll (`while`, `until`) or loop without waiting; when they do, it matches a renderer stepping on the same frames, `stepTo(t0 + k/fps)` (§0), whatever t0 is. Tested: plan = run, also from an off-grid t0. Page globals (`rt.vars`) are copied into the plan as they are at `startCartoon`; a page that changes them later gets a run that may differ from the plan. **Cost.** Planning and playing take time proportional to the number of blocks the scene scripts run (plus one step per frame for the plan's grid). The director keeps only what is still running: `does` behaviours and captions whose `for` has not run out, indexed by the scope they belong to and by character. A caption without `for` (or with a `for` of Infinity) is not one of them: it lasts until its scope ends and nothing can cut it shorter, so all it needs is its timeline record's `dur` set then; its scope keeps a list of such open records (one reference per record, i.e. part of the timeline's own growth), and a caption whose end is not a number (`for: "=0/0"`) keeps nothing at all. Replacing a `does`, a `for` running out, a scope ending all remove what they end at once. For display it keeps only the captions that may still show: the lines being spoken, and per scope a stack of caption blocks that no newer one hides for good (§7.6), newest on top with ends decreasing towards it — a new caption pops every one it hides and every one that has ended, and a frame pops the ones that ended by its time, so a frame looks at one caption per scope and line. A caption that ends no earlier than the ones before it in its scope clears them, so a loop of captions holds one or two, whatever its mix of `for`; the stack grows only while each caption ends earlier than every one before it, and then each one on it will show again when the newer ones end (unless the scope ends first). So a scene that loops for an hour does not slow down as it goes, in the plan or at 24 fps (tested: a 10-minute loop of `does`, `does … for`, `say`, `caption … for` and `together` — 150,000 timeline records, 60,000 lines — plans in about 0.2 s on a laptop, where it took 10 s before v0.3.1 and grew quadratically, and the director's working state stays under a hundred entries from 30 s to 10 minutes; a 10-minute loop of captions without `for` between captions with `for`, in the scene's scope and in a `for`'s, with lines, holds at most 8 captions for display and plays all 14,400 frames in about 0.25 s, each with the expected caption; before, the display set grew by one caption per loop, and an hour of such a loop took 94 s to play in an earlier version, 0.24 s now). What does grow, by design, is the timeline (one record per block run) and what the program itself keeps running: `does … also` without `for` adds a behaviour that lives until its scope ends, and a caption without `for` holds its timeline record open until then (its `dur` is known only when the scope ends), even once a newer caption hides it: its scope's list of open records grows with the timeline, never the live items (tested: a 5-minute loop of 100 captions a second without `for`, with `for` Infinity and with `for` NaN, in the scene's scope and in a `for`'s, keeps at most 4 live items, scopes and displayed captions, where an earlier version held one live item per caption, 540,001 after 9 minutes; the 10-minute caption loop above now holds 23 live items at its end, 10,959 before). The runtime's event queue holds one entry per waiting thread and per pending timer (a scene pin, a `does … for`'s stop): a thread that is rescheduled leaves its old entry behind, and a `while`/`until` polling inside a `for` reschedules every step, so the queue is compacted whenever it doubles, and the stop timer of a `does … for` that was replaced early is cancelled. So a script that polls inside a 10-minute `for`, in a cartoon or in a behaviour on the live clock, keeps a queue of a few dozen entries (tested; it grew by one entry per step before). Compaction never changes the order events run in. A runtime runs **one cartoon at a time**. Once it has ended (`frame().scene === null` after `rt.duration`), another may start on the same runtime, at the current time, on a fresh stage (nobody placed, no moods; characters and the page's behaviours are kept). Starting one while another is still running is an error. A `startCartoon` that throws never leaves a cartoon running (§6). ### 7.8 `rt.frame()` ``` { t, scene: name | null, place: name | null, background, grade: "normal" | …, caption: text | null, characters: { id: { x, y, h, facing, visible, pose: rt.pose(id), talking: 0 | 1 } }, order: [id, …] } ``` `background` is the place entry's `background` (or the place name). Without a cartoon, every character is visible at its default placement. - **`order`** lists every character id once, in the order the characters were added: first the ones the page added with `addCharacter`, in the order it added them; then the cast members that were not there yet, in the cast's key order (a cast is a JSON object, so this is JavaScript's `Object.keys` order: ids that are array indices, such as `"7"`, come first in ascending order, then the others as written; the Python port reads the cast the same way). It is the order to draw them in, first drawn first (behind), the same in every frame, and the same in both engines. Each frame gets its own array. - **`characters`** has exactly one own key per character id, each defined rather than assigned, so an id such as `__proto__` or `constructor` is a key like any other and never the object's prototype; look a character up by its id with an own-property test, never with `in` or a bare lookup of an id that may not exist. Its key order is **not meaningful**: JavaScript lists array-index ids first whatever order they were added in, the Python port lists them in the order added. Iterate `order`. - The default placement (§7.2: spread in cast order, then the other characters) is not this order: a cartoon spreads its cast first, whatever the page added before. ### 7.9 Text view (`engine/text.js`) `toText` prints scenes as a screenplay-like outline: `cast: A = Sunny, B = Grump` · `scene "Morning" at park` · `A says "Good morning, Grump!" (happy)` · `B does annoyed for 2 s` · `B also does wag` · `caption "One bite later..." for 1.6 s` · `cut to park (bloom)` · `A enters from the left in 2 s` · `A exits to the left in 2.4 s` · `place A at x 0.3, facing left in 1 s` · `B looks happy`. An inline behaviour is printed indented under its `does`. One-way for now; parsing it back (the screenplay view, `A: Hello! (happy)`) is Phase 3. ### 7.10 Not built yet `sing {who, text, at}` (lines timed to a song), camera blocks (zoom, pan), `clip` on `say` (a recorded audio clip instead of TTS), interactive scene hats (key/click in a cartoon). ## 8. The skeleton (v0.4: `engine/runtime.js`, `engine/desugar.js`, `tests/skeleton_engine.test.mjs`) Every character is a tree of bones (plan §1d): hips → torso → head, upper arm → forearm → hand, thigh → shin → foot, tails, ears, hair. v0.4 adds what makes the tree act like a skeleton: **chains** (a tail whips, a back bends), **follow-through** (ears flop after a hop), **reach** (a hand on the hip), **views** (front, side, back drawings) and **joint limits** (in profile an elbow or a knee bends one way only; seen head on it bends either way on screen, `docs/SHAPES.md` §3.4). The engine computes channels and two new pose keys; the renderer (`ui/shapes.js`, `docs/SHAPES.md`) draws them, and the Python port (`py/c4c_runtime`) runs the engine's half with the same bits (§8.9). Exact time, determinism and step independence (§0) hold for all of it. ### 8.1 The shared interface (the engine and the renderer both build to exactly this) - **Chains.** A chain is a run of nodes with the same role whose segments are numbered from the root of the chain: `part:#` (i = 0 nearest the body). Tails (role `tail`, 4 segments), spines (role `torso`: `torso#0` = the belly / waist bend, `torso#1` = the chest), ears and hair where they look good. Every segment is a child of the previous one, so the general channel `part:` (no index) rotates segment 0 and the rest follow: exactly today's look for every existing move (backward compatible). Index channels add per-segment bends. Quadruped legs keep `#i` = leg number: the meaning of `#i` depends on the role (§2). **The engine writes segment 0 on the general channel** (`part:tail`, `part:ear:1`) and segment i ≥ 1 on `#i` (the `chain` motion and the friendly blocks never write `#0`; a raw channel block still can). So a part a template draws in one piece moves with the chain's first segment, and on a chain the root (which reads the general channels and its `#0`) turns by exactly its share. How the renderer reads them (`ui/shapes.js` `applyPose`, `docs/SHAPES.md` §3.2): a node that is no chain follower reads the general channels and its index ones (`engine/parts.js` `partAngle`); a **chain follower** (segment i ≥ 1 of a chain the template draws) reads **only** its index channels `part:#i` and `part::#i` (plus `curl` × the general channel where a template says so), never the general channel again (`swingPart tail 25` turns a four-segment tail at its root, as a one-piece tail turned, not 100° at its tip); and the last segment a template draws (a one-piece part: its only one) also reads the index channels of every segment beyond it (v0.4.1), so a part drawn in fewer pieces shows the whole bend. - **Reach.** `pose.reach = { ":": { x, y, w } }` for hands and feet (role `hand` or `foot`, side −1 / 1). x, y are character-local 1080p pixels with the origin at the feet centre, x toward the character's own front / right as drawn, y **up**; w is the 0..1 blend weight (ramped by the block; the pose holds it to 0..1). Character-local means in the character's own frame: the renderer draws the back view as the front mirrored and mirrors a point with it (`docs/SHAPES.md` §3.5), so a point stays where it is relative to the character. A named spot is `{ spot, w }` instead (`spot` ∈ `hip head chest mouth`; the block's own field is `at`): the renderer resolves the spot on the limb's own side (`ui/shapes.js` reads `r.spot`, else finite `r.x` and `r.y`). v0.4.1: a spot moved by an offset is `{ spot, dx, dy, w }`, dx and dy **fractions of the character's height** (dx toward the character's front in profile and outward seen head on, so the left limb's dx goes left as its spot is on the left; dy up); the renderer resolves the spot, then adds dx × and dy × the character's drawn height. An offset of 0, 0 is the spot itself, `{ spot, w }`. (The renderer places a spot for the character's own body shape, and follows an offset beyond the limb's reach only as far as the limb reaches: `docs/SHAPES.md` §3.5.) The renderer solves 2-bone IK (upper limb + lower limb) toward the target, blending with the FK pose by w; a template may declare a spot one of its limbs cannot reach (a dog's front paws and its hips), and then the reach leaves that limb as the rest of the pose has it (`ui/shapes.js` `canReach`). The renderer also draws a reaching limb at the depth its target implies (in front of the body seen from the front; behind it from behind for the chest and the mouth). - **View.** `pose.view` is `"front" | "side" | "back"`, or `null` = the template's default (front for people, side for animals drawn in profile). The latest writer wins, like mood. The renderer draws the template's tree for that view; the switch is instant in the renderer, and the friendly `turnView` makes it look like a turn by squashing sx to 0 and back around the switch. - **Joint limits** are a renderer rule: each template node may declare `limits: {min, max}` in degrees (counter-clockwise positive, relative to rest); the renderer clamps the summed angle (every channel the node reads, §2); the pose data is unchanged (`rt.pose` reports the unclamped sum). - **Follow-through** is engine-side and deterministic: it produces ordinary part channels (§8.3). ### 8.2 Chains `chain {ch, shape = sin, amp = 1, w | hz | every (default every 1 s), phase = 0, decay = 0, lag = 0.08, falloff = 1, segments = 4, sync = local}` is a motion (it does not wait; it lives until its scope ends; start-or-keep; §2). `ch` is one part channel without an index (`part:tail`, `part:ear:1`); segment 0 is `ch` itself, segment i ≥ 1 is the channel `ch#i` (§8.1): t_0 = tl, t_i = tl − i·lag (tl: the motion's clock, as for wave: local or since the behaviour started) segment i = 0 when t_i < 0 (the wave has not reached it yet) segment i = amp · f_i · shape(w·t_i + phase) otherwise, f_0 = 1, f_i = f_(i−1) · falloff with the wave shapes of §3 (`dampsin` fades by e^(−decay·t_i), the segment's own clock). `amp` is live; `w`, `phase`, `decay`, `lag`, `falloff` are fixed when the motion starts. `f_i` is a running product (not `pow`), so both engines get the same bits. Every segment channel is written from the start (0 until the wave arrives). Friendly chain blocks (desugared; part words and the "lift outward" rule of §4; a side-less pair moves both sides mirrored). A **chain role** is spread over its segments — `tail` 4, `torso` ("body") 2, `ear` 2, `hair` 2 (`engine/parts.js` `CHAIN_SEGMENTS`; `segments` overrides, a whole number 1–16). Any other part is not a chain: it moves whole (the plain channel, no index), and a `segments` there is ignored (a warning). (v0.4 also listed the neck, with 2 segments; v0.4.1 does not, since no template draws a neck: a block that names it is a warning, §8.6.) | block | defaults | becomes | |---|---|---| | `swish {part, deg, every / hz / w, phase, lag, falloff, segments, sync}` ("swish tail 40° every 0.8 s") | tail, 40°, every 0.8 s, lag 0.08 s, falloff 1 | `chain {ch, shape: sin, amp: ±deg/n, …speed, lag, falloff, segments: n}` per side — deg is the whole part's swing, shared by its n segments, so the tip swings about deg (the segments add up); a part that is no chain: `wave {ch, amp: ±deg}` (= `swingPart`) | | `flop {…the same}` ("flop ears 25° every 1.2 s") | ears, 25°, every 1.2 s, lag 0.15 s, falloff 1.3 (the tips move more) | the same | | `bend {part, deg, over, ease, segments}` ("bend body 20° in 0.5 s") | body, 20° | `ramp {to: {ch: ±deg/n, ch#1: ±deg/n, … ch#(n−1)}}` — the angle shared by the segments (the tip turns deg in all); no chain: `ramp {to: {ch: ±deg}}` | | `bow {deg, over, ease}` ("bow 30° in 0.5 s") | 30° | `bend body −deg`: forward, toward the front as drawn (clockwise); `bow 0` stands up straight | A formula is divided as text: `deg: "=x"` over 3 segments is `"=(x) / 3"`; numbers are divided as numbers. `segments: 1` writes the part's own channel only. A template that draws the part in one piece (a cat's ears, a person's hair, a bird's tail, a dog's body) shows the whole bend (v0.4.1: its piece reads every segment's channel; in v0.4 it showed only segment 0's share, deg / n); one with more segments than the block writes leaves the rest straight (the monster's third antenna segment). A body standing on legs that hang from it turns only as far as its planted legs follow (the renderer eases it into that limit, `docs/SHAPES.md` §3.7): through the `bow` pack (40°) a dog's body tips about 22°, a cat's 24°, a monster's 30°; the person and the kid, standing on their hips, bow 40°. ### 8.3 Follow-through `follow {ch (or a list), source, gain = 1, hz = 2, damping = 0.3}` is a motion: its channels lag the **source** channel like a damped spring, `y'' = w²·(u − y) − 2·damping·w·y'` with `w = 2π·hz`, following the source's value `u`; each channel gets `rest + gain·(y − u)` (rest: 0, or 1 for `sx sy`). `gain` is live; `hz` (held to 0..60) and `damping` (below 0 is 0) are fixed at the start, a value that is not a number counting as 0. A spring of 0 Hz (a formula giving 0, a negative number or NaN) has no pull: the motion adds nothing (its channels stay at rest) and never ticks. - **The source** is the value the character's pose has on that exact channel key (`dy`, `rot`, `part:head`, `part:torso#1` …), from every instance's base channels and motions, combined as in a pose (§2) — **leaving out every follow motion's output** (a follower never follows a follower, so the order followers run in never matters). `mood` is not a number (an error). - **The grid.** A follow motion that starts at t0 ticks at `t0 + k/240` for k = 0, 1, 2, … (`FOLLOW_RATE` = 240; `h = 1/240`). The scheduler runs the ticks in time order with the block events, **after the events at the same time** (a block at a tick's time is seen by that tick), so the state at T depends only on what happened up to T: stepping at 24 fps, 30 fps, odd steps or one jump gives the same bits (tested). At tick k: read `u` = the source at `t0 + k/240`, then k = 0, or y or v not finite: y = u, v = 0 otherwise: v = (v + h·(w2·(u − y))) / den; y = y + h·v (w2 = w·w, den = 1 + h·(2·damping·w)) — semi-implicit Euler with the damping implicit (stable for every hz ≤ 60 and every damping ≥ 0), only `+ − × /` on doubles in exactly this order, so the Python port gets the same bits from the same source values. A NaN or an Infinity in the source makes that tick's output NaN, as in any motion, and the next tick starts settled again (the spring recovers as soon as the source does). The output at T is `rest + gain·(y − u)` of the last tick at or before T (y and u of that tick): a step of 1/240 s. Because the tick integrates with the source read at its end, the grid answers the continuous spring one tick early (tested within 1 px of a 40 px step against the exact solution, under-, critically and over-damped). That holds for springs up to 6 per second: a faster one swings further than the continuous spring (at 30 per second and no damping its first swing is 43.5 px for a 40 px step, at 60 per second 64.6 px), so validation warns about a spring `hz` above 6 (v0.4.1, §8.6). - A follower stops when its scope ends (the motion is removed) or its instance stops; its next start begins settled again (y = u). It costs one source read per tick (the motions that may write that channel). `rt.stepTo(Infinity)` returns once nothing is left to run; a follower that is still running ticks for ever then, as a `forever` with a `wait` in it runs for ever. - Inputs: a source that reads `loud`/`talking` at tick times sees the inputs as set before the step, so such a follower is as step-dependent as a poll (§0); likewise a source motion whose live value draws `random` draws at every tick. Programs without live inputs or `random` in their sources are exact. Friendly: `letFollow {part, after = body, amount = 1, hz, damping, segments}` — "let the [ears] follow [the body]". `after` is `body` or a part word. It becomes one `follow` per source, `gain = amount × f` with the factor `f` worked out first as a double, then multiplied (a formula `amount` becomes the text `"=(amount) * "`, `f` written as JavaScript's `String(f)`): - `body`: the bounce `dy` per side with `f = (sign × 0.4) / n` (degrees per pixel; sign the friendly side, so a hop flops both ears outward together), then the tilt `rot` and the spine's segments (`part:torso`, `part:torso#1`) each with `f = 1 / n` on every target channel, both sides alike (the part keeps its angle for a moment while the body turns, then catches up). - a part word: its own channels, the plain one and, for a chain role, its segments `#1 … #(m−1)` (m: the role's own count), each with `f = 1 / n` on every target channel. A pair named without a side (`ears`, `arms`) is both sides' channels: a target on one side follows the same side (`f = 1 / n`); a target with no side (the hair, the tail) follows the pair's friendly angle, `f = (side × 0.5) / n` (half the right side, minus half the left: both sides swinging outward together are one swing). The targets are the part's channels (sided): the plain channel and, for a chain role, `#1 … #(n−1)` (the gain shared over n segments, so the tip lags by the whole amount and the chain curves). ### 8.4 Reach `reach {part, to: {x, y} | at: spot (+ dx, dy), over = 0.4, ease = smooth, hold?}` and `release {part, over = 0.4, ease}`: - `part` is a hand or a foot: `right hand`, `left foot`, `hands` / `feet` (both), `paws` (the front paws: hands) …; anything else is an error. Without `part`: the right hand. A four-legged body's front paws are its hands (v0.4.1): `front paws` and `front left paw` are `hand:-1` / `hand:1` (the renderer moves the front legs for them), `back paws` the feet; a paw named without front or back is a front paw (`paws`, `left paw`: hands; a later fix in v0.4.1, before which `reach paws for the mouth` moved a dog's back paws, which reach no spot). (In v0.4 "front" was dropped: `front paws` named the back paws.) - The limb's **weight** glides to 1 (reach) or 0 (release) over `over` with `ease`, exactly like a `ramp` of a base channel: the script waits `over`; cut off by a scope end or a stop it freezes; one value per limb per instance, the last writer wins. It starts from the limb **as the pose has it** when the block runs (whichever move reached or released it last), so a move can take over a hand another move put somewhere: a `release` keeps that target (the spot, or the point frozen where it is) and glides the weight down from there. The pose holds the weight to 0..1 (a `snappy` glide overshoots; the weight does not). - `at` names a spot (`hip head chest mouth`): the pose carries `{spot, w}`. `to: {x, y}` is a point (numbers or formulas, evaluated when the block runs) in the interface's character-local coordinates (§8.1): x toward the character's own front / right as drawn, **the same for either side** (a point is a place, not an angle: `reach hands to x 80, y 300` puts both hands at that one point, and a left hand reaching forward in a side view has a positive x like the right), y up from the feet; the pose carries `{x, y, w}` exactly as given. A limb already reaching for a point (w > 0, in this move or another) glides its point to the new one over the same `over` and `ease`; from a spot to a point or to another spot (or from nothing) the target changes at once. - (v0.4.1) `at` with `dx`, `dy` (numbers or formulas, evaluated when the block runs; either may be left out, 0): the spot moved by fractions of the character's height, dx toward the character's front in profile and outward seen head on, dy up. A spot is anatomy; the offset makes a place near it that fits every character, whatever its size ("hand up beside the head": `reach right hand at head dx 0.1 dy -0.03`). The pose carries `{spot, dx, dy, w}`; with no offset (or 0, 0) exactly `{spot, w}`. A limb already reaching the **same** spot (w > 0) glides its offset from the one it has (none is 0, 0) to the new one over the same `over` and `ease`, as a point glides; another spot or a point is set at once. A release keeps the spot and its offset. `dx`, `dy` with a point (`to`) or without `at` are an error. - `hold: s` (friendly): `reach …`, `wait s`, `release {part, over, ease}` — reach, hold, let go the same way. - In the pose, per limb, the instance whose `reach` / `release` ran last wins (ties: the later instance), with its own weight and target; a limb never reached by it is left out. A stopped instance's reach is gone at once (like its offsets). `pose.reach` is `{}` when nothing reaches. ### 8.5 Views `view {to}` sets the view (`front side back`; `to` is required) at once; it is per instance, the latest write wins across instances (ties: the later instance), and `pose.view` is `null` until a move sets one. A stopped instance's view is gone. Friendly `turnView {to = side, over = 0.24}` ("turn to the side in 0.24 s") becomes `together { for over: drive sx "=abs(cos(pi*local/over))" } { wait over/2; view {to} }`: the width squashes to 0 and back (multiplying whatever sx the move has), the view switches at exactly the narrowest point, and the script waits `over`. A turn of 0 s (or less) switches at once (a warning). ### 8.6 Validation Errors: a `chain` `ch` that is not one part channel without an index; `segments` that is not a whole number 1–16 (a formula included); a `follow` without `ch` or `source`, or with an empty list of channels, a `source` that is not one channel, `mood` as `ch` or `source`; a `view` without `to`; a spring `hz` ≤ 0 or > 60, `damping` < 0 (numbers as written); a `reach` / `release` part that is not a hand or foot; `reach` without `to` or `at`, or with both; a point that is not `{x, y}` (both numbers or formulas, nothing else); `dx` / `dy` without `at` (v0.4.1); `dx` / `dy` that are not numbers or formulas; an unknown spot or view; a friendly chain block's part that is a raise (`lift:`) or a chain role with an index (`part:tail#2`); an unknown `after`. Warnings (odd values): a chain down a leg (its #i is the leg number), down a part nothing draws as a chain, or a role nothing draws; `segments` on a part that is not a chain; a part no character draws (v0.4.1: the neck, `engine/parts.js` `UNDRAWN_ROLES`: `swish neck`, `bend neck`, `letFollow neck`, `letFollow ears after neck`, a raw `part:neck` in any block: "no character draws a neck, so … moves nothing"); a `lag` below 0 or above 1 s; a `falloff` ≤ 0 or above 2; a spring `hz` above 6 (v0.4.1, §8.3); a `turnView` of 0 s or less; a negative `hold`. A raw channel in a primitive skeleton block (`chain {ch: "part:elbow"}`, `follow {ch, source}`) of a well-formed role nothing draws is a warning, as for every primitive block (§2); the friendly blocks name parts, so there an unknown part is an error (§4). Every skeleton block moves a character, so in a scene it is an error (use `does`). ### 8.7 Text view `sin wave down the tail chain · every 1 s · amp 20 · 0.1 s later each segment · each × 0.8 · 4 segments` · `tail 0.5 × behind dy, springing 2 per second, damping 0.3` · `reach hands for the hip in 0.4 s` · `reach right hand to x 80, y 300, hold 1 s, let go` · `reach right hand for the head + 0.1 out and -0.03 up (× its height) in 0.4 s` · `let go with right hand in 0.3 s` · `show the side view` · `swish tail 30° · every 0.7 s` · `flop ears 25° · every 1.2 s` · `bend body 20° in 0.5 s` · `bow 35° in 0.6 s` · `let the ears follow the head × 2` · `turn to the back in 0.24 s`. ### 8.8 Packs Written with the friendly blocks, in **`packs/skeleton/`**, their permanent home (manifests like every behaviour pack, `type: "behaviour"`, ids `c4c:beh:`; `tools/packs.mjs` checks and hashes them and lists them in `packs/index.json` with `file: "skeleton/.json"`, so the site's pages load them like the others; the parity fixture records them in its `skeleton_runs`, `skeleton_packs`, `pose_at` and `pose_at_seq` sections and the Python port runs them from there): `whip_tail` (swish tail, flop ears, a bob, the hair follows), `bow` (1.2.0: turn to the side, bow 40° easing in and out (1.1.0 started fast, `ease: out`, and a dog's knees snapped into the fold) and back, the head, hair and ears follow, turn to the front; a dog or a cat bows as in play, its feet planted; the bird, which has no side view, bows toward the viewer with a nod, its wings sweeping out at its sides), `hands_on_hips` (reach both hands for the hips, a sway, the head tilting; a dog or a cat cannot reach its hips and stands on four legs), `wave_and_look` (1.3.0: the head tilts toward the right hand, which goes up beside the head with the offset form, `at: head` plus dx, dy (0.09, −0.065), and waves out and in three times between (0.13, −0.11) and (0.07, −0.02), lets go, the head comes back; every character in its own view: the elbow out and the forearm up, a dog's or a cat's front paw raised in front of its chest; 1.1.0 aimed so near the head spot that the elbow went up beside the face, 1.2.0's inner end (0.045, −0.03) put the hand on the temple), `floppy_hop` (a hop; the ears, tail, hair and hands follow), `look_around` (side, the other way and back, front, back, front, over and over). They run on 0, 2 and 4 legs with finite poses, the same whatever the stepping (tested), and `tests/skeleton_drawing.test.mjs` draws every one on every template. `packs/behaviours/` holds the 34 packs shipped before v0.4 (`tests/skeleton_engine.test.mjs` fails if a pack there uses a v0.4 block: the older Python studio's `PackPoser` reads that folder and its old rigs cannot draw `reach` or `view`). ### 8.9 Not built yet, and the Python port The renderer side (chained templates, IK toward `pose.reach`, view trees, joint limits, depth, planted feet) is `ui/shapes.js` (`docs/SHAPES.md`). Scene-level views (`view {who}` in a scene) and a reach toward another character ("point at the dog") are not built. **The Python port** (`py/c4c_runtime`) runs §8: `chain`, `follow` (the tick grid in its scheduler, exactly as above), `reach` / `release` with offsets, `view`, the friendly blocks' desugaring (the same output keys and formula text), validation's messages, the pose keys `reach` / `view`, and warm `pose_at`; the parity fixture holds it to the JS engine on the six packs of §8.8 (run from `packs/skeleton/`, which stays their home) and on the skeleton scenarios, within 1e-9 (`docs/PYTHON_PORT.md`). ================================================================ # The Jabbertoon pack format https://jabbertoon.com/docs/packs/ Every move, character, scene and cartoon Jabbertoon ships is a pack: one JSON file with a manifest (id, version, name, author, licence, a hash of its content) and the content itself. Every shipped pack is CC0 and can be fetched from jabbertoon.com/packs/ by any site. This is the public edition of the format. ## Manifest (required on every pack) ```json { "pack": 1, "id": "c4c:beh:bounce", // namespace:type:slug (shipped), or local:char:<12 hex> / local:beh:<12 hex> (made by a user) "type": "character" | "behaviour" | "scene" | "background" | "prop" | "cartoon", "version": "1.0.0", // semver; projects pin id + version "name": "Bounce", "description": "...", // optional, one plain sentence "author": { "id": "c4c", "name": "Jabbertoon" }, // a user-made pack: { "id": "local" }, never a name "license": "cc0" | "cc-by" | "c4c-free" | "c4c-paid" | "own", "remixable": true, "derived_from": [ { "id": "...", "version": "..." } ], "content_hash": "sha256:", "preview": { "moves": ["idle", "walk", "bounce"], "seconds": 2 }, // optional: how a store preview is rendered "content": { ... } // type-specific, see below } ``` **Canonical JSON** (what `content_hash` hashes; `engine/packs.js` `canonical`, `tools/packs.mjs` `contentHash`, `ui/character_pack.js` `contentHash` and the Python port's `canonical` agree byte for byte): - UTF-8; no whitespace; object keys sorted by UTF-16 code units (JavaScript's default sort); keys whose value is `undefined` are left out (JSON has none); - numbers in the ECMAScript shortest round-trip form (`Number.prototype.toString`, what `JSON.stringify` writes): `4.0` → `4`, `0.1` → `0.1`, `1e-7`, `1e+21`; they are **not** rounded to a number of decimals, so every double hashes exactly; `NaN` and `±Infinity` are refused; - strings exactly as `JSON.stringify` writes them (lower-case `\u00XX` escapes, lone surrogates escaped). ## Content by type - **character**: a character made of shapes on a tree of parts (below). - **behaviour**: a block program with `kind: "behaviour"` (the block language spec). - **scene**: a block program with `kind: "scene"` plus the cast roles it expects (`{A: "any", B: "any"}`). - **background / prop**: vector shapes (preferred) or a PNG with a licence. - **cartoon**: `{ cast?, places?, program }` — the file a user saves. `program` is required; `cast` and `places` are optional (they may live in the program instead, or in both); no other key is allowed (`tools/packs.mjs` `checkScenePack` refuses one). `program` is a block program of `kind: "cartoon"` (spec §7); the outer `cast` and `places` are merged into it (`cartoonProgram`; the outer entries win, field by field). A cast entry is one of: - an **inline descriptor** (what cartoons use today, e.g. `packs/scenes/happy_now.json`): `{ "name"?, "legs"?: 2, "at"?: {x, y, h, facing}, "voice"?, "look"?, "note"? }` — `look` is the character's design (below), the rest is spec §7.1/§7.2; - a **pack reference**: `{ "pack": "c4c:char:dog" }` or `{ "pack": {"id": "c4c:char:dog", "version": "1.0.0"} }`, optionally with the descriptor's fields to override the pack's own (`name`, `at`, `voice`, …); - or a role description string (`"any"`), for a cast the page fills in. `engine/validate.js` checks both forms (an unknown field is a warning; `pack` must be a pack id or `{id, version}`, `at` numbers are placement values, `look` as below). A cartoon's moves are behaviour pack references (`does {behaviour: "c4c:beh:bounce"}` or `{id, version}`), resolved by id, so a shared cartoon pulls its packs; an inline behaviour program in a `does` is allowed (a user's own move travels inside the cartoon), and a store cartoon should reference packs instead. ### Character packs Files and ids: `packs/characters/.json`, id `c4c:char:`, type `character`, author `{id: "c4c", name: "Jabbertoon"}`, licence `cc0`, remixable. Six ship: `person`, `kid`, `dog`, `cat`, `bird`, `monster`. They are generated by `tools/export_templates.mjs` from the templates in `ui/shapes.js` (their default colours and proportions, the Outlined look), never written by hand, and a tree built from one draws exactly like its template, command for command. Nothing in a pack is looked up by template name: everything it needs to draw and move is in it. ```json "content": { "tree": { ... }, // the root part of the character's own view (below) "legs": 4, // how many legs it stands on: the parts whose role is "leg" and that hang from something that is not a leg "kind": "dog", // the template it is, or "custom" "height": 178.4, // its height standing at rest, in tree units "footprint": 120, // half the width of its ground shadow, in tree units (its own view) "look": "outlined", // optional: flat, outlined, soft, storybook, bold, crayon "views": [ // optional (absent: one view, "front"); its own view first, with no tree { "view": "side" }, { "view": "front", "tree": { ... }, "footprint": 64 }, { "view": "back", "tree": { ... }, "footprint": 64 } ], "spots": { // optional: the named spots a hand can reach, per view "side": { "head": { "node": "head", "at": [11.1, 7.36], "alt": [23.68, 16] }, "...": {} } }, "silly": true // optional: the joints have no limits (a template's "Joints: Silly"); absent or false, every part's limits apply } ``` **A part** (a node of a tree) holds only these fields: `id` (unique in its tree; spots name parts by it), `role` (a word of the block language's part vocabulary, or `null` for a decoration nothing moves), `side` (`-1`, `1`, or `0`/absent), `index` (0 to 15: legs back to front, chain segments from the root), `shape`, `pos {x, y}` (from the parent's joint), `rot` (the rest angle, degrees, clockwise on screen), `sx`, `sy`, `z` (drawing order among its siblings), `fill` (`#rgb` or `#rrggbb`), `outline` (`false` for none, or a colour), `face`, `children`, and the skeleton's fields: `limits {min, max}` (degrees, counter-clockwise, relative to rest), `flex` (1 or -1), `over [a, b]`, `sign` (-1), `fold`, `bend`, `curl`, `tip {x, y}`, `fit` (a shape). Their meaning is the shapes document's (`docs/SHAPES.md` §2 and §3). Tree units: y points down, the origin is the ground point under the character. **A shape** is `{type, w, h, ox, oy}` plus what its type reads: `ellipse`, `capsule`, `hcapsule`, `teardrop`, `drop`, `tri`; `rrect` (`r`); `egg` (`top`); `spike` (`lean`); `taper` (`w2`); `strokes` (`lw`, `lines [[x1, y1, x2, y2], ...]`); `poly` (`subs [[[x, y], ...], ...]`). This list is `SHAPE_TYPES` in `ui/shapes.js`, the one the drawing uses. Sizes are never negative. Every type but two needs `w` and `h`: `strokes` needs neither, and `poly` needs only `subs` (its `w` and `h` may be left out; the drawing then takes the size of its outlines). **A face** is `{eyeX, eyeY, eyeR, mouthW, mouthH, mouthY}` (required) and any of `eyeCX`, `eyes` (1 or 2), `oneEye`, `slit`, `pupil`, `brows`, `browColor`, `mouthX`, `mouthColor`, `fangs`, `beak {w, h, color}`, `cheeks`, `z`, `profile`; a face of `null` is no face (a head seen from behind). **Views and spots.** Every view has the same roles, sides and indexes and stands on the same ground point, so the same moves play on it. A pose asks for a view (`pose.view`); a view the character does not have draws its own view (a character with no side view that is asked for one bows toward the viewer instead of leaning). A spot is `{node, at, alt?, mirror?, hands?}`: the part it sits on, the place on that part, where it slides when a body shape puts it out of reach, whether it mirrors for the left hand, and `hands: false` when no paw and no wing tip reaches it in that view (an arm still does: docs/SHAPES.md §3.5). **The rules** (`ui/character_pack.js` `validateCharacter`; every error has a path, says what is wrong and says how to fix it, so a person or an AI can repair a pack): only the fields above; roles from the vocabulary; shape types from `SHAPE_TYPES`; every number finite and within ±10000; colours `#rgb` or `#rrggbb`; at most 200 parts in a tree, 32 hanging from one part, 24 levels deep; no `__proto__`, `constructor` or `prototype` key anywhere; `legs` equal to the legs of every view; ids unique in a tree; a spot on a part of its own view; `limits.min` at most `limits.max`; each view listed once; `silly` true or false; at most 200,000 JSON values in the whole content (every object, list, string, number, boolean and null: about 66,000 outline points), a bound on the work of checking it. `schemas/character-pack.schema.json` (https://jabbertoon.com/schemas/character-pack.schema.json, JSON Schema 2020-12) is the same rules as far as JSON Schema can say them; the seven counting and comparing rules (the part count, the legs, unique ids, spots on real parts, min at most max, the points of one poly shape counted over all its outlines, the values in the whole content) are the validator's only, and the schema's description lists them. Ids and names are counted in letters (code points), as JSON Schema counts them: an emoji is one. The validator is `ui/character_pack.js` validateCharacter. The tools for AI assistants (`validate` and `share_link`) check a character with a Python copy of it that gives the same errors, word for word, so a link is made only for a character the studio and the character maker open; the engine's own Python port never reads a character pack. ### User-made packs and the licence `own` A character or a move a person makes is kept as a pack too, in their own browser only (My characters, My moves): - id `local:char:` (a move: `local:beh:<12 hex>`), so the same character has the same id on any device, and an id says nothing about who made it or where: never a random or per-device id; - author `{id: "local"}` with no name: the tools never ask for a person's name or age; - licence `own`: the maker keeps it; remixable `false`; - `derived_from`: the template it started from (`[{"id": "c4c:char:dog", "version": "1.0.0"}]`), or nothing. A user-made pack travels as a file the person saves or as a share code in a link (docs/LINKS.md); nothing is uploaded. ## Catalogs `node tools/packs.mjs --fix --index` writes `packs/index.json` (every pack but the character packs, as it always has) and `packs/characters/index.json` (the character packs), both as `{"pack_index": 1, "count": n, "packs": [...]}` with each pack whole and its `file` under `packs/`. `node tools/packs.mjs` checks every pack (manifest, program, character content, hash) and both catalogs. ### A character's look One format, written by the studio and a cartoon's cast, read by every renderer (`ui/shapes.js` `makeCharacter(look)` builds the shape character from it as it is): ```json "look": { "template": "dog", // person, kid, dog, cat, bird, monster (ui/shapes.js TEMPLATES) "style": "outlined", // the art style: flat, outlined, soft, storybook, bold, crayon (LOOKS) "shape": "classic", // the body shape: classic, bighead, tall (PROPORTIONS) "colors": { "body": "#d9cbbd", "skin": "#9e8a78", "acc": "#5a4636" }, // what each paints: TEMPLATES[].colors "stretch": { "head": 1, "body": 1, "legs": 1 }, // the studio's stretch sliders "options": { "pigtails": true }, // the template's options (kid: pigtails; monster: legs, eyes) "facing": 1 // the way the art faces as designed: 1, or -1 ("face the other way") } ``` Every field is optional; a renderer uses its default for what is missing or unknown (the person, its own default style, the classic shape, the template's colours). `facing` is the art's own facing `A` (spec §7.2): a cartoon renderer draws a character facing its placement's `facing` whatever `A` is. `engine/validate.js` checks the look but only carries it: a look that is not an object is an error; inside it, a colour outside `colors` (`{"template": "dog", "skin": "#9e8a78"}`, the old flat spelling), an unknown field, a name that is not a string, a colour that is not `#rgb` / `#rrggbb`, a stretch that is not a number more than 0 and a `facing` that is not 1 or -1 are warnings. The studio page's own character spec spells the art style `look` and the body shape `proportions`; they are the same fields, under the studio's names: `makeCharacter` reads them when `style` / `shape` are not given, and `engine/validate.js` takes them as it takes `style` / `shape` (a name, a string), so a character the studio saved works in a cast as it is. Given together with the pack spelling, the pack spelling wins, and validation warns ("is the studio's name for "style", which is given too and wins: keep one"). The studio is stricter with a look that comes from outside its own controls (a `.jabbertoon.json` cartoon file, the draft a browser kept), because its Change sheet writes the values into its controls: `ui/shapes.js` `shapeSpecProblem(look)` refuses a template that is not one (or is missing), a style or shape that is not one, a colour that is not `#rgb` / `#rrggbb` or not one of the template's colour keys, a stretch that is not a number from 0.1 to 10 (the sliders run 0.6 to 1.6) or not `head` / `body` / `legs`, an option or option value the template does not have, a `facing` that is not 1 or -1, a `note` that is not a string, and a `__proto__`, `constructor` or `prototype` key. Any other field is carried as it is. A file with such a look is refused, with the character and the field named ("Character A in that file is damaged (its stretch.head "x" is not a number from 0.1 to 10)."); a kept draft with one is mended by `cleanShapeSpec(look)`, which keeps what is right (an unknown template becomes the person, as it was drawn), and the studio says so. ## Resolution order for a reference `{id, version}` 1. `packs/` shipped with the site (our starter packs) 2. the browser cache (Cache API) 3. `https://jabbertoon.com/packs//.json` 4. (later) the store, with entitlement. A missing pack never crashes a render: the character falls back to the default "blob" character and the behaviour to `idle`, and the validator reports it. Built today: step 1 only, through the pages' catalogs (`loadPacks()` reads `packs/index.json`; the engine resolves `does` ids with `createRuntime({ packs })` and falls back to `c4c:beh:idle` with a warning, spec §7.5; the character packs are read from `packs/characters/`). A user-made pack (`local:*`) resolves only in the browser that keeps it. Steps 2–4, the blob character and the remix checks below are the design. ## Rules - Packs are immutable per version. Fixing a pack = new version. Projects keep working. - `derived_from` is filled automatically when a user remixes; `remixable: false` blocks the remix UI and is honoured by the validator. - Deterministic render + `preview` field ⇒ store previews are generated, never uploaded. - No personal data in any pack. No URLs to third-party hosts inside pack content. ================================================================ # Jabbertoon link formats https://jabbertoon.com/docs/links/ Jabbertoon's pages read instructions from the part of a link after the # sign, which the browser never sends to a server. A link can open a shipped move or a ready-made character by name, or a move or a character of your own carried in the link as a share code. This is the public edition of the link reference. ## 1. The rules - **Fragments only.** Everything after `#` stays in the browser: it is never sent to jabbertoon.com, so nothing a person makes is uploaded by sharing it. A link has keys joined by `&`: `#ch=dog&slot=B`. Values are written with `encodeURIComponent` where they need it (share codes never do). - **Nothing changes until all of it is right.** An unknown key, an unknown value, a damaged share code or a key given twice shows a message, changes nothing, and the page keeps working. Names are looked up as the tools' own names only: `#ch=__proto__`, `#ch=constructor` or `#look=toString` are unknown values, not tricks. - **A link is applied once.** After the studio applies a link it takes it off the address, so a reload does not apply it again. A link that replaces a character says so and offers **Undo**. - **The kept cartoon first.** When the studio has a cartoon kept in this browser, it asks "Keep going with your cartoon?" first and applies the link after the answer. Coming back from the block editor or the character maker, it keeps going without asking; a reload after that asks again. - Pack slugs in `#p=` are the last part of a move's pack id (`c4c:beh:sadidle` is `sadidle`), not the names of the move pages. - **What plays from a link today: a move and a character.** A move plays in the block editor (and in the studio, as a character's move); a character opens in the studio or the character maker. No link plays a scene or a whole cartoon yet: today such a code opens its blocks in the block editor, to read, change and share, and the studio says so in one message that offers that, once. ## 2. The studio, /studio/ | link | what it does | |---|---| | `#p=` | that move (a shipped one, by its pack slug) becomes character A's move in line 1 | | `#ch=