comicforge 0.0.1__py3-none-any.whl
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/__init__.py +10 -0
- comicforge/__main__.py +5 -0
- comicforge/_skill/SKILL.md +325 -0
- comicforge/_skill/reference.md +644 -0
- comicforge/bubbles.py +133 -0
- comicforge/cli.py +396 -0
- comicforge/inspire.py +246 -0
- comicforge/library.py +235 -0
- comicforge/pixelart.py +80 -0
- comicforge/render.py +560 -0
- comicforge/scaffold.py +144 -0
- comicforge/scene.py +112 -0
- comicforge/validate.py +192 -0
- comicforge-0.0.1.dist-info/METADATA +136 -0
- comicforge-0.0.1.dist-info/RECORD +17 -0
- comicforge-0.0.1.dist-info/WHEEL +4 -0
- comicforge-0.0.1.dist-info/entry_points.txt +3 -0
comicforge/scene.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Load the scene library and compose a scene background.
|
|
2
|
+
|
|
3
|
+
A scene is a directory mirroring a character:
|
|
4
|
+
base.svg, <slot>-<variant>.svg, scene.yaml
|
|
5
|
+
|
|
6
|
+
It uses the same base + stackable-overlay model as characters (see
|
|
7
|
+
:mod:`comicforge.library`), but a scene is always a single body (no poses) and
|
|
8
|
+
instead of being placed at a height it is scaled to *cover* a panel (or the
|
|
9
|
+
whole canvas, for a standalone illustration).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
from .library import _inner
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Scene:
|
|
24
|
+
"""A background: a base body plus stackable overlay variants, one viewBox."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
label: str
|
|
28
|
+
w: float
|
|
29
|
+
h: float
|
|
30
|
+
slots: dict[str, list[str]]
|
|
31
|
+
defaults: dict[str, str]
|
|
32
|
+
base: str
|
|
33
|
+
variants: dict[str, str] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
def compose_inner(self, selection: dict[str, str]) -> str:
|
|
36
|
+
parts = [self.base]
|
|
37
|
+
for slot in self.slots: # manifest order = draw order
|
|
38
|
+
variant = selection.get(slot, self.defaults.get(slot))
|
|
39
|
+
if variant is None:
|
|
40
|
+
continue
|
|
41
|
+
if variant not in self.slots[slot]:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"{self.name}: slot '{slot}' has no variant '{variant}'. "
|
|
44
|
+
f"Available: {self.slots[slot]}"
|
|
45
|
+
)
|
|
46
|
+
parts.append(self.variants[f"{slot}-{variant}"])
|
|
47
|
+
return "\n".join(parts)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SceneLibrary:
|
|
51
|
+
"""Loads scenes from ``<root>/<name>/`` lazily, caching parsed scenes."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, root: str | Path):
|
|
54
|
+
self.root = Path(root)
|
|
55
|
+
self._cache: dict[str, Scene] = {}
|
|
56
|
+
|
|
57
|
+
def get(self, name: str) -> Scene:
|
|
58
|
+
if name in self._cache:
|
|
59
|
+
return self._cache[name]
|
|
60
|
+
sdir = self.root / name
|
|
61
|
+
if not sdir.is_dir():
|
|
62
|
+
avail = (
|
|
63
|
+
[p.name for p in self.root.iterdir() if p.is_dir()]
|
|
64
|
+
if self.root.is_dir()
|
|
65
|
+
else []
|
|
66
|
+
)
|
|
67
|
+
raise KeyError(f"scene '{name}' not found in {self.root}. Have: {avail}")
|
|
68
|
+
man = yaml.safe_load((sdir / "scene.yaml").read_text(encoding="utf-8"))
|
|
69
|
+
w, h = man["viewbox"]
|
|
70
|
+
slots = man.get("slots", {})
|
|
71
|
+
variants = {}
|
|
72
|
+
for slot, names in slots.items():
|
|
73
|
+
for v in names:
|
|
74
|
+
variants[f"{slot}-{v}"] = _inner(sdir / f"{slot}-{v}.svg")
|
|
75
|
+
scene = Scene(
|
|
76
|
+
name=man["name"],
|
|
77
|
+
label=man.get("label", man["name"]),
|
|
78
|
+
w=w,
|
|
79
|
+
h=h,
|
|
80
|
+
slots=slots,
|
|
81
|
+
defaults=man.get("default", {}),
|
|
82
|
+
base=_inner(sdir / "base.svg"),
|
|
83
|
+
variants=variants,
|
|
84
|
+
)
|
|
85
|
+
self._cache[name] = scene
|
|
86
|
+
return scene
|
|
87
|
+
|
|
88
|
+
def manifest(self) -> dict:
|
|
89
|
+
"""Machine-readable contract: every scene, its slots and variants."""
|
|
90
|
+
out = {}
|
|
91
|
+
if not self.root.is_dir():
|
|
92
|
+
return out
|
|
93
|
+
for p in sorted(self.root.iterdir()):
|
|
94
|
+
if p.is_dir() and (p / "scene.yaml").exists():
|
|
95
|
+
c = self.get(p.name)
|
|
96
|
+
out[c.name] = {
|
|
97
|
+
"label": c.label,
|
|
98
|
+
"slots": c.slots,
|
|
99
|
+
"default": c.defaults,
|
|
100
|
+
}
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cover(scene: Scene, selection, px, py, pw, ph) -> str:
|
|
105
|
+
"""Place a scene so it fully covers the (px, py, pw, ph) box, centred."""
|
|
106
|
+
s = max(pw / scene.w, ph / scene.h)
|
|
107
|
+
ox = px + (pw - scene.w * s) / 2
|
|
108
|
+
oy = py + (ph - scene.h * s) / 2
|
|
109
|
+
return (
|
|
110
|
+
f'<g transform="translate({ox:.2f},{oy:.2f}) scale({s:.4f})">\n'
|
|
111
|
+
f"{scene.compose_inner(selection)}\n</g>"
|
|
112
|
+
)
|
comicforge/validate.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Statically check a page / scene spec against the libraries it points at.
|
|
2
|
+
|
|
3
|
+
``render`` only fails on the *first* genuinely unusable thing (a missing
|
|
4
|
+
character, a bad variant) and **silently ignores** keys it doesn't recognise —
|
|
5
|
+
so a typo like ``fcae: happy`` or ``post: walk`` renders a default-faced,
|
|
6
|
+
default-posed actor with no error at all. ``validate`` walks the whole spec
|
|
7
|
+
without drawing anything, collecting *every* problem at once:
|
|
8
|
+
|
|
9
|
+
- characters / scenes / pixel sprites that don't exist in the library
|
|
10
|
+
- poses an actor asks for that the character doesn't have
|
|
11
|
+
- slot variants that don't exist for the chosen character+pose / scene
|
|
12
|
+
- actor / scene keys that aren't reserved and aren't a real slot (likely typos)
|
|
13
|
+
- bubble ``speaker`` that names no actor in the panel, unknown bubble ``kind``
|
|
14
|
+
- structural holes (no ``rows``, a row without ``panels``, a bubble with no text)
|
|
15
|
+
|
|
16
|
+
It returns a list of human-readable problem strings (empty == the spec is sound).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from .library import Library
|
|
24
|
+
from .pixelart import PixelLibrary
|
|
25
|
+
from .render import _as_list, _build_libs, load_spec, spec_type
|
|
26
|
+
from .scene import SceneLibrary
|
|
27
|
+
|
|
28
|
+
# keys on an actor / scene dict that are positioning or identity, not slots
|
|
29
|
+
_ACTOR_RESERVED = {"char", "pose", "x", "y", "scale", "flip"}
|
|
30
|
+
_SCENE_RESERVED = {"name"}
|
|
31
|
+
_BUBBLE_KINDS = {"speech", "thought", "shout"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _check_actor(actor: dict, lib: Library, where: str, problems: list[str]) -> None:
|
|
35
|
+
name = actor.get("char")
|
|
36
|
+
if not name:
|
|
37
|
+
problems.append(f"{where}: actor with no 'char'")
|
|
38
|
+
return
|
|
39
|
+
try:
|
|
40
|
+
char = lib.get(name)
|
|
41
|
+
except KeyError as e:
|
|
42
|
+
problems.append(f"{where}: {e}")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
pose = actor.get("pose")
|
|
46
|
+
if pose is not None and pose not in char.poses:
|
|
47
|
+
problems.append(
|
|
48
|
+
f"{where}: {name} has no pose '{pose}'. Have: {list(char.poses)}"
|
|
49
|
+
)
|
|
50
|
+
pose = None # fall back so slot checks still run against a real pose
|
|
51
|
+
|
|
52
|
+
slots = char.slots_for(pose)
|
|
53
|
+
for key, value in actor.items():
|
|
54
|
+
if key in _ACTOR_RESERVED:
|
|
55
|
+
continue
|
|
56
|
+
if key not in slots:
|
|
57
|
+
problems.append(
|
|
58
|
+
f"{where}: {name} has no slot '{key}' "
|
|
59
|
+
f"(ignored when rendering). Slots: {sorted(slots)}"
|
|
60
|
+
)
|
|
61
|
+
elif value not in slots[key]:
|
|
62
|
+
problems.append(
|
|
63
|
+
f"{where}: {name} slot '{key}' has no variant '{value}'. "
|
|
64
|
+
f"Have: {slots[key]}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _check_scene(sc, scenes, where: str, problems: list[str]) -> None:
|
|
69
|
+
sc = {"name": sc} if isinstance(sc, str) else sc
|
|
70
|
+
name = sc.get("name")
|
|
71
|
+
if not name:
|
|
72
|
+
problems.append(f"{where}: scene with no 'name'")
|
|
73
|
+
return
|
|
74
|
+
try:
|
|
75
|
+
scene = scenes.get(name)
|
|
76
|
+
except KeyError as e:
|
|
77
|
+
problems.append(f"{where}: {e}")
|
|
78
|
+
return
|
|
79
|
+
for key, value in sc.items():
|
|
80
|
+
if key in _SCENE_RESERVED:
|
|
81
|
+
continue
|
|
82
|
+
if key not in scene.slots:
|
|
83
|
+
problems.append(
|
|
84
|
+
f"{where}: scene '{name}' has no slot '{key}' "
|
|
85
|
+
f"(ignored when rendering). Slots: {sorted(scene.slots)}"
|
|
86
|
+
)
|
|
87
|
+
elif value not in scene.slots[key]:
|
|
88
|
+
problems.append(
|
|
89
|
+
f"{where}: scene '{name}' slot '{key}' has no variant '{value}'. "
|
|
90
|
+
f"Have: {scene.slots[key]}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _check_pixel(spec, pxlib, where: str, problems: list[str]) -> None:
|
|
95
|
+
if "art" in spec:
|
|
96
|
+
if pxlib is None:
|
|
97
|
+
problems.append(
|
|
98
|
+
f"{where}: pixel art '{spec['art']}' needs a pixel library "
|
|
99
|
+
"(set 'pixel_dir:' in the spec or pass --pixel-dir)"
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
try:
|
|
103
|
+
pxlib.get(spec["art"])
|
|
104
|
+
except KeyError as e:
|
|
105
|
+
problems.append(f"{where}: {e}")
|
|
106
|
+
elif "grid" not in spec:
|
|
107
|
+
problems.append(f"{where}: pixel entry has neither 'art' nor 'grid'")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _check_bubbles(panel: dict, where: str, problems: list[str]) -> None:
|
|
111
|
+
speakers = {a.get("char") for a in panel.get("actors", [])}
|
|
112
|
+
for b in panel.get("bubbles", []):
|
|
113
|
+
if not b.get("text"):
|
|
114
|
+
problems.append(f"{where}: bubble with no 'text'")
|
|
115
|
+
kind = b.get("kind", "speech")
|
|
116
|
+
if kind not in _BUBBLE_KINDS:
|
|
117
|
+
problems.append(
|
|
118
|
+
f"{where}: bubble kind '{kind}' unknown. Have: {sorted(_BUBBLE_KINDS)}"
|
|
119
|
+
)
|
|
120
|
+
speaker = b.get("speaker")
|
|
121
|
+
if speaker is not None and speaker not in speakers:
|
|
122
|
+
problems.append(
|
|
123
|
+
f"{where}: bubble speaker '{speaker}' is not an actor here. "
|
|
124
|
+
f"Actors: {sorted(s for s in speakers if s)}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _check_panel(panel, libs, where, problems) -> None:
|
|
129
|
+
lib, scenes, pxlib = libs
|
|
130
|
+
if panel.get("scene") is not None:
|
|
131
|
+
_check_scene(panel["scene"], scenes, where, problems)
|
|
132
|
+
for spec in _as_list(panel.get("pixel")):
|
|
133
|
+
_check_pixel(spec, pxlib, where, problems)
|
|
134
|
+
for actor in panel.get("actors", []):
|
|
135
|
+
_check_actor(actor, lib, where, problems)
|
|
136
|
+
_check_bubbles(panel, where, problems)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def validate_spec(
|
|
140
|
+
spec,
|
|
141
|
+
library: Library | None = None,
|
|
142
|
+
scenes: SceneLibrary | None = None,
|
|
143
|
+
pixel_library: PixelLibrary | None = None,
|
|
144
|
+
) -> list[str]:
|
|
145
|
+
"""Return every problem found in *spec* (path or dict). Empty == sound.
|
|
146
|
+
|
|
147
|
+
A page spec has ``rows`` of ``panels``; a standalone scene spec has a
|
|
148
|
+
top-level ``scene`` and acts as a single panel. Both are validated.
|
|
149
|
+
"""
|
|
150
|
+
spec_dir = None
|
|
151
|
+
if not isinstance(spec, dict):
|
|
152
|
+
spec_path = Path(spec)
|
|
153
|
+
spec_dir = spec_path.parent.resolve()
|
|
154
|
+
spec = load_spec(spec_path)
|
|
155
|
+
|
|
156
|
+
problems: list[str] = []
|
|
157
|
+
try:
|
|
158
|
+
libs = _build_libs(spec, spec_dir, library, scenes, pixel_library)
|
|
159
|
+
except ValueError as e:
|
|
160
|
+
# a missing library dir is itself the headline problem; nothing else to check
|
|
161
|
+
return [str(e)]
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
kind = spec_type(spec)
|
|
165
|
+
except ValueError as e:
|
|
166
|
+
return [str(e)]
|
|
167
|
+
|
|
168
|
+
if kind == "scene": # standalone illustration, rendered with `scene`
|
|
169
|
+
if "scene" not in spec:
|
|
170
|
+
problems.append("scene spec has no top-level 'scene'")
|
|
171
|
+
if "rows" in spec:
|
|
172
|
+
problems.append(
|
|
173
|
+
"scene spec must not have 'rows' (use a 'page' spec for a grid)"
|
|
174
|
+
)
|
|
175
|
+
_check_panel(spec, libs, "scene", problems)
|
|
176
|
+
return problems
|
|
177
|
+
|
|
178
|
+
rows = spec.get("rows")
|
|
179
|
+
if not rows:
|
|
180
|
+
problems.append(
|
|
181
|
+
"page spec has no 'rows'"
|
|
182
|
+
+ (" (did you mean type: scene?)" if "scene" in spec else "")
|
|
183
|
+
)
|
|
184
|
+
return problems
|
|
185
|
+
for ri, row in enumerate(rows):
|
|
186
|
+
panels = row.get("panels")
|
|
187
|
+
if not panels:
|
|
188
|
+
problems.append(f"r{ri}: row has no 'panels'")
|
|
189
|
+
continue
|
|
190
|
+
for ci, panel in enumerate(panels):
|
|
191
|
+
_check_panel(panel, libs, f"r{ri}c{ci}", problems)
|
|
192
|
+
return problems
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: comicforge
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A tiny, scriptable comic-page engine — author comics as YAML, render to SVG / PNG / PDF
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: cairosvg>=2.7
|
|
7
|
+
Requires-Dist: pyyaml>=6.0
|
|
8
|
+
Requires-Dist: rich>=13.0
|
|
9
|
+
Provides-Extra: inspire
|
|
10
|
+
Requires-Dist: python-dotenv>=1.0; extra == 'inspire'
|
|
11
|
+
Requires-Dist: replicate>=1.0; extra == 'inspire'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# ComicForge
|
|
15
|
+
|
|
16
|
+
A tiny, scriptable comic-page engine. Characters are a **base SVG + stackable
|
|
17
|
+
variant overlays** (faces, arms, …); a comic page is a **declarative YAML spec**;
|
|
18
|
+
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).
|
|
22
|
+
|
|
23
|
+
## Start a project
|
|
24
|
+
|
|
25
|
+
ComicForge ships no art and a project needs no Python — a project is just YAML
|
|
26
|
+
specs + SVG / pixel art that the CLI points at. Install the engine globally and
|
|
27
|
+
scaffold one:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install comicforge # puts `cmf` on your PATH (no venv needed)
|
|
31
|
+
cmf init my-comic # scaffold characters/ scenes/ pixel/ pages/ + skill
|
|
32
|
+
cd my-comic && cmf render pages/hello.yaml
|
|
33
|
+
```
|
|
34
|
+
|
|
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.
|
|
37
|
+
|
|
38
|
+
## Working on the engine
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv sync # create .venv and install deps
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Use
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# render a comic page
|
|
48
|
+
cmf render examples/pes/pages/slepice.yaml -o slepice.png
|
|
49
|
+
cmf render examples/pes/pages/slepice.yaml -o slepice.pdf
|
|
50
|
+
|
|
51
|
+
# render a standalone scene illustration (one background, no comic grid)
|
|
52
|
+
cmf scene examples/pes/pages/dvur-scene.yaml -o dvur.png
|
|
53
|
+
|
|
54
|
+
# render individual panels at low res for quick review
|
|
55
|
+
cmf panel examples/pes/pages/slepice.yaml -o panel.png --row 0 --col 0
|
|
56
|
+
cmf panel examples/pes/pages/slepice.yaml -o panels/ --all --scale 0.5
|
|
57
|
+
|
|
58
|
+
# print the machine-readable contracts (characters / scenes, slots, variants)
|
|
59
|
+
cmf characters --library examples/pes/characters
|
|
60
|
+
cmf scenes --scenes examples/pes/scenes
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or from Python:
|
|
64
|
+
```python
|
|
65
|
+
from comicforge import render_spec
|
|
66
|
+
render_spec("examples/pes/pages/slepice.yaml", "out.pdf")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## How it fits together
|
|
70
|
+
|
|
71
|
+
ComicForge is a **content-free engine** plus **self-contained projects**. There is
|
|
72
|
+
no shared asset library: every project owns all of its own art. A real downstream
|
|
73
|
+
project just does `pip install comicforge` and brings its own characters — none of
|
|
74
|
+
the example art comes with the engine.
|
|
75
|
+
|
|
76
|
+
| Layer | Path | Purpose |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Engine | `comicforge/` | Pure code — loads assets, composes SVG, writes output. No hardcoded content. |
|
|
79
|
+
| Project | `examples/<name>/` | A self-contained comic: its own characters, scenes, pixel art, and page specs. |
|
|
80
|
+
|
|
81
|
+
A project is one directory with four asset folders:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
examples/pes/
|
|
85
|
+
characters/tom/ flat: base.svg + <slot>-<variant>.svg + character.yaml
|
|
86
|
+
characters/bara/ posed: shared faces + character.yaml + poses/{sit,walk}/
|
|
87
|
+
scenes/{dvur,pokoj}/ base.svg + <slot>-<variant>.svg + scene.yaml
|
|
88
|
+
pixel/{heart,sun,...}.yaml grid + palette
|
|
89
|
+
pages/
|
|
90
|
+
slepice.yaml 2×2 comic — Tom and Bára watch chickens
|
|
91
|
+
kosticka.yaml Strip with scenes, pixel art, multiple bubble types
|
|
92
|
+
dvur-scene.yaml Standalone illustration (use `comicforge scene`)
|
|
93
|
+
|
|
94
|
+
comicforge/library.py loads a character, stacks base+overlays, places it
|
|
95
|
+
comicforge/scene.py loads a scene, stacks overlays, scales it to cover a box
|
|
96
|
+
comicforge/bubbles.py speech / thought / shout bubbles with word-wrap + tail
|
|
97
|
+
comicforge/pixelart.py grid+palette → SVG sprite
|
|
98
|
+
comicforge/render.py row/panel pages + standalone scenes → SVG → PNG/PDF
|
|
99
|
+
comicforge/cli.py `render`, `scene`, `panel`, `characters`, `scenes` commands
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Asset directories
|
|
103
|
+
|
|
104
|
+
Each spec declares where its assets live via top-level keys (paths are relative to
|
|
105
|
+
the spec file's directory). For a spec under `pages/`, the sibling asset folders
|
|
106
|
+
are one level up:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
library: "../characters" # character art
|
|
110
|
+
scenes_dir: "../scenes" # scene backgrounds
|
|
111
|
+
pixel_dir: "../pixel" # pixel-art sprites
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
CLI flags (`--library`, `--scenes`, `--pixel-dir`) override spec keys and are
|
|
115
|
+
treated as relative to the current working directory.
|
|
116
|
+
|
|
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.
|
|
119
|
+
|
|
120
|
+
## Design choices
|
|
121
|
+
|
|
122
|
+
- **Base + overlays** in one shared local canvas — "posing" is just choosing which
|
|
123
|
+
overlay SVGs to stack. No rig math; each pose/expression is a small SVG you can
|
|
124
|
+
edit by hand.
|
|
125
|
+
- **Declarative spec** — the whole agent surface is YAML + the `characters` JSON.
|
|
126
|
+
- **SVG all the way down**, rasterized only at the end — crisp vector PDF for print.
|
|
127
|
+
- **Content-free engine** — `comicforge/` ships no art. Each project owns all its
|
|
128
|
+
own assets, so the engine is a dependency, not a content bundle.
|
|
129
|
+
|
|
130
|
+
## Roadmap ideas
|
|
131
|
+
|
|
132
|
+
- A FastAPI + HTMX preview server (live re-render on spec edit).
|
|
133
|
+
- An MCP server exposing `list_characters` / `render_page` as tools.
|
|
134
|
+
- Part-anchored slots (separate eyes/mouth/limb pivots) for finer posing.
|
|
135
|
+
- Free-form panel shapes and a gutter/layout DSL.
|
|
136
|
+
- AI-image panels (drop a generated raster into a panel like pixel art).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
comicforge/__init__.py,sha256=YP6JJ-C6JXrSgKRaYKkFW7pPSsKjl1CnsKXUhwoGN3Q,334
|
|
2
|
+
comicforge/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
|
|
3
|
+
comicforge/bubbles.py,sha256=BxjBvvbKG_F0ZvujwSvNPjDZIOcKQOKg8PAC4Cwi8Vk,4727
|
|
4
|
+
comicforge/cli.py,sha256=XTVDJZQb0b1yLQHcEJr7mX8md2q2tIcQmWGY9SQ6dWg,13125
|
|
5
|
+
comicforge/inspire.py,sha256=uVBhhNIFvfh9wdhFs4uqPDyu9Yh2dt3dQliM6LPa-lk,9089
|
|
6
|
+
comicforge/library.py,sha256=SdR7qtju_JArhInbbqrypP1i5mifR7QyRl1mnDo-mmo,9124
|
|
7
|
+
comicforge/pixelart.py,sha256=ZwvAffhvTZzI0Qm6YtE2dBliMrTtAd6jyJD_BqjRalg,2697
|
|
8
|
+
comicforge/render.py,sha256=9tyHIDlBvFW-JzwP8Qua4_SWrjmrf4kOmOF3P3vVGBM,18839
|
|
9
|
+
comicforge/scaffold.py,sha256=kk0Cqh6ipju5gJFeHtBuDvnsoNDr5iu2jWxzpt745ms,4826
|
|
10
|
+
comicforge/scene.py,sha256=F_R50iyPEera4Qrm1UTVnSCRxu6swTvBDRia-e0CBcQ,3683
|
|
11
|
+
comicforge/validate.py,sha256=0JP3_zXF2SG86guTdKUX4NAPgAirJvp1Q9w_JKdcZgY,6983
|
|
12
|
+
comicforge/_skill/SKILL.md,sha256=StwT5_nJUbpOUCAlHz886SDbO_p2NMDxVAhdwWx3ADg,13772
|
|
13
|
+
comicforge/_skill/reference.md,sha256=FmjJlAKHtOfzmR3Rb2PKeoMxEWR5BZNTze_S2KECj9M,22081
|
|
14
|
+
comicforge-0.0.1.dist-info/METADATA,sha256=yn_ZwEya1emxHogLK8wjOwMvNHYsAvP-_olDuvH3TNY,5652
|
|
15
|
+
comicforge-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
16
|
+
comicforge-0.0.1.dist-info/entry_points.txt,sha256=-79dTD3fWo81P5RApa0IJ_N8HIYCXnacavJtOY_Tg6k,77
|
|
17
|
+
comicforge-0.0.1.dist-info/RECORD,,
|