comicforge 0.1.0__tar.gz → 0.2.1__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.
Files changed (136) hide show
  1. {comicforge-0.1.0 → comicforge-0.2.1}/.claude/skills/comicforge/SKILL.md +31 -2
  2. {comicforge-0.1.0 → comicforge-0.2.1}/.claude/skills/comicforge/reference.md +27 -10
  3. comicforge-0.2.1/.github/workflows/docs.yml +69 -0
  4. {comicforge-0.1.0 → comicforge-0.2.1}/.gitignore +9 -0
  5. {comicforge-0.1.0 → comicforge-0.2.1}/CLAUDE.md +26 -1
  6. {comicforge-0.1.0 → comicforge-0.2.1}/PKG-INFO +20 -8
  7. {comicforge-0.1.0 → comicforge-0.2.1}/README.md +19 -7
  8. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/__init__.py +1 -1
  9. comicforge-0.2.1/comicforge/bubbles.py +232 -0
  10. comicforge-0.2.1/comicforge/caption.py +91 -0
  11. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/render.py +169 -45
  12. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/scaffold.py +2 -0
  13. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/validate.py +23 -3
  14. comicforge-0.2.1/docs/art/characters.md +205 -0
  15. comicforge-0.2.1/docs/art/inspire.md +147 -0
  16. comicforge-0.2.1/docs/art/pixel.md +91 -0
  17. comicforge-0.2.1/docs/art/scenes.md +145 -0
  18. comicforge-0.2.1/docs/assets/favicon.svg +5 -0
  19. comicforge-0.2.1/docs/assets/logo.svg +6 -0
  20. comicforge-0.2.1/docs/claude.md +118 -0
  21. comicforge-0.2.1/docs/concepts.md +144 -0
  22. comicforge-0.2.1/docs/contributing.md +129 -0
  23. comicforge-0.2.1/docs/demos/arms.yaml +15 -0
  24. comicforge-0.2.1/docs/demos/bubble-at.yaml +21 -0
  25. comicforge-0.2.1/docs/demos/bubble-stack.yaml +15 -0
  26. comicforge-0.2.1/docs/demos/bubbles.yaml +19 -0
  27. comicforge-0.2.1/docs/demos/captions.yaml +18 -0
  28. comicforge-0.2.1/docs/demos/coords.yaml +17 -0
  29. comicforge-0.2.1/docs/demos/faces.yaml +18 -0
  30. comicforge-0.2.1/docs/demos/frames.yaml +19 -0
  31. comicforge-0.2.1/docs/demos/grid.yaml +24 -0
  32. comicforge-0.2.1/docs/demos/illustration.yaml +14 -0
  33. comicforge-0.2.1/docs/demos/images.yaml +24 -0
  34. comicforge-0.2.1/docs/demos/lettering.yaml +23 -0
  35. comicforge-0.2.1/docs/demos/pixel.yaml +28 -0
  36. comicforge-0.2.1/docs/demos/poses.yaml +12 -0
  37. comicforge-0.2.1/docs/demos/raster-art/backdrop.png +0 -0
  38. comicforge-0.2.1/docs/demos/scenes.yaml +13 -0
  39. comicforge-0.2.1/docs/demos/strip.yaml +37 -0
  40. comicforge-0.2.1/docs/demos/tutorial-art/characters/pip/base.svg +12 -0
  41. comicforge-0.2.1/docs/demos/tutorial-art/characters/pip/character.yaml +6 -0
  42. comicforge-0.2.1/docs/demos/tutorial-art/characters/pip/face-happy.svg +7 -0
  43. comicforge-0.2.1/docs/demos/tutorial-art/characters/pip/face-neutral.svg +7 -0
  44. comicforge-0.2.1/docs/demos/tutorial-art/pages/first.yaml +16 -0
  45. comicforge-0.2.1/docs/demos/tutorial-art/pixel/heart.yaml +10 -0
  46. comicforge-0.2.1/docs/gallery.md +133 -0
  47. comicforge-0.2.1/docs/guide/actors.md +157 -0
  48. comicforge-0.2.1/docs/guide/bubbles.md +201 -0
  49. comicforge-0.2.1/docs/guide/captions-frames.md +153 -0
  50. comicforge-0.2.1/docs/guide/illustrations.md +113 -0
  51. comicforge-0.2.1/docs/guide/images.md +116 -0
  52. comicforge-0.2.1/docs/guide/pages.md +159 -0
  53. comicforge-0.2.1/docs/guide/pixel-art.md +126 -0
  54. comicforge-0.2.1/docs/guide/scenes.md +91 -0
  55. comicforge-0.2.1/docs/hooks/render_demos.py +105 -0
  56. comicforge-0.2.1/docs/index.md +127 -0
  57. comicforge-0.2.1/docs/install.md +126 -0
  58. comicforge-0.2.1/docs/quickstart.md +191 -0
  59. comicforge-0.2.1/docs/reference/cli.md +331 -0
  60. comicforge-0.2.1/docs/reference/python-api.md +179 -0
  61. comicforge-0.2.1/docs/reference/spec.md +259 -0
  62. comicforge-0.2.1/docs/reference/troubleshooting.md +182 -0
  63. comicforge-0.2.1/docs/starting-a-project.md +118 -0
  64. comicforge-0.2.1/docs/stylesheets/extra.css +188 -0
  65. comicforge-0.2.1/mkdocs.yml +164 -0
  66. {comicforge-0.1.0 → comicforge-0.2.1}/poe_tasks.toml +8 -0
  67. {comicforge-0.1.0 → comicforge-0.2.1}/pyproject.toml +4 -1
  68. {comicforge-0.1.0 → comicforge-0.2.1}/skills/comicforge/SKILL.md +31 -2
  69. {comicforge-0.1.0 → comicforge-0.2.1}/skills/comicforge/reference.md +27 -10
  70. comicforge-0.2.1/tests/test_bubbles.py +62 -0
  71. comicforge-0.2.1/tests/test_caption.py +68 -0
  72. {comicforge-0.1.0 → comicforge-0.2.1}/tests/test_render.py +121 -0
  73. {comicforge-0.1.0 → comicforge-0.2.1}/uv.lock +481 -55
  74. comicforge-0.1.0/comicforge/bubbles.py +0 -167
  75. comicforge-0.1.0/docs/starting-a-project.md +0 -70
  76. comicforge-0.1.0/tests/test_bubbles.py +0 -32
  77. {comicforge-0.1.0 → comicforge-0.2.1}/.github/dependabot.yml +0 -0
  78. {comicforge-0.1.0 → comicforge-0.2.1}/.github/workflows/ci.yml +0 -0
  79. {comicforge-0.1.0 → comicforge-0.2.1}/.github/workflows/release.yml +0 -0
  80. {comicforge-0.1.0 → comicforge-0.2.1}/.python-version +0 -0
  81. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/__main__.py +0 -0
  82. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/cli.py +0 -0
  83. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/inspire.py +0 -0
  84. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/library.py +0 -0
  85. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/pixelart.py +0 -0
  86. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/raster.py +0 -0
  87. {comicforge-0.1.0 → comicforge-0.2.1}/comicforge/scene.py +0 -0
  88. {comicforge-0.1.0 → comicforge-0.2.1}/examples/README.md +0 -0
  89. {comicforge-0.1.0 → comicforge-0.2.1}/examples/README.md.old +0 -0
  90. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/bara/character.yaml +0 -0
  91. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/bara/face-happy.svg +0 -0
  92. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/bara/face-neutral.svg +0 -0
  93. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/bara/poses/sit/base.svg +0 -0
  94. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/bara/poses/sit/pose.yaml +0 -0
  95. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/bara/poses/walk/base.svg +0 -0
  96. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/bara/poses/walk/pose.yaml +0 -0
  97. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/arms-crossed.svg +0 -0
  98. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/arms-down.svg +0 -0
  99. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/arms-hips.svg +0 -0
  100. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/arms-point.svg +0 -0
  101. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/arms-thumbsup.svg +0 -0
  102. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/arms-wave.svg +0 -0
  103. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/base.svg +0 -0
  104. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/character.yaml +0 -0
  105. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/face-angry.svg +0 -0
  106. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/face-happy.svg +0 -0
  107. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/face-laugh.svg +0 -0
  108. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/face-neutral.svg +0 -0
  109. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/face-sad.svg +0 -0
  110. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/face-surprised.svg +0 -0
  111. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/characters/tom/face-wink.svg +0 -0
  112. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/pages/dvur-scene.yaml +0 -0
  113. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/pages/kosticka.yaml +0 -0
  114. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/pages/slepice.yaml +0 -0
  115. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/pixel/bone.yaml +0 -0
  116. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/pixel/heart.yaml +0 -0
  117. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/pixel/star.yaml +0 -0
  118. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/pixel/sun.yaml +0 -0
  119. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/references.yaml +0 -0
  120. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/scenes/dvur/base.svg +0 -0
  121. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/scenes/dvur/scene.yaml +0 -0
  122. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/scenes/dvur/weather-clear.svg +0 -0
  123. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/scenes/dvur/weather-rain.svg +0 -0
  124. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/scenes/pokoj/base.svg +0 -0
  125. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/scenes/pokoj/scene.yaml +0 -0
  126. {comicforge-0.1.0 → comicforge-0.2.1}/examples/pes/theme.yaml +0 -0
  127. {comicforge-0.1.0 → comicforge-0.2.1}/tests/__init__.py +0 -0
  128. {comicforge-0.1.0 → comicforge-0.2.1}/tests/conftest.py +0 -0
  129. {comicforge-0.1.0 → comicforge-0.2.1}/tests/test_cli.py +0 -0
  130. {comicforge-0.1.0 → comicforge-0.2.1}/tests/test_inspire.py +0 -0
  131. {comicforge-0.1.0 → comicforge-0.2.1}/tests/test_library.py +0 -0
  132. {comicforge-0.1.0 → comicforge-0.2.1}/tests/test_pixelart.py +0 -0
  133. {comicforge-0.1.0 → comicforge-0.2.1}/tests/test_raster.py +0 -0
  134. {comicforge-0.1.0 → comicforge-0.2.1}/tests/test_scaffold.py +0 -0
  135. {comicforge-0.1.0 → comicforge-0.2.1}/tests/test_scene.py +0 -0
  136. {comicforge-0.1.0 → comicforge-0.2.1}/tests/test_validate.py +0 -0
