buttery 0.2.0__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 (49) hide show
  1. buttery-0.2.0/.github/workflows/ci.yml +16 -0
  2. buttery-0.2.0/.github/workflows/publish.yml +32 -0
  3. buttery-0.2.0/.gitignore +12 -0
  4. buttery-0.2.0/.python-version +1 -0
  5. buttery-0.2.0/BUILD_LOG.md +34 -0
  6. buttery-0.2.0/LICENSE +21 -0
  7. buttery-0.2.0/PKG-INFO +157 -0
  8. buttery-0.2.0/README.md +131 -0
  9. buttery-0.2.0/examples/bounce.json +114 -0
  10. buttery-0.2.0/examples/bounce.py +23 -0
  11. buttery-0.2.0/examples/bounce_preview.png +0 -0
  12. buttery-0.2.0/examples/launch_demo.json +26 -0
  13. buttery-0.2.0/examples/merge_sorted.gif +0 -0
  14. buttery-0.2.0/examples/merge_sorted.json +2127 -0
  15. buttery-0.2.0/examples/merge_sorted.py +183 -0
  16. buttery-0.2.0/examples/merge_sorted_preview.png +0 -0
  17. buttery-0.2.0/examples/rb_rotation.gif +0 -0
  18. buttery-0.2.0/examples/rb_rotation.json +2146 -0
  19. buttery-0.2.0/examples/rb_rotation.py +238 -0
  20. buttery-0.2.0/examples/rb_rotation_preview.png +0 -0
  21. buttery-0.2.0/examples/squash_bounce.json +1917 -0
  22. buttery-0.2.0/examples/squash_bounce.py +74 -0
  23. buttery-0.2.0/examples/squash_bounce_preview.png +0 -0
  24. buttery-0.2.0/npm/LICENSE +21 -0
  25. buttery-0.2.0/npm/README.md +31 -0
  26. buttery-0.2.0/npm/bin/buttery-mcp.js +37 -0
  27. buttery-0.2.0/npm/package.json +14 -0
  28. buttery-0.2.0/pyproject.toml +47 -0
  29. buttery-0.2.0/skill/SKILL.md +66 -0
  30. buttery-0.2.0/skill/reference.md +64 -0
  31. buttery-0.2.0/spec.md +146 -0
  32. buttery-0.2.0/src/buttery/__init__.py +27 -0
  33. buttery-0.2.0/src/buttery/cli.py +81 -0
  34. buttery-0.2.0/src/buttery/color.py +103 -0
  35. buttery-0.2.0/src/buttery/easing.py +60 -0
  36. buttery-0.2.0/src/buttery/errors.py +121 -0
  37. buttery-0.2.0/src/buttery/evaluate.py +156 -0
  38. buttery-0.2.0/src/buttery/expr.py +343 -0
  39. buttery-0.2.0/src/buttery/mcp_server.py +83 -0
  40. buttery-0.2.0/src/buttery/objects.py +156 -0
  41. buttery-0.2.0/src/buttery/ops.py +106 -0
  42. buttery-0.2.0/src/buttery/parse.py +157 -0
  43. buttery-0.2.0/src/buttery/render.py +362 -0
  44. buttery-0.2.0/src/buttery/scene.py +205 -0
  45. buttery-0.2.0/src/buttery/tools.py +113 -0
  46. buttery-0.2.0/tests/test_expr.py +117 -0
  47. buttery-0.2.0/tests/test_render.py +142 -0
  48. buttery-0.2.0/tests/test_scene.py +171 -0
  49. buttery-0.2.0/uv.lock +1012 -0
