Skip to content

Commit 88090fb

Browse files
authored
Merge pull request #8 from subroy13/dev
contributing docs, setup todo list, and a few bug fixes
2 parents 4a571ee + c2b0d98 commit 88090fb

49 files changed

Lines changed: 2907 additions & 594 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coverage

52 KB
Binary file not shown.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@ dist/
2323
.vscode/
2424

2525
# Mac system files
26-
.DS_Store
26+
.DS_Store
27+
28+
# Poetry
29+
.poetry/*

CLAUDE.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# CLAUDE.md — handanim
2+
3+
> See [repo_overview.md](repo_overview.md) for the full module map, dependency table, and known bug list.
4+
> See [todo.md](todo.md) for the phased roadmap and open issues.
5+
6+
---
7+
8+
## What this project is
9+
10+
**handanim** is a Python library for creating whiteboard-style, hand-drawn animations programmatically. It renders to MP4, GIF, or SVG. The user writes Python code that composes shapes, styles, and timed animation events; the library takes care of the rest.
11+
12+
The long-term arc is threefold:
13+
1. **A great animation toolkit** — expressive, composable, hand-drawn feel with a clean Python API
14+
2. **An AI-scriptable system** — LLMs should be able to think in scenes and write valid handanim code; the `handanim_ai` module scaffolds this
15+
3. **A handwriting font pipeline** — generate realistic single-line stroke fonts from samples of real handwriting, using RNN/transformer models
16+
17+
---
18+
19+
## Guiding Vision
20+
21+
> *Reproducible, but hand-sketchy.*
22+
23+
Every output must look like it was drawn by a careful human hand — wobbly lines, hachure fills, natural stroke pressure — while being fully reproducible from code. The aesthetic is the product, not an afterthought.
24+
25+
This means:
26+
- The "roughness" and "sketchy" feel must be preserved at all costs when touching rendering code
27+
- New primitives should integrate with `SketchStyle` and `StrokeStyle`; they should not look crisp/digital by default
28+
- When adding new drawing primitives, they must produce output that a `SketchAnimation` can animate stroke-by-stroke
29+
30+
---
31+
32+
## Architectural Invariants — Do Not Break These
33+
34+
### 1. Drawables are stateless and immutable
35+
`Drawable.translate()`, `.scale()`, `.rotate()` **return a new `TransformedDrawable`** — they do not mutate the original.
36+
Always use the return value: `obj = obj.translate(dx, dy)`.
37+
Never add mutable state to a `Drawable` subclass.
38+
39+
### 2. `AnimationEvent.apply()` must be a pure function
40+
`apply(opsset, progress)` must not have side effects. It receives an OpsSet, returns a new one. It must not mutate `opsset`, modify `self`, or touch the scene.
41+
The `Scene` caches the output of completed events and re-uses it as input to later events. Any side effect here will corrupt the timeline.
42+
43+
### 3. OpsSet is the universal interface between layers
44+
Shapes produce OpsSet. Animations consume and return OpsSet. Cairo consumes OpsSet.
45+
No layer should reach past OpsSet to touch Cairo directly — that is `OpsSet.render()`'s job only.
46+
47+
### 4. Easing and progress
48+
The `progress` value passed to `apply()` is always in `[0.0, 1.0]`. If `easing_fun` is set on an event, apply it first: `progress = self.easing_fun(raw_progress) if self.easing_fun else raw_progress`. This is currently unimplemented everywhere — wire it in when touching any `apply()` method.
49+
50+
---
51+
52+
## Code Style
53+
54+
- **No unnecessary comments** — name things well instead; only comment non-obvious invariants or external constraints
55+
- **Docstrings on important functions** — Add verbose docstrings for all important functions, as documentations are generated from it.
56+
- **Type annotations** — use them on all new public functions; prefer specific types over `Any`
57+
- **No defensive checks on internal paths** — only validate at system boundaries (file paths, user-supplied coordinates, external API calls)
58+
- **Prefer returning new objects** — consistent with the immutability design; avoid `opsset.opsset = ...` mutations inside animation code except in `OpsSet`'s own transform methods
59+
60+
---
61+
62+
## How the Rendering Pipeline Works (summary)
63+
64+
```
65+
Scene.render()
66+
└─ create_event_timeline() # one OpsSet per frame
67+
└─ for each active drawable:
68+
get_animated_opsset_at_time(drawable_id, t, event_and_progress)
69+
└─ recursive: apply events in order, cache completed ones
70+
└─ event.apply(opsset, progress) → OpsSet
71+
└─ for each frame OpsSet:
72+
viewport.apply_to_context(ctx)
73+
opsset.render(ctx) # the only Cairo call
74+
write frame to video
75+
```
76+
77+
The cache key for a completed event state is `"{drawable_id}__{event_id}"`.
78+
The initial OpsSet (before any animation) is cached as `"{drawable_id}__init"`.
79+
80+
---
81+
82+
## Where to Add Things
83+
84+
| What | Where |
85+
|---|---|
86+
| New shape | New file in `primitives/`, subclass `Drawable`, implement `draw() -> OpsSet` |
87+
| New animation | New file in `animations/`, subclass `AnimationEvent`, implement `apply(opsset, progress) -> OpsSet` |
88+
| New style option | Add field to `StrokeStyle` / `FillStyle` / `SketchStyle` in `core/styles.py` |
89+
| New fill pattern | `stylings/fillpatterns.py`, must return an `OpsSet` |
90+
| New color constant | `stylings/color.py` |
91+
| AI prompt | `handanim_ai/prompts/` as a `.txt` file |
92+
| New example | `examples/` as a standalone runnable script |
93+
94+
---
95+
96+
## Things to Be Careful About
97+
98+
- **`SVG` vs `VectorSVG`** — there are two SVG importers. `VectorSVG` (using `svgelements`) is the current one with full color/transform support. `SVG` (using `svgpathtools`) is legacy, paths-only, and deprecated. Prefer `VectorSVG`. Do not add new code that imports from `primitives/svg.py`.
99+
- **`DrawableGroup` parallel mode** — uses `drawable_element_id` metadata on OpsSet to extract individual results after a group transformation. If you touch group animation logic in `scene.py`, test with nested groups.
100+
- **Viewport coordinates** — the default world space is `(0, 1777) × (0, 1000)` for a 1920×1088 scene. All primitive positions are in this coordinate system, not pixels.
101+
- **`SketchAnimation` and fill**`get_partial_sketch` splits draw ops from fill ops using a `METADATA` op with `drawing_mode == "fill"`. Primitives that have both a stroked outline and a fill must emit this metadata correctly.
102+
103+
---
104+
105+
## Current Priorities (see todo.md for full list)
106+
107+
1. Add visual regression tests before expanding the surface area further
108+
2. Implement `FlowchartNode` / `FlowchartConnector` as the next major primitive cluster
109+
3. Complete `apply_strokes_gradient` and wire up `ColorTransitionAnimation`
110+
4. Improve caching (static object cache, group transform result cache)

DEVELOPMENT.md

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# DEVELOPMENT.md — handanim
2+
3+
Developer reference for setting up, testing, documenting, and running handanim locally.
4+
5+
---
6+
7+
## Prerequisites
8+
9+
- Python 3.11+
10+
- [Poetry](https://python-poetry.org/) for dependency management
11+
- Cairo system library (`pycairo` requires it; see platform notes below)
12+
13+
### Cairo installation
14+
15+
**macOS**
16+
```bash
17+
brew install cairo pkg-config
18+
```
19+
20+
**Ubuntu / Debian**
21+
```bash
22+
sudo apt-get install libcairo2-dev pkg-config python3-dev
23+
```
24+
25+
**Windows** — install via the [GTK for Windows Runtime](https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases).
26+
27+
---
28+
29+
## Setup
30+
31+
```bash
32+
# Clone and enter the repo
33+
git clone <repo-url>
34+
cd handanim
35+
36+
# Install all dependencies (runtime + dev)
37+
poetry install --with dev
38+
39+
# Activate the virtual environment (optional, for a plain shell)
40+
poetry shell
41+
```
42+
43+
---
44+
45+
## Running the Tests
46+
47+
### Full test suite
48+
49+
```bash
50+
poetry run python3 -m pytest
51+
```
52+
53+
### Run a specific test file
54+
55+
```bash
56+
poetry run python3 -m pytest tests/test_opsset.py
57+
poetry run python3 -m pytest tests/test_sketch.py
58+
poetry run python3 -m pytest tests/test_visuals.py
59+
```
60+
61+
### Run a single test by name
62+
63+
```bash
64+
poetry run python3 -m pytest -k "test_rotate_90_degrees"
65+
```
66+
67+
### Verbose output
68+
69+
```bash
70+
poetry run python3 -m pytest -v
71+
```
72+
73+
---
74+
75+
## Test Coverage
76+
77+
Coverage is measured with `pytest-cov`. The `--cov-report=term-missing` flag shows exact line numbers not covered — open the file alongside the report to see which branches are untested.
78+
79+
### Terminal report (line numbers)
80+
81+
```bash
82+
poetry run python3 -m pytest --cov=src/handanim --cov-report=term-missing
83+
```
84+
85+
### HTML report (browsable, colour-coded per file)
86+
87+
```bash
88+
poetry run python3 -m pytest --cov=src/handanim --cov-report=html
89+
open htmlcov/index.html # macOS
90+
xdg-open htmlcov/index.html # Linux
91+
```
92+
93+
### Single module only
94+
95+
```bash
96+
poetry run python3 -m pytest --cov=src/handanim/primitives/text --cov-report=term-missing
97+
```
98+
99+
### Enforce a minimum threshold (useful in CI)
100+
101+
```bash
102+
poetry run python3 -m pytest --cov=src/handanim --cov-fail-under=50
103+
```
104+
105+
---
106+
107+
## Visual Regression Tests
108+
109+
Visual regression tests live in `tests/test_visuals.py`. They render small deterministic scenes to PNG and compare them against reference files stored in `tests/snapshots/` using [SSIM](https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.structural_similarity).
110+
111+
**How numpy seeding makes renders deterministic:** every test runs with `numpy.random.seed(42)` (set via the `seed_numpy` autouse fixture in `conftest.py`). This makes the random jitter in rough primitives (Line, Rectangle, etc.) identical across runs.
112+
113+
### Regenerate reference snapshots
114+
115+
Run this after intentionally changing rendering output — for example, after modifying a primitive's draw method or a style default:
116+
117+
```bash
118+
poetry run python3 -m pytest tests/test_visuals.py --snapshot-update
119+
```
120+
121+
Review the diff in `tests/snapshots/` before committing to make sure the visual change is intentional.
122+
123+
### Failure threshold
124+
125+
A visual test fails when SSIM drops below **0.98**. This catches structural rendering changes while tolerating sub-pixel float differences across platforms.
126+
127+
---
128+
129+
## Building the Documentation
130+
131+
Docs are built with [Sphinx](https://www.sphinx-doc.org/) using the [Furo](https://pradyunsg.me/furo/) theme. Docstrings are pulled in automatically via `sphinx.ext.autodoc`.
132+
133+
```bash
134+
# Build HTML docs
135+
cd docs
136+
poetry run make html
137+
138+
# Output is written to docs/build/html/
139+
# Open in browser
140+
open build/html/index.html # macOS
141+
xdg-open build/html/index.html # Linux
142+
```
143+
144+
### Regenerating the API stubs
145+
146+
If you add a new module and want it to appear in the docs, regenerate the `.rst` stubs from the project root:
147+
148+
```bash
149+
poetry run sphinx-apidoc -o docs/source src/handanim --force
150+
```
151+
152+
Then rebuild HTML as above.
153+
154+
### Live reload during doc writing
155+
156+
```bash
157+
poetry run sphinx-autobuild docs/source docs/build/html
158+
# Serves at http://127.0.0.1:8000 and reloads on file change
159+
```
160+
161+
> `sphinx-autobuild` is not in the dev dependencies by default. Install it once with `poetry add --group dev sphinx-autobuild`.
162+
163+
---
164+
165+
## Running Examples
166+
167+
Each script in `examples/` is a self-contained scene that renders to an MP4 (written to `examples/output/`).
168+
169+
```bash
170+
# Pythagoras theorem — text, polygons, eraser
171+
poetry run python3 examples/pythagoras.py
172+
173+
# (a+b)² visual proof — algebra with hand-drawn shapes
174+
poetry run python3 examples/a_plus_b_square.py
175+
176+
# Distributive property with an SVG character
177+
poetry run python3 examples/distributive_property.py
178+
179+
# Solar system orbit animation
180+
poetry run python3 examples/solar_system.py
181+
182+
# Custom font rendering
183+
poetry run python3 examples/custom_font.py
184+
```
185+
186+
Output files land in `examples/output/`. The examples are the canonical "does this actually work end-to-end" check — run one before and after touching rendering code.
187+
188+
---
189+
190+
## Quick Smoke Test (no video output)
191+
192+
To verify a primitive renders without errors, use `OpsSet.quick_view()`. It renders to a temporary SVG and opens it in your browser:
193+
194+
```python
195+
from handanim.primitives import Rectangle
196+
from handanim.core.styles import StrokeStyle, SketchStyle
197+
198+
rect = Rectangle(
199+
top_left=(100, 100),
200+
width=400,
201+
height=300,
202+
stroke_style=StrokeStyle(color=(0.1, 0.1, 0.8), width=2),
203+
sketch_style=SketchStyle(roughness=1),
204+
)
205+
rect.draw().quick_view()
206+
```
207+
208+
Run it with:
209+
210+
```bash
211+
poetry run python3 -c "
212+
from handanim.primitives import Rectangle
213+
from handanim.core.styles import StrokeStyle
214+
rect = Rectangle((100,100), 400, 300, stroke_style=StrokeStyle(color=(0,0,0.8), width=2))
215+
rect.draw().quick_view(block=False)
216+
"
217+
```
218+
219+
---
220+
221+
## Project Layout Cheat Sheet
222+
223+
```
224+
src/handanim/
225+
├── core/ # OpsSet, Drawable, AnimationEvent, Scene, Viewport, styles
226+
├── primitives/ # Line, Rectangle, Ellipse, Arrow, Text, Math, VectorSVG, …
227+
├── animations/ # SketchAnimation, FadeIn/Out, Zoom, Translate
228+
└── stylings/ # color constants, fill patterns, stroke utilities
229+
230+
tests/
231+
├── conftest.py # shared fixtures (seed_numpy, render_to_png_bytes)
232+
├── snapshots/ # reference PNGs for visual regression
233+
├── test_opsset.py # geometry unit tests (no Cairo)
234+
├── test_sketch.py # SketchAnimation logic tests (no Cairo)
235+
└── test_visuals.py # visual regression tests (Cairo + pytest-snapshot)
236+
237+
examples/ # runnable end-to-end scene scripts
238+
docs/ # Sphinx source and build output
239+
```
240+
241+
---
242+
243+
## Useful One-Liners
244+
245+
```bash
246+
# Check that the package imports cleanly
247+
poetry run python3 -c "import handanim; print('ok')"
248+
249+
# List all test node IDs without running them
250+
poetry run python3 -m pytest --collect-only -q
251+
252+
# Run only tests that don't touch Cairo (fast pure-logic subset)
253+
poetry run python3 -m pytest tests/test_opsset.py tests/test_sketch.py
254+
255+
# Show which snapshot files exist
256+
ls tests/snapshots/
257+
```

0 commit comments

Comments
 (0)