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.
- buttery-0.2.0/.github/workflows/ci.yml +16 -0
- buttery-0.2.0/.github/workflows/publish.yml +32 -0
- buttery-0.2.0/.gitignore +12 -0
- buttery-0.2.0/.python-version +1 -0
- buttery-0.2.0/BUILD_LOG.md +34 -0
- buttery-0.2.0/LICENSE +21 -0
- buttery-0.2.0/PKG-INFO +157 -0
- buttery-0.2.0/README.md +131 -0
- buttery-0.2.0/examples/bounce.json +114 -0
- buttery-0.2.0/examples/bounce.py +23 -0
- buttery-0.2.0/examples/bounce_preview.png +0 -0
- buttery-0.2.0/examples/launch_demo.json +26 -0
- buttery-0.2.0/examples/merge_sorted.gif +0 -0
- buttery-0.2.0/examples/merge_sorted.json +2127 -0
- buttery-0.2.0/examples/merge_sorted.py +183 -0
- buttery-0.2.0/examples/merge_sorted_preview.png +0 -0
- buttery-0.2.0/examples/rb_rotation.gif +0 -0
- buttery-0.2.0/examples/rb_rotation.json +2146 -0
- buttery-0.2.0/examples/rb_rotation.py +238 -0
- buttery-0.2.0/examples/rb_rotation_preview.png +0 -0
- buttery-0.2.0/examples/squash_bounce.json +1917 -0
- buttery-0.2.0/examples/squash_bounce.py +74 -0
- buttery-0.2.0/examples/squash_bounce_preview.png +0 -0
- buttery-0.2.0/npm/LICENSE +21 -0
- buttery-0.2.0/npm/README.md +31 -0
- buttery-0.2.0/npm/bin/buttery-mcp.js +37 -0
- buttery-0.2.0/npm/package.json +14 -0
- buttery-0.2.0/pyproject.toml +47 -0
- buttery-0.2.0/skill/SKILL.md +66 -0
- buttery-0.2.0/skill/reference.md +64 -0
- buttery-0.2.0/spec.md +146 -0
- buttery-0.2.0/src/buttery/__init__.py +27 -0
- buttery-0.2.0/src/buttery/cli.py +81 -0
- buttery-0.2.0/src/buttery/color.py +103 -0
- buttery-0.2.0/src/buttery/easing.py +60 -0
- buttery-0.2.0/src/buttery/errors.py +121 -0
- buttery-0.2.0/src/buttery/evaluate.py +156 -0
- buttery-0.2.0/src/buttery/expr.py +343 -0
- buttery-0.2.0/src/buttery/mcp_server.py +83 -0
- buttery-0.2.0/src/buttery/objects.py +156 -0
- buttery-0.2.0/src/buttery/ops.py +106 -0
- buttery-0.2.0/src/buttery/parse.py +157 -0
- buttery-0.2.0/src/buttery/render.py +362 -0
- buttery-0.2.0/src/buttery/scene.py +205 -0
- buttery-0.2.0/src/buttery/tools.py +113 -0
- buttery-0.2.0/tests/test_expr.py +117 -0
- buttery-0.2.0/tests/test_render.py +142 -0
- buttery-0.2.0/tests/test_scene.py +171 -0
- 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
|
buttery-0.2.0/.gitignore
ADDED
|
@@ -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
|
+

|
|
35
|
+
|
|
36
|
+
*`examples/merge_sorted.py`: the merge is simulated in Python, then each step becomes keyframes.*
|
|
37
|
+
|
|
38
|
+

|
|
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.
|
buttery-0.2.0/README.md
ADDED
|
@@ -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
|
+

|
|
9
|
+
|
|
10
|
+
*`examples/merge_sorted.py`: the merge is simulated in Python, then each step becomes keyframes.*
|
|
11
|
+
|
|
12
|
+

|
|
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"))
|
|
Binary file
|
|
@@ -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
|