@@ -0,0 +1,16 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v6
14
+ - run: sudo apt-get update -qq && sudo apt-get install -y -qq libegl1 libgl1 libfontconfig1 ffmpeg # skia-python + mp4 tests
15
+ - run: uv sync
16
+ - run: uv run pytest -q
@@ -0,0 +1,32 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v6
13
+ - run: uv build
14
+ - uses: actions/upload-artifact@v4
15
+ with:
16
+ name: dist
17
+ path: dist/
18
+
19
+ publish:
20
+ needs: build
21
+ runs-on: ubuntu-latest
22
+ environment:
23
+ name: pypi
24
+ url: https://pypi.org/project/buttery/
25
+ permissions:
26
+ id-token: write # OIDC token for PyPI trusted publishing
27
+ steps:
28
+ - uses: actions/download-artifact@v4
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ out/
8
+ *.mp4
9
+ *.mov
10
+ .pytest_cache/
11
+ .env
12
+ .env.*
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,34 @@
1
+ # How v1 was written
2
+
3
+ Order of work, bottom up. Each step was smoke-tested before the next.
4
+
5
+ 1. **Read the spec, check the toolchain.** uv, Python 3.13, ffmpeg, and whether skia-python has a macOS arm64 wheel. Spun up a scratch venv and drew one circle with skia to confirm array export and PNG encoding. Discovered the MCP SDK is 2.x (`MCPServer`, not `FastMCP`).
6
+
7
+ 2. **Scaffold.** `pyproject.toml` (hatchling, `src/` layout, `animator` console script), `.python-version`, `.gitignore`, `uv sync`.
8
+
9
+ 3. **Leaf modules, no dependencies.**
10
+ - `color.py`: hex and CSS-name parsing, lerp, hex output, a pydantic `Color` type.
11
+ - `easing.py`: the seven easing curves.
12
+ - `ops.py`: numeric implementations and arity table for the op vocabulary, including the 1D value noise.
13
+
14
+ 4. **Expression AST** (`expr.py`). `Ref`, `Op`, `Tween` as pydantic models with a shared operator-overloading mixin so `0.2 * sin(6 * T)` builds the same tree as the JSON. `Expr` is a discriminated union with a before-validator that parses strings. Sugar functions (`T`, `sin`, `tween`, `keyframes`, ...) and `deps()`.
15
+
16
+ 5. **Shorthand parser** (`parse.py`). Tokenizer plus recursive descent for `+ - * /`, unary minus, calls, `pi`/`tau`, references. Folds constant sub-expressions.
17
+ *Smoke test here:* Python vs JSON trees equal, parse errors readable. Hit and fixed a forward-ref problem by defining `Expr` after the node classes.
18
+
19
+ 6. **Objects and scene.**
20
+ - `errors.py`: `SceneError` (path, message, object, property, t) and conversion of pydantic errors into that shape.
21
+ - `objects.py`: `Circle Rect Line Text Group`, positional `id`, `obj.ref.prop` proxy, `animatable()` introspection via a `Kind` marker in the annotations.
22
+ - `evaluate.py`: compile each expression to a closure, topological order, `CompiledScene.state(t)`.
23
+ - `scene.py`: `Scene` model, `check()` for ids / references / cycles / tween kinds, JSON load and save, `state()`.
24
+ *Smoke test here:* state at t, JSON round trip, every error class.
25
+
26
+ 7. **Renderer** (`render.py`). Skia rasterizer in world units with y up, fill/stroke paints, cap-height-centered text, group transforms, sub-frame motion blur, PNG encode, parallel render with a process pool, ffmpeg for mp4. Looked at a preview and a blurred frame to confirm orientation and blur.
27
+
28
+ 8. **Agent surface.** `tools.py` (four functions, JSON in and out, all exceptions caught into structured errors), `cli.py`, `mcp_server.py`. Ran a full mp4 render from the CLI and listed tools through the MCP server in-process.
29
+
30
+ 9. **Tests** (`tests/`): expressions and parser, scene validation and evaluation, rendering and tools. Fixed the deprecated default typeface with a font fallback chain.
31
+
32
+ 10. **Examples, skill, README.** `examples/bounce.py`, `examples/launch_demo.json`, `skill/SKILL.md` and `reference.md`, this file.
33
+
34
+ - **Renamed to `buttery`** (2026-09-07). Package dir, pyproject name/script, imports, CLI, skill, README, examples, tests. The `animator` references above are historical.
buttery-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fletcher Graham
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
buttery-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.5
2
+ Name: buttery
3
+ Version: 0.2.0
4
+ Summary: Agent-friendly 2D explainer animations. The scene is a pure function of time.
5
+ Project-URL: Homepage, https://github.com/fletchgraham/buttery
6
+ Project-URL: Repository, https://github.com/fletchgraham/buttery
7
+ Project-URL: Issues, https://github.com/fletchgraham/buttery/issues
8
+ Author: Fletcher Graham
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: 2d,agent,animation,explainer,mcp,motion-graphics,skia
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Multimedia :: Graphics
19
+ Classifier: Topic :: Multimedia :: Video
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: mcp>=2.0
22
+ Requires-Dist: numpy>=1.26
23
+ Requires-Dist: pydantic>=2.10
24
+ Requires-Dist: skia-python>=138
25
+ Description-Content-Type: text/markdown
26
+
27
+ # buttery
28
+
29
+ Agent-friendly 2D explainer animations. **The scene is a pure function of time**: `state(t)` resolves every
30
+ property at `t`, nothing accumulates between frames, every frame renders independently and in parallel.
31
+
32
+ Pydantic models *are* the schema. JSON is the product contract; the Python API is sugar over the same models.
33
+
34
+ ![Merging two sorted lists, one comparison per beat](https://raw.githubusercontent.com/fletchgraham/buttery/main/examples/merge_sorted.gif)
35
+
36
+ *`examples/merge_sorted.py`: the merge is simulated in Python, then each step becomes keyframes.*
37
+
38
+ ![Red-black tree left rotation, one step at a time](https://raw.githubusercontent.com/fletchgraham/buttery/main/examples/rb_rotation.gif)
39
+
40
+ *`examples/rb_rotation.py`: nodes are groups, edges are lines whose endpoints reference the nodes, so the
41
+ edges follow the rotation for free. The before/after trees are two tuples; the script diffs them.*
42
+
43
+ ```
44
+ Agent tool surface validate / state / preview / render (tools.py, cli.py, mcp_server.py)
45
+ Authoring Python sugar <-> JSON (objects.py, expr.py, parse.py)
46
+ Core Scene, primitives, expression DAG (scene.py, evaluate.py)
47
+ Renderer skia-python, motion blur via sub-frames (render.py)
48
+ ```
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install buttery # or: uv add buttery
54
+ brew install ffmpeg # for .mp4 output (PNG sequences work without it)
55
+ ```
56
+
57
+ The renderer is skia-python, which ships large platform wheels; expect a heavier install than the code size suggests.
58
+
59
+ From a clone:
60
+
61
+ ```bash
62
+ uv sync # Python 3.11+, pydantic, skia-python, numpy, mcp
63
+ uv run pytest
64
+ ```
65
+
66
+ ## Python
67
+
68
+ ```python
69
+ from buttery import *
70
+
71
+ dot = Circle("dot", r=0.3, fill="coral")
72
+ dot.x = tween(-3, 3, at=0, dur=1.5, ease="out_cubic")
73
+ dot.y = 0.2 * sin(6 * T)
74
+
75
+ ring = Circle("ring", fill=None, stroke="white")
76
+ ring.x = dot.ref.x
77
+ ring.r = dot.ref.r + 0.5
78
+
79
+ scene = Scene(duration=3).add(dot, ring, Text("label", content="Hello", y=-1.6))
80
+ scene.state(1.0) # plain data
81
+ scene.preview(1.0, path="p.png") # quick low-res PNG
82
+ scene.render("out.mp4") # motion blur, all cores, ffmpeg
83
+ scene.save("scene.json") # the same thing as JSON
84
+ ```
85
+
86
+ `obj.prop` reads the stored expression; `obj.ref.prop` makes a reference to it (the spec's `dot.r + 0.5`
87
+ became `dot.ref.r + 0.5` because a plain attribute read cannot be both a value and a reference).
88
+
89
+ ## JSON
90
+
91
+ ```json
92
+ {
93
+ "duration": 3,
94
+ "objects": [
95
+ {"id": "dot", "type": "circle", "r": 0.3, "fill": "coral",
96
+ "x": {"op": "tween", "keys": [[0, -3], [1.5, 3]], "ease": "out_cubic"},
97
+ "y": {"op": "mul", "args": [0.2, {"op": "sin", "args": [{"op": "mul", "args": [6, "t"]}]}]}},
98
+ {"id": "ring", "type": "circle", "fill": null, "stroke": "white", "x": "dot.x", "r": "dot.r + 0.5"}
99
+ ]
100
+ }
101
+ ```
102
+
103
+ Shorthand strings like `"dot.r + 0.5"` parse (no `eval`) into the same op tree. `uv run buttery schema`
104
+ prints the JSON schema from `Scene.model_json_schema()`.
105
+
106
+ **Coordinates**: world units, origin at center, y up. The frame is `view_width` (default 8) units wide.
107
+ **Primitives**: `circle rect line text group`. `text` wraps at `max_width` and honors newlines. **Ops**: `add sub mul div neg sin cos abs min max clamp smoothstep noise`.
108
+ **Eases**: `linear in_quad out_quad in_out_quad out_cubic in_out_cubic spring`. Colors: hex or CSS names;
109
+ `fill`/`stroke` can be tweened between colors.
110
+
111
+ ## Agent surface
112
+
113
+ ```bash
114
+ uv run buttery validate scene.json
115
+ uv run buttery state scene.json --t 1.25
116
+ uv run buttery preview scene.json --t 1.25 --out p.png --scale 0.25
117
+ uv run buttery render scene.json out.mp4 [--no-motion-blur --samples 8 --shutter 0.5 --workers N]
118
+ uv run buttery mcp # MCP server on stdio: same four tools + scene://schema resource
119
+ ```
120
+
121
+ Every call returns `{"ok": true, ...}` or `{"ok": false, "errors": [{"path", "message", "object", "property", "t"}]}`.
122
+ Never a bare stack trace.
123
+
124
+ Claude Code skill: `skill/` (symlink or copy it into `~/.claude/skills/buttery`). Register the MCP server with
125
+ `claude mcp add buttery -- uv run --directory /path/to/this/repo buttery mcp`.
126
+ Or from the published packages, no checkout needed: `claude mcp add buttery -- npx -y buttery-mcp`
127
+ (the [`buttery-mcp`](https://www.npmjs.com/package/buttery-mcp) npm shim runs `uvx buttery mcp`; see `npm/`).
128
+
129
+ ## Validation and evaluation rules
130
+
131
+ - Structural validation is pydantic (unknown fields rejected, arity checked, colors checked).
132
+ - Semantic validation: unique ids (global across groups), references resolve to numeric animatable
133
+ properties, no dependency cycles, tween key kinds match the property kind.
134
+ - Evaluation compiles each expression to a closure once, walks the DAG in dependency order, and never mutates the scene.
135
+ - Motion blur: `samples` sub-frames spread across `shutter` × frame interval, averaged (0.5 = 180° shutter).
136
+
137
+ ## Layout
138
+
139
+ ```
140
+ src/buttery/
141
+ expr.py AST models (Op, Tween, Ref), operator overloading, sugar (T, sin, tween, keyframes, ...)
142
+ parse.py shorthand parser -> AST, constant folding
143
+ objects.py Circle, Rect, Line, Text, Group
144
+ scene.py Scene, semantic checks, state(t)
145
+ evaluate.py compile + topological evaluation
146
+ render.py skia rasterizer, motion blur, parallel render, ffmpeg
147
+ tools.py validate / state / preview / render (JSON in, JSON out)
148
+ cli.py `buttery` command
149
+ mcp_server.py MCP server
150
+ examples/ bounce.py, squash_bounce.py, merge_sorted.py, rb_rotation.py (Python) and their .json, launch_demo.json
151
+ skill/ Claude Code skill
152
+ tests/
153
+ ```
154
+
155
+ ## Not in v1
156
+
157
+ Equations/LaTeX, 3D or cameras, GUI, audio, plugins, manim parity.
@@ -0,0 +1,131 @@
1
+ # buttery
2
+
3
+ Agent-friendly 2D explainer animations. **The scene is a pure function of time**: `state(t)` resolves every
4
+ property at `t`, nothing accumulates between frames, every frame renders independently and in parallel.
5
+
6
+ Pydantic models *are* the schema. JSON is the product contract; the Python API is sugar over the same models.
7
+
8
+ ![Merging two sorted lists, one comparison per beat](https://raw.githubusercontent.com/fletchgraham/buttery/main/examples/merge_sorted.gif)
9
+
10
+ *`examples/merge_sorted.py`: the merge is simulated in Python, then each step becomes keyframes.*
11
+
12
+ ![Red-black tree left rotation, one step at a time](https://raw.githubusercontent.com/fletchgraham/buttery/main/examples/rb_rotation.gif)
13
+
14
+ *`examples/rb_rotation.py`: nodes are groups, edges are lines whose endpoints reference the nodes, so the
15
+ edges follow the rotation for free. The before/after trees are two tuples; the script diffs them.*
16
+
17
+ ```
18
+ Agent tool surface validate / state / preview / render (tools.py, cli.py, mcp_server.py)
19
+ Authoring Python sugar <-> JSON (objects.py, expr.py, parse.py)
20
+ Core Scene, primitives, expression DAG (scene.py, evaluate.py)
21
+ Renderer skia-python, motion blur via sub-frames (render.py)
22
+ ```
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install buttery # or: uv add buttery
28
+ brew install ffmpeg # for .mp4 output (PNG sequences work without it)
29
+ ```
30
+
31
+ The renderer is skia-python, which ships large platform wheels; expect a heavier install than the code size suggests.
32
+
33
+ From a clone:
34
+
35
+ ```bash
36
+ uv sync # Python 3.11+, pydantic, skia-python, numpy, mcp
37
+ uv run pytest
38
+ ```
39
+
40
+ ## Python
41
+
42
+ ```python
43
+ from buttery import *
44
+
45
+ dot = Circle("dot", r=0.3, fill="coral")
46
+ dot.x = tween(-3, 3, at=0, dur=1.5, ease="out_cubic")
47
+ dot.y = 0.2 * sin(6 * T)
48
+
49
+ ring = Circle("ring", fill=None, stroke="white")
50
+ ring.x = dot.ref.x
51
+ ring.r = dot.ref.r + 0.5
52
+
53
+ scene = Scene(duration=3).add(dot, ring, Text("label", content="Hello", y=-1.6))
54
+ scene.state(1.0) # plain data
55
+ scene.preview(1.0, path="p.png") # quick low-res PNG
56
+ scene.render("out.mp4") # motion blur, all cores, ffmpeg
57
+ scene.save("scene.json") # the same thing as JSON
58
+ ```
59
+
60
+ `obj.prop` reads the stored expression; `obj.ref.prop` makes a reference to it (the spec's `dot.r + 0.5`
61
+ became `dot.ref.r + 0.5` because a plain attribute read cannot be both a value and a reference).
62
+
63
+ ## JSON
64
+
65
+ ```json
66
+ {
67
+ "duration": 3,
68
+ "objects": [
69
+ {"id": "dot", "type": "circle", "r": 0.3, "fill": "coral",
70
+ "x": {"op": "tween", "keys": [[0, -3], [1.5, 3]], "ease": "out_cubic"},
71
+ "y": {"op": "mul", "args": [0.2, {"op": "sin", "args": [{"op": "mul", "args": [6, "t"]}]}]}},
72
+ {"id": "ring", "type": "circle", "fill": null, "stroke": "white", "x": "dot.x", "r": "dot.r + 0.5"}
73
+ ]
74
+ }
75
+ ```
76
+
77
+ Shorthand strings like `"dot.r + 0.5"` parse (no `eval`) into the same op tree. `uv run buttery schema`
78
+ prints the JSON schema from `Scene.model_json_schema()`.
79
+
80
+ **Coordinates**: world units, origin at center, y up. The frame is `view_width` (default 8) units wide.
81
+ **Primitives**: `circle rect line text group`. `text` wraps at `max_width` and honors newlines. **Ops**: `add sub mul div neg sin cos abs min max clamp smoothstep noise`.
82
+ **Eases**: `linear in_quad out_quad in_out_quad out_cubic in_out_cubic spring`. Colors: hex or CSS names;
83
+ `fill`/`stroke` can be tweened between colors.
84
+
85
+ ## Agent surface
86
+
87
+ ```bash
88
+ uv run buttery validate scene.json
89
+ uv run buttery state scene.json --t 1.25
90
+ uv run buttery preview scene.json --t 1.25 --out p.png --scale 0.25
91
+ uv run buttery render scene.json out.mp4 [--no-motion-blur --samples 8 --shutter 0.5 --workers N]
92
+ uv run buttery mcp # MCP server on stdio: same four tools + scene://schema resource
93
+ ```
94
+
95
+ Every call returns `{"ok": true, ...}` or `{"ok": false, "errors": [{"path", "message", "object", "property", "t"}]}`.
96
+ Never a bare stack trace.
97
+
98
+ Claude Code skill: `skill/` (symlink or copy it into `~/.claude/skills/buttery`). Register the MCP server with
99
+ `claude mcp add buttery -- uv run --directory /path/to/this/repo buttery mcp`.
100
+ Or from the published packages, no checkout needed: `claude mcp add buttery -- npx -y buttery-mcp`
101
+ (the [`buttery-mcp`](https://www.npmjs.com/package/buttery-mcp) npm shim runs `uvx buttery mcp`; see `npm/`).
102
+
103
+ ## Validation and evaluation rules
104
+
105
+ - Structural validation is pydantic (unknown fields rejected, arity checked, colors checked).
106
+ - Semantic validation: unique ids (global across groups), references resolve to numeric animatable
107
+ properties, no dependency cycles, tween key kinds match the property kind.
108
+ - Evaluation compiles each expression to a closure once, walks the DAG in dependency order, and never mutates the scene.
109
+ - Motion blur: `samples` sub-frames spread across `shutter` × frame interval, averaged (0.5 = 180° shutter).
110
+
111
+ ## Layout
112
+
113
+ ```
114
+ src/buttery/
115
+ expr.py AST models (Op, Tween, Ref), operator overloading, sugar (T, sin, tween, keyframes, ...)
116
+ parse.py shorthand parser -> AST, constant folding
117
+ objects.py Circle, Rect, Line, Text, Group
118
+ scene.py Scene, semantic checks, state(t)
119
+ evaluate.py compile + topological evaluation
120
+ render.py skia rasterizer, motion blur, parallel render, ffmpeg
121
+ tools.py validate / state / preview / render (JSON in, JSON out)
122
+ cli.py `buttery` command
123
+ mcp_server.py MCP server
124
+ examples/ bounce.py, squash_bounce.py, merge_sorted.py, rb_rotation.py (Python) and their .json, launch_demo.json
125
+ skill/ Claude Code skill
126
+ tests/
127
+ ```
128
+
129
+ ## Not in v1
130
+
131
+ Equations/LaTeX, 3D or cameras, GUI, audio, plugins, manim parity.
@@ -0,0 +1,114 @@
1
+ {
2
+ "size": [
3
+ 1920,
4
+ 1080
5
+ ],
6
+ "fps": 60,
7
+ "duration": 3.0,
8
+ "background": "#111111",
9
+ "view_width": 8.0,
10
+ "objects": [
11
+ {
12
+ "id": "dot",
13
+ "opacity": 1.0,
14
+ "type": "circle",
15
+ "x": {
16
+ "op": "tween",
17
+ "keys": [
18
+ [
19
+ 0.0,
20
+ -3.0
21
+ ],
22
+ [
23
+ 1.5,
24
+ 3.0
25
+ ]
26
+ ],
27
+ "ease": "out_cubic"
28
+ },
29
+ "y": {
30
+ "op": "mul",
31
+ "args": [
32
+ 0.2,
33
+ {
34
+ "op": "sin",
35
+ "args": [
36
+ {
37
+ "op": "mul",
38
+ "args": [
39
+ 6.0,
40
+ "t"
41
+ ]
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ },
47
+ "r": 0.3,
48
+ "fill": "coral",
49
+ "stroke": null,
50
+ "stroke_width": 0.05
51
+ },
52
+ {
53
+ "id": "ring",
54
+ "opacity": {
55
+ "op": "tween",
56
+ "keys": [
57
+ [
58
+ 0.0,
59
+ 0.0
60
+ ],
61
+ [
62
+ 0.5,
63
+ 1.0
64
+ ],
65
+ [
66
+ 2.5,
67
+ 1.0
68
+ ],
69
+ [
70
+ 3.0,
71
+ 0.0
72
+ ]
73
+ ],
74
+ "ease": "linear"
75
+ },
76
+ "type": "circle",
77
+ "x": "dot.x",
78
+ "y": 0.0,
79
+ "r": {
80
+ "op": "add",
81
+ "args": [
82
+ "dot.r",
83
+ 0.5
84
+ ]
85
+ },
86
+ "fill": null,
87
+ "stroke": "white",
88
+ "stroke_width": 0.04
89
+ },
90
+ {
91
+ "id": "floor",
92
+ "opacity": 1.0,
93
+ "type": "line",
94
+ "x1": -3.5,
95
+ "y1": -0.6,
96
+ "x2": 3.5,
97
+ "y2": -0.6,
98
+ "stroke": "#444444",
99
+ "stroke_width": 0.03
100
+ },
101
+ {
102
+ "id": "label",
103
+ "opacity": 1.0,
104
+ "type": "text",
105
+ "content": "state(t) is a pure function",
106
+ "x": 0.0,
107
+ "y": -1.6,
108
+ "size": 0.4,
109
+ "fill": "#cccccc",
110
+ "font": null,
111
+ "align": "center"
112
+ }
113
+ ]
114
+ }
@@ -0,0 +1,23 @@
1
+ """Python sugar example. Run: uv run python examples/bounce.py"""
2
+
3
+ from buttery import Circle, Line, Scene, T, Text, keyframes, sin, tween
4
+
5
+ dot = Circle("dot", r=0.3, fill="coral")
6
+ dot.x = tween(-3, 3, at=0, dur=1.5, ease="out_cubic")
7
+ dot.y = 0.2 * sin(6 * T)
8
+
9
+ ring = Circle("ring", fill=None, stroke="white", stroke_width=0.04)
10
+ ring.x = dot.ref.x
11
+ ring.r = dot.ref.r + 0.5
12
+ ring.opacity = keyframes({0: 0, 0.5: 1, 2.5: 1, 3: 0})
13
+
14
+ floor = Line("floor", x1=-3.5, y1=-0.6, x2=3.5, y2=-0.6, stroke="#444444", stroke_width=0.03)
15
+ label = Text("label", content="state(t) is a pure function", y=-1.6, size=0.4, fill="#cccccc")
16
+
17
+ scene = Scene(duration=3).add(dot, ring, floor, label)
18
+
19
+ if __name__ == "__main__":
20
+ scene.save("examples/bounce.json")
21
+ print(scene.state(1.0))
22
+ scene.preview(1.0, path="examples/bounce_preview.png")
23
+ print(scene.render("examples/bounce.mp4"))
@@ -0,0 +1,26 @@
1
+ {
2
+ "duration": 5,
3
+ "fps": 60,
4
+ "size": [1920, 1080],
5
+ "background": "#0e0e11",
6
+ "objects": [
7
+ {"id": "track", "type": "line", "x1": -3.2, "y1": -0.9, "x2": 3.2, "y2": -0.9, "stroke": "#2a2a30", "stroke_width": 0.04},
8
+ {"id": "ball", "type": "circle", "r": 0.35, "fill": "#ff7f50",
9
+ "x": {"op": "tween", "keys": [[0.4, -3], [1.8, 2.4], [3.2, -1.0], [4.4, 3]], "ease": "in_out_cubic"},
10
+ "y": {"op": "add", "args": [-0.55, {"op": "mul", "args": [0.9, {"op": "abs", "args": [{"op": "sin", "args": ["4*t"]}]}]}]}},
11
+ {"id": "shadow", "type": "rect", "x": "ball.x", "y": -0.92, "h": 0.06, "corner_radius": 0.03, "fill": "#000000",
12
+ "w": {"op": "sub", "args": [1.2, {"op": "mul", "args": [0.5, {"op": "add", "args": ["ball.y", 0.55]}]}]},
13
+ "opacity": {"op": "sub", "args": [0.7, {"op": "mul", "args": [0.4, {"op": "add", "args": ["ball.y", 0.55]}]}]}},
14
+ {"id": "box", "type": "rect", "w": 0.9, "h": 0.9, "corner_radius": 0.12, "x": 2.2, "y": 1.1, "fill": "#4a90d9",
15
+ "rotation": {"op": "tween", "keys": [[1, 0], [2.5, 180]], "ease": "spring"},
16
+ "opacity": {"op": "tween", "keys": [[0.8, 0], [1.2, 1]]}},
17
+ {"id": "orbit", "type": "group", "x": -2.2, "y": 1.1, "rotation": "90*t", "children": [
18
+ {"id": "o1", "type": "circle", "x": 0.6, "r": 0.12, "fill": "#ffd166"},
19
+ {"id": "o2", "type": "circle", "x": -0.6, "r": 0.12, "fill": "#06d6a0"},
20
+ {"id": "o3", "type": "circle", "y": 0.6, "r": 0.12, "fill": "#ef476f"},
21
+ {"id": "o4", "type": "circle", "y": -0.6, "r": 0.12, "fill": "#118ab2"}
22
+ ]},
23
+ {"id": "title", "type": "text", "content": "Eased motion, real motion blur", "y": -1.7, "size": 0.42,
24
+ "fill": {"op": "tween", "keys": [[0, "#0e0e11"], [1, "#e8e8ec"]], "ease": "out_quad"}}
25
+ ]
26
+ }
Binary file