comicforge 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {comicforge-0.2.3 → comicforge-0.2.4}/.claude/skills/comicforge/SKILL.md +83 -14
- {comicforge-0.2.3 → comicforge-0.2.4}/.claude/skills/comicforge/reference.md +62 -16
- {comicforge-0.2.3 → comicforge-0.2.4}/.github/workflows/ci.yml +2 -2
- {comicforge-0.2.3 → comicforge-0.2.4}/.github/workflows/docs.yml +1 -1
- {comicforge-0.2.3 → comicforge-0.2.4}/.github/workflows/release.yml +1 -1
- {comicforge-0.2.3 → comicforge-0.2.4}/PKG-INFO +1 -1
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/__init__.py +8 -1
- comicforge-0.2.4/comicforge/bubbles.py +485 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/caption.py +42 -14
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/cli.py +42 -10
- comicforge-0.2.4/comicforge/layout.py +343 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/pixelart.py +3 -3
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/raster.py +94 -12
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/render.py +342 -176
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/validate.py +146 -9
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/claude.md +8 -3
- comicforge-0.2.4/docs/demos/auto-rows.yaml +22 -0
- comicforge-0.2.4/docs/demos/speakers.yaml +47 -0
- comicforge-0.2.4/docs/demos/tails.yaml +44 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/guide/bubbles.md +145 -3
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/guide/captions-frames.md +45 -1
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/guide/images.md +23 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/guide/pages.md +51 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/reference/cli.md +36 -4
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/reference/python-api.md +61 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/reference/spec.md +17 -1
- {comicforge-0.2.3 → comicforge-0.2.4}/pyproject.toml +2 -2
- {comicforge-0.2.3 → comicforge-0.2.4}/skills/comicforge/SKILL.md +83 -14
- {comicforge-0.2.3 → comicforge-0.2.4}/skills/comicforge/reference.md +62 -16
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/test_bubbles.py +35 -1
- comicforge-0.2.4/tests/test_caption.py +128 -0
- comicforge-0.2.4/tests/test_layout.py +311 -0
- comicforge-0.2.4/tests/test_raster.py +447 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/uv.lock +7 -7
- comicforge-0.2.3/comicforge/bubbles.py +0 -232
- comicforge-0.2.3/tests/test_caption.py +0 -68
- comicforge-0.2.3/tests/test_raster.py +0 -241
- {comicforge-0.2.3 → comicforge-0.2.4}/.github/dependabot.yml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/.gitignore +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/.python-version +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/CLAUDE.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/README.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/__main__.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/inspire.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/library.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/scaffold.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/comicforge/scene.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/art/characters.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/art/inspire.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/art/pixel.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/art/scenes.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/assets/favicon.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/assets/logo.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/concepts.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/contributing.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/arms.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/bubble-at.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/bubble-stack.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/bubbles.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/captions.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/coords.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/faces.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/frames.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/grid.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/illustration.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/images.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/lettering.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/pixel.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/poses.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/raster-art/backdrop.png +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/scenes.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/strip.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/tutorial-art/characters/pip/base.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/tutorial-art/characters/pip/character.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/tutorial-art/characters/pip/face-happy.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/tutorial-art/characters/pip/face-neutral.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/tutorial-art/pages/first.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/demos/tutorial-art/pixel/heart.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/gallery.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/guide/actors.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/guide/illustrations.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/guide/pixel-art.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/guide/scenes.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/hooks/render_demos.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/index.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/install.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/quickstart.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/reference/troubleshooting.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/starting-a-project.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/docs/stylesheets/extra.css +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/README.md +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/README.md.old +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/bara/character.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/bara/face-happy.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/bara/face-neutral.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/bara/poses/sit/base.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/bara/poses/sit/pose.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/bara/poses/walk/base.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/bara/poses/walk/pose.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/arms-crossed.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/arms-down.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/arms-hips.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/arms-point.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/arms-thumbsup.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/arms-wave.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/base.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/character.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/face-angry.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/face-happy.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/face-laugh.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/face-neutral.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/face-sad.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/face-surprised.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/characters/tom/face-wink.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/pages/dvur-scene.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/pages/kosticka.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/pages/slepice.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/pixel/bone.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/pixel/heart.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/pixel/star.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/pixel/sun.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/references.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/scenes/dvur/base.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/scenes/dvur/scene.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/scenes/dvur/weather-clear.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/scenes/dvur/weather-rain.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/scenes/pokoj/base.svg +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/scenes/pokoj/scene.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/examples/pes/theme.yaml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/hooks/pre-commit +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/mkdocs.yml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/poe_tasks.toml +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/__init__.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/conftest.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/test_cli.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/test_inspire.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/test_library.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/test_pixelart.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/test_render.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/test_scaffold.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/test_scene.py +0 -0
- {comicforge-0.2.3 → comicforge-0.2.4}/tests/test_validate.py +0 -0
|
@@ -47,15 +47,21 @@ needs no Python (see [`reference.md`](reference.md)).
|
|
|
47
47
|
against the libraries without drawing anything, and lists *all* problems at
|
|
48
48
|
once. Unlike `render`, it flags mis-spelled keys (e.g. `fcae:`, `imge:`) that
|
|
49
49
|
render silently ignores. Exits non-zero when anything is wrong. Works on page
|
|
50
|
-
and `scene` specs.
|
|
50
|
+
and `scene` specs. It also prints bubble layout **warnings** (out of reading
|
|
51
|
+
order, crossing tails, a bubble covering a speaker point) — they don't fail
|
|
52
|
+
the check unless `--strict`, but fix them before calling a page done.
|
|
51
53
|
4. Render:
|
|
52
54
|
- comic page: `cmf render mystrip.yaml -o out.pdf`
|
|
53
55
|
- single illustration: `cmf scene myscene.yaml -o out.png`
|
|
54
56
|
- one panel of a page (to iterate on a single panel without re-reading the
|
|
55
57
|
whole page): `cmf panel mystrip.yaml --row 0 --col 1 -o panel.png`
|
|
56
58
|
(0-indexed; defaults to row 0 col 0), or `--all` to write every panel into a
|
|
57
|
-
directory.
|
|
58
|
-
full
|
|
59
|
+
directory. Add `--on-page` to get the panel exactly as the page draws it
|
|
60
|
+
(full frame stroke, page background at the corners); `--layout` prints
|
|
61
|
+
each panel's position on the page as JSON (`--all` writes it as
|
|
62
|
+
`layout.json`) to lay the renders out again. This is the right way
|
|
63
|
+
to inspect one panel — **do not** render the full page and crop it with
|
|
64
|
+
ImageMagick or other tooling.
|
|
59
65
|
- one character on its own (to eyeball a pose/expression):
|
|
60
66
|
`cmf character bara sit happy --library examples/pes/characters`
|
|
61
67
|
— extra args are bare pose/variant names or `key=value` (`pose=walk`,
|
|
@@ -93,7 +99,9 @@ pixel_dir: "../pixel" # path to pixel-art dir (omit if inline only)
|
|
|
93
99
|
|
|
94
100
|
rows: # page is a stack of rows…
|
|
95
101
|
- height: 1.0 # relative row height (default 1), or
|
|
96
|
-
# height_mm: 60 for a fixed height
|
|
102
|
+
# height_mm: 60 for a fixed height, or
|
|
103
|
+
# height: auto to size it from its images
|
|
104
|
+
# + captions (squeezed to fit the page) —
|
|
97
105
|
# weighted rows share what is left
|
|
98
106
|
panels: # …each row is a left→right list of panels
|
|
99
107
|
- width: 1.0 # relative panel width (default 1)
|
|
@@ -159,6 +167,22 @@ image: {src: "01.png", fit: contain} # or the long form, to pick the fit
|
|
|
159
167
|
distorts, exactly like a `scene:` background.
|
|
160
168
|
- `fit: contain` scales it to fit *inside* the panel; the leftover strips show
|
|
161
169
|
the panel's `bg:` colour.
|
|
170
|
+
- `crop: {top, bottom, left, right}` trims that many *image pixels* off each
|
|
171
|
+
edge before fitting — the trimmed image is what `fit`, a scene canvas and a
|
|
172
|
+
`height: auto` row see.
|
|
173
|
+
- `at: t|b|l|r|tl|tr|bl|br|c` pins the image to that edge, so `fit: cover`
|
|
174
|
+
crops from the other side (`at: t` keeps heads, loses feet). Default centred.
|
|
175
|
+
- **Making a page of images fit:** give the rows `height: auto`. Each row is as
|
|
176
|
+
tall as its images want at their panel width plus the caption band; if the
|
|
177
|
+
rows overflow the page their art (never the captions) is squeezed by one
|
|
178
|
+
common factor and `cover` crops the excess. Steer what gets lost with `at:`
|
|
179
|
+
per image, and `crop:` away empty sky/floor first so the squeeze is smaller.
|
|
180
|
+
Don't compute row `height_mm` from image sizes and caption wraps yourself.
|
|
181
|
+
To paginate, call `comicforge.page_squeeze(spec)` (1.0 = fits unsqueezed) on
|
|
182
|
+
a candidate page and break when it drops below what you tolerate.
|
|
183
|
+
- Captions in panels of varying width: `caption_style: {max_chars: auto}` wraps
|
|
184
|
+
to the band's width; `em` (default 1.0) scales the measure for a narrower
|
|
185
|
+
(<1) or wider (>1) font than DejaVu Sans.
|
|
162
186
|
- `.png`, `.jpg`/`.jpeg`, `.gif` and `.webp` are supported.
|
|
163
187
|
- The image is **embedded** as a base64 data URI, so an `.svg` or `.pdf` render
|
|
164
188
|
is one self-contained file that still works when moved. That also means the
|
|
@@ -171,13 +195,27 @@ A standalone illustration can be image-backed too — set `type: scene` with an
|
|
|
171
195
|
dimensions times `scale:` (default `1`, i.e. one output px per image px).
|
|
172
196
|
|
|
173
197
|
**Where bubbles land on a raster panel.** There are no actors to anchor to, so
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
bubble
|
|
179
|
-
|
|
180
|
-
|
|
198
|
+
mark each speaking head with a panel-level `speakers: {name: [x, y]}` (panel
|
|
199
|
+
fractions) and give bubbles `speaker: name`. The tail then aims at the point
|
|
200
|
+
(unless `to:` is given), and every bubble with a speaker and no `x`/`y`/`at` is
|
|
201
|
+
placed **in reading order**: on its speaker's side, its top clearly below the
|
|
202
|
+
previous bubble's top, clear of speaker points and earlier tails. So a
|
|
203
|
+
right-hand first speaker gets the top right and the reply goes lower left. Emit
|
|
204
|
+
the lines in the order they are spoken and omit coordinates.
|
|
205
|
+
|
|
206
|
+
Without `speakers:`, a bubble with no `y` is placed below the measured bottom of
|
|
207
|
+
the bubble before it, starting near the panel top, and a bubble with no `x` is
|
|
208
|
+
horizontally centred. Every bubble — explicit or automatic — is then nudged so
|
|
209
|
+
it stays inside the panel (one wider or taller than the panel is centred
|
|
210
|
+
instead).
|
|
211
|
+
|
|
212
|
+
```yaml
|
|
213
|
+
image: "art/03.png"
|
|
214
|
+
speakers: {ema: [0.72, 0.3], jan: [0.25, 0.42]}
|
|
215
|
+
bubbles:
|
|
216
|
+
- {text: "Kde jsi byl?", speaker: ema}
|
|
217
|
+
- {text: "Venku.", speaker: jan}
|
|
218
|
+
```
|
|
181
219
|
|
|
182
220
|
## Standalone illustration (no comic grid)
|
|
183
221
|
|
|
@@ -212,6 +250,7 @@ bubbles: [ ... ]
|
|
|
212
250
|
kind: speech # speech | thought | shout
|
|
213
251
|
speaker: tom # OPTIONAL: auto-place above this actor + aim the tail at
|
|
214
252
|
# their head. Prefer this over manual x/y/to.
|
|
253
|
+
# Also names a panel `speakers:` point (actors win).
|
|
215
254
|
at: tr # OPTIONAL corner/edge to hug: tl t tr l c r bl b br.
|
|
216
255
|
# Each column (l/c/r) stacks its own top and
|
|
217
256
|
# bottom, so tl + tr sit side by side.
|
|
@@ -221,9 +260,21 @@ bubbles: [ ... ]
|
|
|
221
260
|
# kept inside the panel.
|
|
222
261
|
to: [0.4, 0.5] # OPTIONAL tail target (panel fraction); else speaker's head
|
|
223
262
|
max_chars: 22 # wrap width (optional)
|
|
263
|
+
glue_singles: false # OPTIONAL let a one-letter word end a line (default: it
|
|
264
|
+
# is tied to the next word; see Line breaks below)
|
|
224
265
|
fs: 16 # font size px (optional; overrides bubble_style.font_size)
|
|
225
266
|
uppercase: true # OPTIONAL: force this bubble's text to CAPS
|
|
226
267
|
# (overrides bubble_style.uppercase)
|
|
268
|
+
tail_from: r # OPTIONAL where the tail leaves: edge t/b/l/r, a position
|
|
269
|
+
# 0..1 along the auto edge, or {edge: r, pos: 0.8}
|
|
270
|
+
tail: curve # OPTIONAL wedge (default) | curve: tapered comic tail with
|
|
271
|
+
# curved sides | line: a thin line running to just
|
|
272
|
+
# short of the speaker | none. Any kind; a thought
|
|
273
|
+
# keeps its circles (dotted along a line)
|
|
274
|
+
tail_bend: 0.4 # OPTIONAL curve/line bow, -1..1, 0 straight, + = right of
|
|
275
|
+
# travel; omit for a gentle bend away from the
|
|
276
|
+
# nearest bubble (or panel centre)
|
|
277
|
+
tail_gap: 12 # OPTIONAL px a line tail stops short of its target
|
|
227
278
|
```
|
|
228
279
|
|
|
229
280
|
**Page-wide bubble defaults** — set `bubble_style` at the *top level* of a
|
|
@@ -239,6 +290,8 @@ bubble_style:
|
|
|
239
290
|
ink: "#21304a" # text colour; font: "DejaVu Sans, sans-serif"
|
|
240
291
|
em: 1.0 # width scale of the text measure — 0.8 for a narrow
|
|
241
292
|
# handwriting font, so bubbles hug the words
|
|
293
|
+
tail: wedge # wedge | curve | line | none; tail_bend: (auto)
|
|
294
|
+
# tail_gap: 12; tail_from: (auto)
|
|
242
295
|
rows: [ ... ]
|
|
243
296
|
```
|
|
244
297
|
|
|
@@ -247,12 +300,28 @@ along the bottom of the panel, inside the frame, separated from the art by a
|
|
|
247
300
|
hairline in the frame colour. The art box shrinks to make room, so bubble and
|
|
248
301
|
actor coordinates stay relative to the picture. Page-wide look via
|
|
249
302
|
`caption_style` (`font`, `font_size`, `ink`, `bg`, `pad`, `max_chars`,
|
|
250
|
-
`align: left|center`, `rule`, `uppercase`);
|
|
303
|
+
`align: left|center`, `rule`, `uppercase`, `glue_singles`);
|
|
304
|
+
`comicforge.caption.height(text, style)` tells
|
|
251
305
|
you how tall a band will be, for sizing rows.
|
|
252
306
|
|
|
307
|
+
**Line breaks.** Wrapping breaks between words, with two exceptions, in bubbles
|
|
308
|
+
and captions alike. A one-letter word is tied to the word after it, because
|
|
309
|
+
Czech typography does not leave a one-letter preposition or conjunction
|
|
310
|
+
(`k s v z o u a i`) at the end of a line — set `glue_singles: false` on the
|
|
311
|
+
style or the line to allow it. A no-break space (`U+00A0`, or `U+202F`) is
|
|
312
|
+
never broken at, so type one to tie words the automatic rule cannot know about
|
|
313
|
+
(`v roce 1223`, a name and its title). Ordinary runs of spaces and newlines
|
|
314
|
+
still collapse to one space.
|
|
315
|
+
|
|
253
316
|
`thought` draws an ellipse with a trail of dots; `shout` draws a spiky burst.
|
|
254
|
-
The tail is a slim
|
|
255
|
-
short so it never overlaps the figure
|
|
317
|
+
The tail is a slim wedge dropping from the bubble underside; its tip is capped
|
|
318
|
+
short so it never overlaps the figure (`tail: curve` draws the classic curved
|
|
319
|
+
comic tail, `tail: line` a thin line to just short of the speaker, `tail: none`
|
|
320
|
+
nothing).
|
|
321
|
+
|
|
322
|
+
`cmf validate` also prints layout **warnings** (exit 0 unless `--strict`): a
|
|
323
|
+
bubble out of reading order, crossing tails, a bubble covering a speaker point.
|
|
324
|
+
When one shows up, nudge that bubble with `at:` or `x`/`y`.
|
|
256
325
|
|
|
257
326
|
### pixel art
|
|
258
327
|
|
|
@@ -440,15 +440,21 @@ renders a default-faced, default-posed actor with no error. `validate` flags:
|
|
|
440
440
|
- actor / scene keys that aren't reserved and aren't a real slot (likely typos)
|
|
441
441
|
- panel keys the renderer doesn't know — `imge:` for `image:`, say
|
|
442
442
|
- raster `image:` files that are missing, unreadable, or of an unsupported type;
|
|
443
|
-
an `image:` with no `src`, an unknown image key, an unknown `fit`
|
|
444
|
-
|
|
443
|
+
an `image:` with no `src`, an unknown image key, an unknown `fit` or `at`,
|
|
444
|
+
a bad `crop` (unknown side, negative, or trimming the whole image); a row
|
|
445
|
+
`height` that is neither a weight nor `auto`, an `auto` row with no image
|
|
446
|
+
- bubble `speaker` naming neither an actor nor a `speakers:` point in the panel;
|
|
447
|
+
a malformed `speakers:` point; unknown bubble `kind`, `tail`, `tail_from`,
|
|
448
|
+
or a `tail_bend` outside -1..1
|
|
445
449
|
- structural holes (a page with no `rows`, a row with no `panels`, a bubble with
|
|
446
450
|
no `text`, a `scene` spec with neither `scene:` nor `image:`); an unknown
|
|
447
451
|
`type:`, or a `type:` that contradicts the structure (a `scene` spec carrying
|
|
448
452
|
`rows`)
|
|
449
453
|
|
|
450
454
|
Exits `0` when the spec is sound, `1` (with a bulleted problem list) otherwise —
|
|
451
|
-
so it works in a pre-render check or CI.
|
|
455
|
+
so it works in a pre-render check or CI. It also lays out the bubbles and
|
|
456
|
+
prints **warnings** — a bubble out of reading order, tails that cross, a bubble
|
|
457
|
+
covering a speaker point — which only fail the check with `--strict`.
|
|
452
458
|
|
|
453
459
|
```bash
|
|
454
460
|
cmf validate examples/pes/pages/slepice.yaml # -> "...: ok"
|
|
@@ -479,6 +485,13 @@ cmf panel <spec.yaml> [-o <output>] # default: output/<spec>-r<R>c<C>-<ts>.png
|
|
|
479
485
|
[--all] # render every panel into a directory
|
|
480
486
|
# (default: output/<spec>-panels-<ts>/)
|
|
481
487
|
[--scale 0.5] # size vs full-page (default 0.5 = low res)
|
|
488
|
+
[--on-page] # crop panel + half a gutter out of the page render,
|
|
489
|
+
# so frame/corners look exactly as on the page
|
|
490
|
+
[--layout] # print each panel's page position as JSON, no render
|
|
491
|
+
# ({width, height, panels: [{row, col, file,
|
|
492
|
+
# box: {x,y,w,h}, image: {x,y,w,h}}]}, page px;
|
|
493
|
+
# image = what the render covers, honours --on-page);
|
|
494
|
+
# --all also writes it as layout.json
|
|
482
495
|
[--library <dir>]
|
|
483
496
|
[--scenes <dir>]
|
|
484
497
|
[--pixel-dir <dir>]
|
|
@@ -602,34 +615,63 @@ For the full spec grammar, see [SKILL.md](SKILL.md). Key points:
|
|
|
602
615
|
corner radius also clips the art),
|
|
603
616
|
`bubble_style` (page-wide bubble look: `font`, `font_size`, `pad`, `radius`,
|
|
604
617
|
`stroke`, `stroke_width`, `fill`, `ink`, `uppercase`, `em` — width scale of
|
|
605
|
-
the text measure for narrower fonts
|
|
618
|
+
the text measure for narrower fonts; `tail` `wedge`/`curve`/`line`/`none`,
|
|
619
|
+
`tail_bend`, `tail_gap`, `tail_from`)
|
|
606
620
|
- **Rows and panels**: `rows[].height` (relative weight) or `rows[].height_mm`
|
|
607
|
-
(fixed; weighted rows share the rest, all-fixed leaves the bottom blank)
|
|
621
|
+
(fixed; weighted rows share the rest, all-fixed leaves the bottom blank) or
|
|
622
|
+
`rows[].height: auto` (tallest of each panel's image aspect at its width,
|
|
623
|
+
after `crop:`, plus its caption band; auto rows that overflow the page have
|
|
624
|
+
their art — not their captions — squeezed by one common factor, the excess
|
|
625
|
+
cropped by `cover`; needs an `image:` panel; fixed rows are measured first,
|
|
626
|
+
weighted rows get what is left; `comicforge.page_squeeze(spec)` returns that
|
|
627
|
+
art factor, 1.0 when nothing is squeezed — use it to paginate),
|
|
608
628
|
`rows[].panels[].width` (relative weight), panel keys: `bg`, `frame`
|
|
609
629
|
(per-panel override), `caption` (narration band under the art, inside the
|
|
610
630
|
frame: a string or `{text, max_chars}`; page-wide `caption_style` with
|
|
611
|
-
`font`, `font_size`, `ink`, `bg`, `pad`, `max_chars
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
631
|
+
`font`, `font_size`, `ink`, `bg`, `pad`, `max_chars` (a count, or `auto` to
|
|
632
|
+
wrap to the band's width), `em` (width scale of the `auto` measure),
|
|
633
|
+
`align`, `rule`, `uppercase`, `glue_singles`),
|
|
634
|
+
`scene`, `image`, `actors`, `pixel`, `bubbles`, `speakers` (`{name: [x, y]}`
|
|
635
|
+
head points in panel fractions that bubbles' `speaker:` resolves to when no
|
|
636
|
+
actor has that char) — `validate` flags any other panel key as a typo
|
|
637
|
+
- **Line breaking** (bubbles and captions): a one-letter word is tied to the
|
|
638
|
+
word after it with a no-break space, so a Czech one-letter preposition or
|
|
639
|
+
conjunction (`k s v z o u a i`) never ends a line; `glue_singles: false` on
|
|
640
|
+
the style or the individual line turns that off. A no-break space in the
|
|
641
|
+
text (`U+00A0`, `U+202F`) is never a break point, which is how an author
|
|
642
|
+
ties words the rule cannot know about. Other whitespace runs collapse to one
|
|
643
|
+
space.
|
|
644
|
+
- **Image keys**: `image: path.png` or `image: {src:, fit:, at:, crop:}` with
|
|
615
645
|
`fit: cover` (default; scale-to-fill + centre-crop) or `contain`
|
|
616
|
-
(fit inside + letterbox)
|
|
646
|
+
(fit inside + letterbox); `at:` (`t`, `b`, `l`, `r`, `tl`, `tr`, `bl`, `br`,
|
|
647
|
+
`c` — default centre) pins the image to that edge so `cover` crops the
|
|
648
|
+
opposite side; `crop: {top, bottom, left, right}` trims image pixels off
|
|
649
|
+
each edge before fitting (and before a `height: auto` row or a scene canvas
|
|
650
|
+
measures it). The path resolves against the spec file's dir and
|
|
617
651
|
the image is embedded as a base64 data URI, so SVG/PDF output stays
|
|
618
652
|
self-contained. A `type: scene` spec can use `image:` in place of `scene:`,
|
|
619
653
|
and takes its canvas size from the image
|
|
620
654
|
- **Actor keys**: `char`, `pose` (optional; defaults to the character's default
|
|
621
655
|
pose), per-slot variant keys (`face`, `arms`, etc.), `x`, `y`, `scale`, `flip`
|
|
622
656
|
- **Bubble keys**: `text`, `kind` (speech/thought/shout), `speaker`, optional
|
|
623
|
-
`at`/`x`/`y`/`to`/`max_chars`/`fs`/`uppercase` (`fs
|
|
624
|
-
the page-level `bubble_style`).
|
|
657
|
+
`at`/`x`/`y`/`to`/`max_chars`/`fs`/`uppercase`/`glue_singles` (`fs`,
|
|
658
|
+
`uppercase` and `glue_singles` override the page-level `bubble_style`).
|
|
659
|
+
Omit `y` and bubbles stack down by their
|
|
625
660
|
measured height without overlapping; omit `x` and they centre (or sit above
|
|
626
661
|
their `speaker`). `at` hugs a corner or edge instead — `tl`, `t`, `tr`, `l`,
|
|
627
662
|
`c`, `r`, `bl`, `b`, `br` — and each column keeps its own top and bottom
|
|
628
663
|
stack, so `tl` + `tr` sit side by side and `bl` climbs up from the bottom.
|
|
629
|
-
`to: [x, y]` (panel fractions) aims a tail without a speaker.
|
|
630
|
-
|
|
631
|
-
`
|
|
632
|
-
|
|
664
|
+
`to: [x, y]` (panel fractions) aims a tail without a speaker. `tail_from`
|
|
665
|
+
(`t`/`b`/`l`/`r`, a position 0..1, or `{edge, pos}`) picks where the tail
|
|
666
|
+
leaves; `tail` (`wedge`/`curve`/`line`/`none`, any kind) picks the look
|
|
667
|
+
and `tail_bend` (-1..1, auto: away from the nearest bubble) bows a curve
|
|
668
|
+
or line. On a panel with
|
|
669
|
+
`speakers:`, bubbles with a speaker and no `x`/`y`/`at` are placed in
|
|
670
|
+
reading order (speaker's side, each top clearly below the previous one's,
|
|
671
|
+
clear of heads and earlier tails). Every bubble is
|
|
672
|
+
kept inside the panel — so a generated spec over raster panels marks the heads
|
|
673
|
+
in `speakers:`, emits the lines in spoken order with a `speaker` each, and
|
|
674
|
+
omits coordinates; reach for `at` or `x`/`y` only when `validate` warns
|
|
633
675
|
- **Pixel keys**: `art` (library name) or `grid`+`palette`, plus `x`/`y`/`scale`
|
|
634
676
|
|
|
635
677
|
---
|
|
@@ -645,6 +687,10 @@ render_spec("examples/pes/pages/slepice.yaml", "slepice.png")
|
|
|
645
687
|
# Render a standalone scene
|
|
646
688
|
render_scene("examples/pes/pages/dvur-scene.yaml", "dvur.png")
|
|
647
689
|
|
|
690
|
+
# Bubble boxes + tail endpoints of one panel, in panel fractions, no render
|
|
691
|
+
from comicforge import bubble_layout
|
|
692
|
+
bubble_layout("examples/pes/pages/slepice.yaml", row=0, col=1)["bubbles"]
|
|
693
|
+
|
|
648
694
|
# Load a spec dict for inspection
|
|
649
695
|
spec = load_spec("examples/pes/pages/slepice.yaml")
|
|
650
696
|
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
run: sudo apt-get update && sudo apt-get install -y libcairo2
|
|
22
22
|
|
|
23
23
|
- name: Install uv
|
|
24
|
-
uses: astral-sh/setup-uv@v10.0
|
|
24
|
+
uses: astral-sh/setup-uv@v10.1.0
|
|
25
25
|
with:
|
|
26
26
|
enable-cache: true
|
|
27
27
|
|
|
@@ -70,7 +70,7 @@ jobs:
|
|
|
70
70
|
run: sudo apt-get update && sudo apt-get install -y libcairo2
|
|
71
71
|
|
|
72
72
|
- name: Install uv
|
|
73
|
-
uses: astral-sh/setup-uv@v10.0
|
|
73
|
+
uses: astral-sh/setup-uv@v10.1.0
|
|
74
74
|
with:
|
|
75
75
|
enable-cache: true
|
|
76
76
|
|
|
@@ -5,6 +5,13 @@ Designed so an LLM (or you) can author pages as plain declarative text.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from .pixelart import PixelLibrary # noqa: F401
|
|
8
|
-
from .render import
|
|
8
|
+
from .render import ( # noqa: F401
|
|
9
|
+
bubble_layout,
|
|
10
|
+
load_spec,
|
|
11
|
+
page_squeeze,
|
|
12
|
+
panel_layout,
|
|
13
|
+
render_scene,
|
|
14
|
+
render_spec,
|
|
15
|
+
)
|
|
9
16
|
|
|
10
17
|
__version__ = "0.2.0"
|