Start

Writing a strip

The Comic Strip Document, built up from one panel to a two-speaker scene.

A strip is a JSON document: a version number and a list of panels. Each panel names a backdrop, puts some bodies on stage, and gives some of them lines. That is the entire format. You can write it by hand, generate it from any language, or store it in a CMS field.

One panel, one speaker

The smallest useful document
Open in playground →
strip.json
{
  "version": 1,
  "panels": [
    {
      "backdrop": "field",
      "camera": "medium",
      "bodies": [
        {
          "character": "anna",
          "emotion": {
            "angle": 0,
            "intensity": 0.6
          }
        }
      ],
      "speakers": [
        {
          "speaker": "anna",
          "balloon": "speech",
          "text": "My first strip."
        }
      ]
    }
  ]
}
  • backdrop is an id from a registered pack. The default pack has field, pastoral, and room8bs.
  • bodies lists who is on stage, left to right. Each body names a character id and an emotion.
  • speakers lists the balloons. A line’s speaker must be a body in this panel, or "caption".

Emotion is a point, not a word

emotion is { angle, intensity } on a wheel. Angle 0 is HAPPY and each named emotion sits π/4 further round; intensity runs 0 to 1, and 0 means NEUTRAL whatever the angle. The renderer picks the nearest face the pack has, so you never get an error for an angle between two names. Emotions and poses shows the whole wheel.

Feeling angle intensity
Neutral 0 0
Happy 0 0.41
Coy 0.785 (π/4) 0.41
Bored 1.571 (π/2) 0.41
Scared 2.356 (3π/4) 0.41
Sad 3.142 (π) 0.41
Angry 3.927 (5π/4) 0.41
Shout 4.712 (3π/2) 0.41
Laugh 5.498 (7π/4) 0.41

Two speakers

Add a second body and a second line. Balloons draw in speakers order and the layout places each one beside its speaker’s head.

Two bodies, two lines
Open in playground →
strip.json
{
  "version": 1,
  "panels": [
    {
      "backdrop": "pastoral",
      "camera": "medium",
      "bodies": [
        {
          "character": "dan",
          "emotion": {
            "angle": 0,
            "intensity": 0.6
          }
        },
        {
          "character": "anna",
          "emotion": {
            "angle": 0.785398,
            "intensity": 0.5
          }
        }
      ],
      "speakers": [
        {
          "speaker": "dan",
          "balloon": "speech",
          "text": "Two bodies, two lines."
        },
        {
          "speaker": "anna",
          "balloon": "speech",
          "text": "Left to right, in the order you list them."
        }
      ]
    }
  ]
}

A three-panel strip

Panels are independent: each one names its own backdrop, camera, and cast. Characters keep their identity across panels, and their pose cycles so the same face is not repeated panel after panel.

The strip from the home page
Open in playground →
Show all 3 panelsShow first panel only
strip.json
{
  "version": 1,
  "panels": [
    {
      "backdrop": "pastoral",
      "camera": "establishing",
      "bodies": [
        {
          "character": "dan",
          "emotion": {
            "angle": 0,
            "intensity": 0.6
          }
        },
        {
          "character": "anna",
          "emotion": {
            "angle": 0,
            "intensity": 0.4
          }
        }
      ],
      "speakers": [
        {
          "speaker": "caption",
          "balloon": "caption",
          "text": "toonstrip draws comic strips from data."
        },
        {
          "speaker": "dan",
          "balloon": "speech",
          "text": "You write the panels as JSON."
        }
      ]
    },
    {
      "backdrop": "pastoral",
      "camera": "medium",
      "bodies": [
        {
          "character": "anna",
          "emotion": {
            "angle": 2.356194490192345,
            "intensity": 0.6
          }
        },
        {
          "character": "dan",
          "emotion": {
            "angle": 0,
            "intensity": 0.7
          }
        }
      ],
      "speakers": [
        {
          "speaker": "anna",
          "balloon": "speech",
          "text": "And it draws them right here, on a canvas?"
        },
        {
          "speaker": "dan",
          "balloon": "speech",
          "text": "Live. Resize the page and watch it reflow."
        }
      ]
    },
    {
      "backdrop": "pastoral",
      "camera": "reaction",
      "bodies": [
        {
          "character": "anna",
          "emotion": {
            "angle": 5.497787143782138,
            "intensity": 0.9
          }
        }
      ],
      "speakers": [
        {
          "speaker": "anna",
          "balloon": "thought",
          "text": "No build step for the art. No image files to keep in sync."
        }
      ]
    }
  ]
}
strip.json
{
  "version": 1,
  "panels": [
    {
      "backdrop": "pastoral",
      "camera": "establishing",
      "bodies": [
        {
          "character": "dan",
          "emotion": {
            "angle": 0,
            "intensity": 0.6
          }
        },
        {
          "character": "anna",
          "emotion": {
            "angle": 0,
            "intensity": 0.4
          }
        }
      ],
      "speakers": [
        {
          "speaker": "caption",
          "balloon": "caption",
          "text": "toonstrip draws comic strips from data."
        },
        {
          "speaker": "dan",
          "balloon": "speech",
          "text": "You write the panels as JSON."
        }
      ]
    }
    // … 2 more panels
  ]
}

Validating

@toonstrip/schema ships the JSON Schema and a validator. The element and the Astro component validate at the boundary, but validating in your build catches mistakes earlier and with a path to the field.

import { validateDocument, ComicStripValidationError } from "@toonstrip/schema";

try {
  const doc = validateDocument(JSON.parse(raw));
} catch (err) {
  if (err instanceof ComicStripValidationError) console.error(err.message);
}

A validation error names the path and the problem:

Invalid ComicStripDocument:
  /panels/0/camera must be equal to one of the allowed values
  /panels/0 must NOT have additional properties

Note

Unknown keys are rejected, not ignored. emotions instead of emotion is an error, which is what you want from a format that other tools will write.

The validator checks structure only. Whether a character or backdrop id exists is checked when the strip loads against the registered packs, and an unknown id there is a hard error, never a silent fallback.

Next

Emotions and poses, then cameras and panels. Or skip to the playground and edit any of these strips.