@@ -17,7 +17,8 @@ description: >
17
17
  You write a **comic page as a YAML spec**; the engine renders it to SVG/PNG/PDF.
18
18
  This file is the authoring contract. For deeper explanations (path resolution,
19
19
  how to add characters/scenes, pixel-art format, Python API, full CLI reference)
20
- see [`reference.md`](reference.md) in this skill directory.
20
+ see [`reference.md`](reference.md) in this skill directory, or the documentation
21
+ site at <https://mojzis.github.io/comicforge/>.
21
22
 
22
23
  ## Loop
23
24
 
@@ -75,20 +76,31 @@ Every position inside a panel is a **fraction 0..1 of that panel**:
75
76
 
76
77
  ```yaml
77
78
  title: "Optional page title" # bold caption strip at the top
79
+ title_style: {font_size: 26, color: "#21304a"} # optional
78
80
  type: page # page (default) | scene — see below
79
81
  page: A4 # A4 | A5 | letter | [w_mm, h_mm]
82
+ bg: "#ffffff" # paper colour
80
83
  px_per_mm: 4 # raster scale (vector PDF ignores it)
81
84
  margin_mm: 14
82
85
  gutter_mm: 6
86
+ frame: # panel outline, page-wide; a panel's own
87
+ width: 3.5 # `frame:` overrides. width 0 = no line
88
+ color: "#21304a"
89
+ radius: 10 # corner radius (also clips the art)
83
90
  library: "../characters" # path to character dir
84
91
  scenes_dir: "../scenes" # path to scenes dir (omit if no scenes used)
85
92
  pixel_dir: "../pixel" # path to pixel-art dir (omit if inline only)
86
93
 
87
94
  rows: # page is a stack of rows…
88
- - height: 1.0 # relative row height (default 1)
95
+ - height: 1.0 # relative row height (default 1), or
96
+ # height_mm: 60 for a fixed height —
97
+ # weighted rows share what is left
89
98
  panels: # …each row is a left→right list of panels
90
99
  - width: 1.0 # relative panel width (default 1)
91
100
  bg: "#fbfaf6" # optional flat panel background
101
+ frame: {width: 0} # optional per-panel outline override
102
+ caption: "Rain came." # narration band under the art, inside the
103
+ # frame; or {text:, max_chars:}
92
104
  scene: dvur # optional scene background (see below)
93
105
  image: "art/01.png" # optional raster background (see below)
94
106
  actors: [ ... ] # characters (drawn back→front in list order)
@@ -200,6 +212,9 @@ bubbles: [ ... ]
200
212
  kind: speech # speech | thought | shout
201
213
  speaker: tom # OPTIONAL: auto-place above this actor + aim the tail at
202
214
  # their head. Prefer this over manual x/y/to.
215
+ at: tr # OPTIONAL corner/edge to hug: tl t tr l c r bl b br.
216
+ # Each column (l/c/r) stacks its own top and
217
+ # bottom, so tl + tr sit side by side.
203
218
  x: 0.5 # OPTIONAL bubble centre (panel fraction); else from speaker
204
219
  y: 0.18 # OPTIONAL; omit and bubbles stack downward by measured
205
220
  # height, so they never overlap. All bubbles are
@@ -218,9 +233,23 @@ page or scene spec to style every bubble at once; per-bubble keys override:
218
233
  bubble_style:
219
234
  uppercase: true # render all bubble text in CAPS (classic comic lettering)
220
235
  font_size: 16 # default font size px for all bubbles
236
+ pad: 14 # text inset from the outline
237
+ radius: 18 # speech-bubble corner radius
238
+ stroke: "#21304a" # outline colour; stroke_width: 3; fill: "#ffffff"
239
+ ink: "#21304a" # text colour; font: "DejaVu Sans, sans-serif"
240
+ em: 1.0 # width scale of the text measure — 0.8 for a narrow
241
+ # handwriting font, so bubbles hug the words
221
242
  rows: [ ... ]
222
243
  ```
