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
{
"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."
}
]
}
]
}backdropis an id from a registered pack. The default pack hasfield,pastoral, androom8bs.bodieslists who is on stage, left to right. Each body names acharacterid and anemotion.speakerslists the balloons. A line’sspeakermust 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.4–1 |
| Coy | 0.785 (π/4) |
0.4–1 |
| Bored | 1.571 (π/2) |
0.4–1 |
| Scared | 2.356 (3π/4) |
0.4–1 |
| Sad | 3.142 (π) |
0.4–1 |
| Angry | 3.927 (5π/4) |
0.4–1 |
| Shout | 4.712 (3π/2) |
0.4–1 |
| Laugh | 5.498 (7π/4) |
0.4–1 |
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.
{
"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.
Show all 3 panelsShow first panel only
{
"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."
}
]
}
]
}{
"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.