Write a panel as a comic script#
Comic writers already have a format. It is not YAML, it has been in use for decades, and asking someone to abandon it in order to try a compiler is a poor trade.
So Scenet reads it.
PAGE ONE
PANEL 1
@shot: full_shot
Alice and Bob face each other on a rainy street corner. She is exasperated.
ALICE
You forgot your umbrella!
BOB
I know.
PANEL 2
@shot: medium_close_up
Closer now. Bob will not meet her eye.
BOB (whisper)
I left it on purpose.
ALICE (shouting)
You what?!
Save that as umbrella.script and compile it exactly like any other document:
scenet build umbrella.script --strip
The rules#
Line |
Means |
|---|---|
|
Starts a new panel. Anything before the first one is an error. |
|
A directive. |
|
The next lines are dialogue spoken by |
|
Same, with a balloon kind. |
Anything else |
Prose. Preserved, never interpreted. |
|
Ignored. Pages are not modelled yet. |
The one detail that trips people up: a speaker cue is recognised by the name being all
caps, not the whole line. BOB (whisper) qualifies, because only BOB is tested.
Prose is never interpreted#
“Alice and Bob face each other on a rainy street corner” is not parsed, not natural-language-processed, and does not affect the output in any way. It is kept because a script is a document people read, and stripping the description would make the file worse for its primary audience.
If you want the rain, you have to say so in the panel description — and rain is not yet a construct in the language.
Front matter#
A script is dialogue and camera direction. It has no way to say who ALICE is, which
puppet she uses, or where she stands. That comes from a YAML preamble between ---
fences:
---
panel:
size: [900, 700]
cast:
ALICE: {reference: alice, pose: pointing, at: left_third}
BOB: {reference: bob, pose: arms_crossed, at: right_third, facing: left}
staging:
- ALICE left_of BOB
- ALICE looking_at BOB
- ALICE ground_shared_with BOB
---
PAGE ONE
PANEL 1
...
Everything in the front matter is a default every panel inherits — exactly the same mechanism as shared defaults in a sequence.
Note the actor ids are written in caps here, to match the speaker cues. That is a convention, not a requirement; the ids simply have to agree.
From Python#
from scenet import parse_script
panels = parse_script("""---
cast:
ALICE: {reference: alice}
---
PANEL 1
@shot: close_up
A quiet room.
ALICE
Is anyone there?
PANEL 2
ALICE (whisper)
Anyone?
""")
# Panels are named by the number in their heading.
assert list(panels) == ["1", "2"]
assert panels["1"].camera.shot.value == "close_up"
assert panels["1"].script[0].text == "Is anyone there?"
# The prose line is preserved in the source and interpreted nowhere.
assert panels["2"].script[0].kind.value == "whisper"
Panel names come straight from the heading, so PANEL 1 becomes "1". The CLI uses
them as filename suffixes: umbrella.script compiles to umbrella.1.svg,
umbrella.2.svg.
parse_script gives you IR;
load_script reads a file; and
compile_document dispatches on the .script
extension so you do not have to care.
The front matter is not optional in practice#
A script with no cast will not parse at all. Validation is total, so a speaker cue naming somebody who is not in the cast is an error at parse time rather than a blank balloon discovered later:
from scenet import ScriptSyntaxError, parse_script
try:
parse_script("PANEL 1" + chr(10) + "ALICE" + chr(10) + "Hello.")
except ScriptSyntaxError as exc:
assert "unknown actor" in str(exc)
assert "ALICE" in str(exc)
That is the first error you will hit when adapting an existing script: every speaker cue needs a matching entry in the front-matter cast.
Both frontends produce the same IR#
This is the point of the whole arrangement. script_front computes no coordinates and
knows nothing about geometry; it produces the same
PanelIR the YAML frontend does, and everything downstream is
unaware that a second syntax exists.
Adding a third syntax means adding a parser and one line in the extension table. Nothing else changes.