223
244
 
245
+ **Captions** — narration, as opposed to a character's bubble — go in a band
246
+ along the bottom of the panel, inside the frame, separated from the art by a
247
+ hairline in the frame colour. The art box shrinks to make room, so bubble and
248
+ actor coordinates stay relative to the picture. Page-wide look via
249
+ `caption_style` (`font`, `font_size`, `ink`, `bg`, `pad`, `max_chars`,
250
+ `align: left|center`, `rule`, `uppercase`); `comicforge.caption.height(text, style)` tells
251
+ you how tall a band will be, for sizing rows.
252
+
224
253
  `thought` draws an ellipse with a trail of dots; `shout` draws a spiky burst.
225
254
  The tail is a slim line dropping from the bubble underside; its tip is capped
226
255
  short so it never overlaps the figure.
@@ -4,6 +4,9 @@ This guide covers everything you need to author comics with ComicForge — for b
4
4
  humans and LLMs. The [SKILL.md](SKILL.md) file is the quick-reference contract;
5
5
  this document goes deeper on structure, paths, and how all the pieces fit together.
6
6
 
7
+ The same material, with rendered examples beside every spec, is published at
8
+ <https://mojzis.github.io/comicforge/>.
9
+
7
10
  ---
8
11
 
9
12
  ## Engine + self-contained projects
@@ -592,12 +595,22 @@ Do **not** auto-vectorize the result — hand/LLM-author the SVG using it as a g
592
595
 
593
596
  For the full spec grammar, see [SKILL.md](SKILL.md). Key points:
594
597
 
