The Jabbertoon pack format

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)

{
  "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:<hex of the canonical content JSON>",
  "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/<slug>.json, id c4c:char:<slug>, 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.

"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:<the first 12 hex of its content_hash> (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):

"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)
  2. https://jabbertoon.com/packs/<id>/<version>.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.