595
- - **Page-level keys**: `title`, `page` (A4/A5/letter/[w,h]), `px_per_mm`,
596
- `margin_mm`, `gutter_mm`, `library`, `scenes_dir`, `pixel_dir`,
597
- `bubble_style` (page-wide bubble defaults: `uppercase`, `font_size`)
598
- - **Rows and panels**: `rows[].height` (relative weight), `rows[].panels[].width`
599
- (relative weight), panel keys: `bg`, `scene`, `image`, `actors`, `pixel`,
600
- `bubbles` — `validate` flags any other panel key as a typo
598
+ - **Page-level keys**: `title`, `title_style` (`font_size`, `color`, `font`),
599
+ `page` (A4/A5/letter/[w,h]), `bg` (paper colour), `px_per_mm`, `margin_mm`,
600
+ `gutter_mm`, `library`, `scenes_dir`, `pixel_dir`,
601
+ `frame` (panel outline: `width` in px, 0 for none; `color`; `radius` — the
602
+ corner radius also clips the art),
603
+ `bubble_style` (page-wide bubble look: `font`, `font_size`, `pad`, `radius`,
604
+ `stroke`, `stroke_width`, `fill`, `ink`, `uppercase`, `em` — width scale of
605
+ the text measure for narrower fonts)
606
+ - **Rows and panels**: `rows[].height` (relative weight) or `rows[].height_mm`
607
+ (fixed; weighted rows share the rest, all-fixed leaves the bottom blank),
608
+ `rows[].panels[].width` (relative weight), panel keys: `bg`, `frame`
609
+ (per-panel override), `caption` (narration band under the art, inside the
610
+ frame: a string or `{text, max_chars}`; page-wide `caption_style` with
611
+ `font`, `font_size`, `ink`, `bg`, `pad`, `max_chars`, `align`, `rule`, `uppercase`),
612
+ `scene`, `image`, `actors`, `pixel`, `bubbles` — `validate` flags any other
613
+ panel key as a typo
601
614
  - **Image keys**: `image: path.png` or `image: {src:, fit:}` with
602
615
  `fit: cover` (default; scale-to-fill + centre-crop) or `contain`
603
616
  (fit inside + letterbox). The path resolves against the spec file's dir and
@@ -607,12 +620,16 @@ For the full spec grammar, see [SKILL.md](SKILL.md). Key points:
607
620
  - **Actor keys**: `char`, `pose` (optional; defaults to the character's default
608
621
  pose), per-slot variant keys (`face`, `arms`, etc.), `x`, `y`, `scale`, `flip`
609
622
  - **Bubble keys**: `text`, `kind` (speech/thought/shout), `speaker`, optional
610
- `x`/`y`/`to`/`max_chars`/`fs`/`uppercase` (`fs` and `uppercase` override
623
+ `at`/`x`/`y`/`to`/`max_chars`/`fs`/`uppercase` (`fs` and `uppercase` override
611
624
  the page-level `bubble_style`). Omit `y` and bubbles stack down by their
612
625
  measured height without overlapping; omit `x` and they centre (or sit above
613
- their `speaker`). Every bubble is kept inside the panel — so a generated spec
614
- over raster panels, which has no `speaker` to anchor to, can omit coordinates
615
- entirely
626
+ their `speaker`). `at` hugs a corner or edge instead — `tl`, `t`, `tr`, `l`,
627
+ `c`, `r`, `bl`, `b`, `br` — and each column keeps its own top and bottom
628
+ 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. Every bubble is
630
+ kept inside the panel — so a generated spec over raster panels, which has no
631
+ `speaker` to anchor to, can omit coordinates entirely, or place each with `at`
632
+ to keep it off the faces
616
633
  - **Pixel keys**: `art` (library name) or `grid`+`palette`, plus `x`/`y`/`scale`
617
634
 
618
635
  ---
@@ -0,0 +1,69 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "mkdocs.yml"
9
+ - "examples/**" # the gallery renders from the demo project
10
+ - "comicforge/**" # …through the engine, so a renderer change matters
11
+ - "pyproject.toml"
12
+ - ".github/workflows/docs.yml"
13
+ pull_request:
14
+ paths:
15
+ - "docs/**"
16
+ - "mkdocs.yml"
17
+ - "examples/**"
18
+ - "comicforge/**"
19
+ workflow_dispatch:
20
+
21
+ concurrency:
22
+ group: docs-${{ github.ref }}
23
+ cancel-in-progress: true
24
+
25
+ permissions: {}
26
+
27
+ jobs:
28
+ build:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v7
32
+
33
+ # Every image on the site is rendered at build time by
34
+ # docs/hooks/render_demos.py, so the docs build needs cairo just like a
35
+ # normal render does.
36
+ - name: Install cairo
37
+ run: sudo apt-get update && sudo apt-get install -y libcairo2
38
+
39
+ - name: Install uv
40
+ uses: astral-sh/setup-uv@v10.0.1
41
+ with:
42
+ enable-cache: true
43
+
44
+ - name: Sync docs dependencies
45
+ run: uv sync --group docs
46
+
47
+ # --strict fails the build on a broken cross-reference, a dead anchor, a
48
+ # missing snippet, or a demo spec that no longer renders.
49
+ - name: Build site
50
+ run: uv run mkdocs build --strict
51
+
52
+ - uses: actions/upload-pages-artifact@v4
53
+ with:
54
+ path: site/
55
+
56
+ deploy:
57
+ # Pull requests get the build as a check; only main publishes.
58
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
59
+ needs: build
60
+ runs-on: ubuntu-latest
61
+ environment:
62
+ name: github-pages
63
+ url: ${{ steps.deployment.outputs.page_url }}
64
+ permissions:
65
+ pages: write
66
+ id-token: write
67
+ steps:
68
+ - id: deployment
69
+ uses: actions/deploy-pages@v4
@@ -34,3 +34,12 @@ review.html
34
34
 
35
35
  # Source archive (redundant after flattening)
36
36
  *.zip
37
+
38
+ # Docs site: `mkdocs build` output, and the demo renders the build hook draws
39
+ # (every image on the site is regenerated from a spec — never checked in).
40
+ site/
41
+ docs/assets/renders/
42
+
43
+ # …except the one raster the `image:` demo needs as *input*, which `*.png` above
44
+ # would otherwise swallow.
45
+ !docs/demos/raster-art/*.png
@@ -21,6 +21,7 @@ comicforge/ The engine — pure code, no bundled art.
21
21
  examples/pes/ A self-contained demo project (characters, scenes, pixel, pages).
22
22
  Develop against the engine the same way a downstream project would.
23
23
  skills/comicforge/ The portable authoring skill (SKILL.md + reference.md).
24
+ docs/ The documentation site (MkDocs Material) — see below.
24
25
  tests/ pytest suite.
25
26
  ```
26
27
 
@@ -82,6 +83,29 @@ cmf scene examples/pes/pages/dvur-scene.yaml -o dvur.png
82
83
  cmf panel examples/pes/pages/slepice.yaml -o panels/ --all
83
84
  ```
84
85
 
86
+ ## Docs site
87
+
88
+ `docs/` is a MkDocs Material site published to GitHub Pages by
89
+ `.github/workflows/docs.yml`.
90
+
91
+ ```bash
92
+ uv sync --group docs
93
+ poe docs # serve with live reload
94
+ poe docs-build # mkdocs build --strict — what CI runs
95
+ ```
96
+
97
+ **Every image on the site is rendered at build time** by
98
+ `docs/hooks/render_demos.py`, from a spec in `docs/demos/` or from
99
+ `examples/pes/`. Nothing is a checked-in screenshot, so a renderer change that
100
+ breaks a demo breaks the docs build. To add an illustrated example: write
101
+ `docs/demos/<name>.yaml` (small custom `page:`, `library:` pointing at
102
+ `../../examples/pes/characters`), `cmf validate` it, then reference
103
+ `assets/renders/<name>.png` and include the spec with a `pymdownx.snippets`
104
+ include — see `docs/guide/bubbles.md` for the pattern.
105
+
106
+ `--strict` plus the `validation:` block in `mkdocs.yml` fails the build on a
107
+ broken cross-reference, a dead anchor, a missing snippet or a missing render.
108
+
85
109
  ## Conventions / invariants
86
110
 
87
111
  - **Content-free engine:** never hardcode an asset name, path, or default project
@@ -91,7 +115,8 @@ cmf panel examples/pes/pages/slepice.yaml -o panels/ --all
91
115
  - The CLI subcommands (`render`/`scene`/`panel`/`characters`/`scenes`) are the
92
116
  stable surface; `characters`/`scenes` emit JSON manifests other tools depend on.
93
117
  - When you change the spec grammar or CLI, update the authoring skill in
94
- `skills/comicforge/` (both `SKILL.md` and `reference.md`) in the same change.
118
+ `skills/comicforge/` (both `SKILL.md` and `reference.md`) **and** the affected
119
+ pages under `docs/` in the same change.
95
120
  - Demo art is hand/LLM-authored crisp SVG, edited directly in `examples/pes/`
96
121
  (there is no procedural generator). Use `comicforge inspire` for *reference*
97
122
  images only — never auto-vectorize them; it breaks overlay/anchor registration.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: comicforge
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: A tiny, scriptable comic-page engine — author comics as YAML, render to SVG / PNG / PDF
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: cairosvg>=2.7
@@ -13,12 +13,18 @@ Description-Content-Type: text/markdown
13
13
 
14
14
  # ComicForge
15
15
 
16
+ **📖 Documentation: <https://mojzis.github.io/comicforge/>**
17
+
16
18
  A tiny, scriptable comic-page engine. Characters are a **base SVG + stackable
17
19
  variant overlays** (faces, arms, …); a comic page is a **declarative YAML spec**;
18
20
  output is **SVG / PNG / PDF**. Built so an LLM (or you) can author pages as plain
19
- text — see the authoring skill [`skills/comicforge/SKILL.md`](skills/comicforge/SKILL.md)
20
- for the full authoring contract and [`skills/comicforge/reference.md`](skills/comicforge/reference.md)
21
- for a deeper reference. Working **on the engine** instead? See [`CLAUDE.md`](CLAUDE.md).
21
+ text.
22
+
23
+ - [Your first comic](https://mojzis.github.io/comicforge/quickstart/) — a project, a character, a strip
24
+ - [Spec reference](https://mojzis.github.io/comicforge/reference/spec/) · [CLI reference](https://mojzis.github.io/comicforge/reference/cli/) · [Gallery](https://mojzis.github.io/comicforge/gallery/)
25
+ - [`skills/comicforge/SKILL.md`](skills/comicforge/SKILL.md) — the authoring contract an LLM reads
26
+ - Working **on the engine**? See [`CLAUDE.md`](CLAUDE.md) and
27
+ [Contributing](https://mojzis.github.io/comicforge/contributing/).
22
28
 
23
29
  ## Start a project
24
30
 
@@ -32,13 +38,17 @@ cmf init my-comic # scaffold characters/ scenes/ pixel/ pages/
32
38
  cd my-comic && cmf render pages/hello.yaml
33
39
  ```
34
40
 
35
- See [`docs/starting-a-project.md`](docs/starting-a-project.md) for the data-only
36
- setup, version pinning, and when to make it a real Python project.
41
+ See [Starting a project](https://mojzis.github.io/comicforge/starting-a-project/)
42
+ for the data-only setup, version pinning, and when to make it a real Python
43
+ project.
37
44
 
38
45
  ## Working on the engine
39
46
 
40
47
  ```bash
41
48
  uv sync # create .venv and install deps
49
+ uv run poe test # the tight loop
50
+ uv run poe check # everything, before opening a PR
51
+ uv run poe docs # serve the documentation site locally
42
52
  ```
43
53
 
44
54
  ## Use
@@ -114,8 +124,10 @@ pixel_dir: "../pixel" # pixel-art sprites
114
124
  CLI flags (`--library`, `--scenes`, `--pixel-dir`) override spec keys and are
115
125
  treated as relative to the current working directory.
116
126
 
117
- See [`skills/comicforge/reference.md`](skills/comicforge/reference.md) for the full path
118
- resolution rule, how to add characters/scenes/sprites, and the complete CLI reference.
127
+ The [documentation site](https://mojzis.github.io/comicforge/) covers the full
128
+ path-resolution rule, how to add characters / scenes / sprites, and the complete
129
+ CLI and spec references — every picture on it is rendered from a spec at build
130
+ time.
119
131
 
120
132
  ## Design choices
121
133
 
@@ -1,11 +1,17 @@
1
1
  # ComicForge
2
2
 
3
+ **📖 Documentation: <https://mojzis.github.io/comicforge/>**
4
+
3
5
  A tiny, scriptable comic-page engine. Characters are a **base SVG + stackable
4
6
  variant overlays** (faces, arms, …); a comic page is a **declarative YAML spec**;
5
7
  output is **SVG / PNG / PDF**. Built so an LLM (or you) can author pages as plain
6
- text — see the authoring skill [`skills/comicforge/SKILL.md`](skills/comicforge/SKILL.md)
7
- for the full authoring contract and [`skills/comicforge/reference.md`](skills/comicforge/reference.md)
8
- for a deeper reference. Working **on the engine** instead? See [`CLAUDE.md`](CLAUDE.md).
8
+ text.
9
+
10
+ - [Your first comic](https://mojzis.github.io/comicforge/quickstart/) — a project, a character, a strip
11
+ - [Spec reference](https://mojzis.github.io/comicforge/reference/spec/) · [CLI reference](https://mojzis.github.io/comicforge/reference/cli/) · [Gallery](https://mojzis.github.io/comicforge/gallery/)
12
+ - [`skills/comicforge/SKILL.md`](skills/comicforge/SKILL.md) — the authoring contract an LLM reads
13
+ - Working **on the engine**? See [`CLAUDE.md`](CLAUDE.md) and
14
+ [Contributing](https://mojzis.github.io/comicforge/contributing/).
9
15
 
10
16
  ## Start a project
11
17
 
@@ -19,13 +25,17 @@ cmf init my-comic # scaffold characters/ scenes/ pixel/ pages/
19
25
  cd my-comic && cmf render pages/hello.yaml
20
26
  ```
21
27
 
22
- See [`docs/starting-a-project.md`](docs/starting-a-project.md) for the data-only
23
- setup, version pinning, and when to make it a real Python project.
28
+ See [Starting a project](https://mojzis.github.io/comicforge/starting-a-project/)
29
+ for the data-only setup, version pinning, and when to make it a real Python
30
+ project.
24
31
 
25
32
  ## Working on the engine
26
33
 
27
34
  ```bash
28
35
  uv sync # create .venv and install deps
36
+ uv run poe test # the tight loop
37
+ uv run poe check # everything, before opening a PR
38
+ uv run poe docs # serve the documentation site locally
29
39
  ```
30
40
 
31
41
  ## Use
@@ -101,8 +111,10 @@ pixel_dir: "../pixel" # pixel-art sprites
101
111
  CLI flags (`--library`, `--scenes`, `--pixel-dir`) override spec keys and are
102
112
  treated as relative to the current working directory.
103
113
 
104
- See [`skills/comicforge/reference.md`](skills/comicforge/reference.md) for the full path
105
- resolution rule, how to add characters/scenes/sprites, and the complete CLI reference.
114
+ The [documentation site](https://mojzis.github.io/comicforge/) covers the full
115
+ path-resolution rule, how to add characters / scenes / sprites, and the complete
116
+ CLI and spec references — every picture on it is rendered from a spec at build
117
+ time.
106
118
 
107
119
  ## Design choices
108
120
 
@@ -7,4 +7,4 @@ Designed so an LLM (or you) can author pages as plain declarative text.
7
7
  from .pixelart import PixelLibrary # noqa: F401
8
8
  from .render import load_spec, render_scene, render_spec # noqa: F401
9
9
 
10
- __version__ = "0.1.0"
10
+ __version__ = "0.2.0"
@@ -0,0 +1,232 @@
1
+ """Speech bubbles: speech, thought, and shout, with naive word-wrap + tails.
2
+
3
+ All coordinates here are absolute page px. A bubble is positioned by its centre
4
+ (bx, by); the tail points toward `tail` (also page px).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ from xml.sax.saxutils import escape
11
+
12
+ FONT = "DejaVu Sans, Helvetica, Arial, sans-serif"
13
+ INK = "#21304a"
14
+
15
+ # Every knob a bubble's look has. A page's `bubble_style:` overrides any of
16
+ # these for the whole page; a bubble's own keys override again.
17
+ DEFAULT_STYLE = {
18
+ "font": FONT,
19
+ "font_size": 16,
20
+ "pad": 14, # text inset from the outline
21
+ "radius": 18, # corner radius of a speech bubble (capped at half height)
22
+ "stroke": INK, # outline colour
23
+ "stroke_width": 3,
24
+ "fill": "#ffffff",
25
+ "ink": INK, # text colour
26
+ "uppercase": False,
27
+ "em": 1.0, # width scale for the text measure: <1 for a narrower font
28
+ }
29
+
30
+
31
+ def resolve_style(*layers) -> dict:
32
+ """Merge style dicts over ``DEFAULT_STYLE``; later layers win, ``None`` skipped."""
33
+ out = dict(DEFAULT_STYLE)
34
+ for layer in layers:
35
+ if layer:
36
+ out.update({k: v for k, v in layer.items() if v is not None})
37
+ return out
38
+
39
+
40
+ def _wrap(text: str, max_chars: int) -> list[str]:
41
+ lines, cur = [], ""
42
+ for word in text.split():
43
+ if cur and len(cur) + 1 + len(word) > max_chars:
44
+ lines.append(cur)
45
+ cur = word
46
+ else:
47
+ cur = f"{cur} {word}".strip()
48
+ if cur:
49
+ lines.append(cur)
50
+ return lines or [""]
51
+
52
+
53
+ # Rough advance widths in em for a humanist sans (DejaVu Sans is the default
54
+ # font): capitals are a good third wider than lowercase, so an all-caps bubble
55
+ # must be measured as such or the text runs past its outline.
56
+ _EM = {"upper": 0.70, "lower": 0.56, "digit": 0.64, "space": 0.32, "other": 0.34}
57
+
58
+
59
+ def text_width(text: str, fs: float) -> float:
60
+ """Estimated rendered width of *text* at font size *fs*, in px."""
61
+
62
+ def em(ch):
63
+ if ch.isupper():
64
+ return _EM["upper"]
65
+ if ch.islower():
66
+ return _EM["lower"]
67
+ if ch.isdigit():
68
+ return _EM["digit"]
69
+ if ch.isspace():
70
+ return _EM["space"]
71
+ return _EM["other"]
72
+
73
+ return sum(em(ch) for ch in text) * fs
74
+
75
+
76
+ def _box(text, max_chars, fs, pad, em=1.0):
77
+ """Wrap *text* and return (lines, line_height, body_width, body_height)."""
78
+ lines = _wrap(text, max_chars)
79
+ lh = fs * 1.25
80
+ longest = max((text_width(ln, fs) * em for ln in lines), default=fs)
81
+ w = max(longest + 2 * pad, 60)
82
+ h = len(lines) * lh + 2 * pad
83
+ return lines, lh, w, h
84
+
85
+
86
+ # how far each bubble kind's outline reaches beyond the text body box
87
+ _OUTSET = {"thought": (12, 16), "shout": (16, 16)}
88
+
89
+
90
+ def bubble_size(text, kind="speech", max_chars=22, fs=None, pad=None, style=None):
91
+ """Outer (width, height) a `bubble` call will occupy, tail excluded.
92
+
93
+ `thought` and `shout` draw outside the text body, so callers that stack
94
+ bubbles or keep them inside a panel need this, not just the body box.
95
+ """
96
+ st = resolve_style(style)
97
+ fs = st["font_size"] if fs is None else fs
98
+ pad = st["pad"] if pad is None else pad
99
+ _lines, _lh, w, h = _box(text, max_chars, fs, pad, st["em"])
100
+ ow, oh = _OUTSET.get(kind, (0, 0))
101
+ return w + ow, h + oh
102
+
103
+
104
+ def _text_block(lines, cx, top, fs, lh, st):
105
+ spans = []
106
+ for i, ln in enumerate(lines):
107
+ spans.append(
108
+ f'<tspan x="{cx:.1f}" y="{top + fs + i * lh:.1f}">{escape(ln)}</tspan>'
109
+ )
110
+ return (
111
+ f'<text text-anchor="middle" font-family="{st["font"]}" '
112
+ f'font-size="{fs}" fill="{st["ink"]}">{"".join(spans)}</text>'
113
+ )
114
+
115
+
116
+ def _paint(st, scale=1.0):
117
+ """fill/stroke attributes shared by every outline a bubble draws."""
118
+ return (
119
+ f'fill="{st["fill"]}" stroke="{st["stroke"]}" '
120
+ f'stroke-width="{st["stroke_width"] * scale:.2f}"'
121
+ )
122
+
123
+
124
+ def bubble(
125
+ text, bx, by, tail=None, kind="speech", max_chars=22, fs=None, pad=None, style=None
126
+ ):
127
+ st = resolve_style(style)
128
+ fs = st["font_size"] if fs is None else fs
129
+ pad = st["pad"] if pad is None else pad
130
+ lines, lh, w, h = _box(text, max_chars, fs, pad, st["em"])
131
+ x, y = bx - w / 2, by - h / 2
132
+ txt = _text_block(lines, bx, y + pad, fs, lh, st)
133
+
134
+ if kind == "shout":
135
+ body = _burst(x, y, w, h, st)
136
+ elif kind == "thought":
137
+ rx, ry = w / 2 + 6, h / 2 + 8
138
+ body = (
139
+ f'<ellipse cx="{bx:.1f}" cy="{by:.1f}" rx="{rx:.1f}" ry="{ry:.1f}" '
140
+ f"{_paint(st)}/>"
141
+ )
142
+ else:
143
+ body = (
144
+ f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" '
145
+ f'rx="{min(st["radius"], h / 2):.1f}" {_paint(st)}/>'
146
+ )
147
+
148
+ tail_svg = ""
149
+ if tail is not None:
150
+ tail_svg = _tail(bx, by, w, h, tail, kind, st)
151
+
152
+ return f"<g>{body}{tail_svg}{txt}</g>"
153
+
154
+
155
+ def _tail(bx, by, w, h, tail, kind, st):
156
+ """A slim tail from the bubble's underside (or its top, when the speaker is
157
+ above it) pointing toward the target — but stopping well short of it, so
158
+ the tip never reaches the figure."""
159
+ tx, ty = tail
160
+ # exit from the edge facing the target: a side edge when the target lies
161
+ # further out beside the bubble than above or below it (relative to the
162
+ # body's own size), else top/bottom — nudged toward the target but kept
163
+ # within the middle of that edge
164
+ over_x = (abs(tx - bx) - w / 2) / (w / 2)
165
+ over_y = (abs(ty - by) - h / 2) / (h / 2)
166
+ if over_x > 0 and over_x > over_y:
167
+ ex = bx - w / 2 if tx < bx else bx + w / 2
168
+ ey = min(max(ty, by - h * 0.3), by + h * 0.3)
169
+ else:
170
+ ex = min(max(tx, bx - w * 0.3), bx + w * 0.3)
171
+ ey = by - h / 2 if ty < by - h / 2 else by + h / 2
172
+ dx, dy = tx - ex, ty - ey
173
+ dist = math.hypot(dx, dy) or 1.0
174
+ reach = min(dist * 0.45, 46) # capped length keeps the tip off the figure
175
+ ux, uy = dx / dist, dy / dist
176
+ tipx, tipy = ex + ux * reach, ey + uy * reach
177
+
178
+ if kind == "thought":
179
+ dots = ""
180
+ for f in (0.45, 0.74, 1.0):
181
+ r = 6 * (1 - f) + 2.5
182
+ dots += (
183
+ f'<circle cx="{ex + ux * reach * f:.1f}" '
184
+ f'cy="{ey + uy * reach * f:.1f}" r="{r:.1f}" {_paint(st, 0.85)}/>'
185
+ )
186
+ return dots
187
+ # narrow tapered tail for speech/shout
188
+ perp = math.atan2(uy, ux) + math.pi / 2
189
+ base = 6
190
+ ax = ex + math.cos(perp) * base
191
+ ay = ey + math.sin(perp) * base
192
+ bx2 = ex - math.cos(perp) * base
193
+ by2 = ey - math.sin(perp) * base
194
+ return (
195
+ f'<path d="M{ax:.1f} {ay:.1f} L{tipx:.1f} {tipy:.1f} '
196
+ f'L{bx2:.1f} {by2:.1f} Z" {_paint(st, 0.85)} stroke-linejoin="round"/>'
197
+ )
198
+
199
+
200
+ def _cloud(x, y, w, h):
201
+ # rounded body + scalloped top edge via overlapping circles
202
+ rx = min(20, h / 2)
203
+ body = (
204
+ f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" '
205
+ f'rx="{rx:.1f}" fill="#ffffff" stroke="{INK}" stroke-width="3"/>'
206
+ )
207
+ bumps = ""
208
+ n = max(3, int(w // 34))
209
+ for i in range(n):
210
+ cx = x + (i + 0.5) * w / n
211
+ bumps += (
212
+ f'<circle cx="{cx:.1f}" cy="{y:.1f}" r="13" '
213
+ f'fill="#ffffff" stroke="{INK}" stroke-width="3"/>'
214
+ )
215
+ # mask the inner stroke segments by redrawing body fill on top edge
216
+ cover = (
217
+ f'<rect x="{x + 3:.1f}" y="{y:.1f}" width="{w - 6:.1f}" height="14" '
218
+ f'fill="#ffffff" stroke="none"/>'
219
+ )
220
+ return body + bumps + cover + body.replace('fill="#ffffff"', 'fill="none"')
221
+
222
+
223
+ def _burst(x, y, w, h, st):
224
+ cx, cy = x + w / 2, y + h / 2
225
+ rx, ry = w / 2 + 8, h / 2 + 8
226
+ n = 18
227
+ pts = []
228
+ for i in range(n * 2):
229
+ a = math.pi * i / n
230
+ rr = 1.0 if i % 2 == 0 else 0.78
231
+ pts.append(f"{cx + math.cos(a) * rx * rr:.1f},{cy + math.sin(a) * ry * rr:.1f}")
232
+ return f'<polygon points="{" ".join(pts)}" {_paint(st)} stroke-linejoin="round"/>'