entry: farmer state / farmer transition · requires a registered asset with identity.json
A lamp that turns on must not also move two pixels
Edit endpoints regenerate the whole canvas. Ask a model for “the same lamp, but lit” and you get a lamp that is lit and also very slightly different everywhere — a shifted shade, a redrawn base, a resampled edge. At runtime that reads as a bug. This fork does not ask the model to be stable. It takes the pixels it wanted from the generation, pastes the anchor's pixels back over everything else, and then asserts the result.
“Code guarantees over model trust: composite-back for states, endpoint locking for transitions, recomposite diffs for scenes — each asserted, with the check artifact saved next to the output.”
README.md, key design rules-
04a
The state pass
farmer/states.py:69A registered object's current state is tiled into all four cells of a 2×2 sheet and the model is asked for the same new state in every cell. Co-generation on one canvas forces identity between the candidates; the pipeline then picks one, and throws away everything it did not ask for.
Figure 9 — the guarantee, on real numbers. Every figure in this diagram is read from objects/lamp/runs/state-on/state-report.jsonandobjects/wallclock/runs/state-cracked/state-report.jsonin the repository. The median selection can be checked by hand: sorting the lamp's four candidate scores puts 26,733 at index 2, and the report recordspicked_cell: 3.$ cat objects/lamp/runs/state-on/state-report.json { "object": "lamp", "state": "on", "from": "idle", "file": "states/on.png", "region": [ 0, 0, 194, 341 ], "picked_cell": 3, "changed_px": 26733, "candidates_changed_px": [ 26455, 26533, 26810, 26733 ], "outside_region": { "outside_pixels_checked": 2387, "outside_pixels_different": 0, "identical_outside_region": true }, "despill": { "mode": "default", "residual": 0 }, "emissive": true, "run_dir": "objects/lamp/runs/state-on", "pass": true }
The command that writes it takes its flags from
cli.py:242–253:--tonames the new state,--fromdefaults toidle,--deltadescribes what changes (defaulting tothe object switched to its <state> state
),--regionoverrides the auto-detected box,--emissiverequests the glow overlay, and--pickforces a candidate cell index. -
--emissive → composite.extract_glow(on, off)
-
04b
Separating the glow instead of baking it
farmer/composite.py:227An emissive state is stored twice: once as the finished state PNG, and once as a screen-blend layer that reproduces it from the unlit state. The separation is a direct algebraic solve, not a heuristic.
farmer/composite.py:227–255extract_glow — solve screen(off, glow) = on# premultiply against transparent black so alpha differences (a glow that # extends past the off-state silhouette) contribute correctly n_rgb = n[..., :3] * n[..., 3:4] d_rgb = d[..., :3] * d[..., 3:4] denom = np.clip(1.0 - d_rgb, 1e-4, 1.0) glow = np.clip((n_rgb - d_rgb) / denom, 0.0, 1.0) # only brightening is # representable in screen glow_u8[glow_u8 < threshold] = 0 # threshold = 10 of 255 im = im.filter(ImageFilter.GaussianBlur(blur)) # blur = 2.0, so a runtime # can pulse it without banding alpha = arr.max(axis=-1) # normal-blend runtimes get # a usable falloff too
The returned PNG carries the screen layer in RGB and a per-pixel intensity in alpha, so a runtime that cannot do screen blending still has something sensible to composite. The lamp's copy lives atobjects/lamp/overlays/on_glow.pngand is recorded in the identity as"overlay"with"blend": "screen". -
two registered states → run_transition()
-
04c
Transitions whose endpoints are not opinions
farmer/states.py:198A transition strip has one obvious failure mode: the first frame nearly matches the start state and the last nearly matches the end state, so playing it snaps at both ends. The fix is mechanical. The endpoints are put into the template as actual pixels, the model is asked to reproduce them exactly, and then — after slicing — they are thrown away and replaced with the real state images anyway.
Figure 10 — endpoint locking. Strips are 2–6 frames ( states.py:204) and are packed withloop=False, because a transition is not a cycle. The percentage lines in the middle cells are generated asround(100 * i / (frames - 1)).Status, stated plainlyThe transition path is fully implemented and exercised end to end by
tests/test_states.pywith a mock provider — including the assertion thatframe_0andframe_3are bit-identical to the state images. It is not represented in the repository's shipped registry:objects/lamp/identity.jsonrecords"transitions": {}, andobjects/lamp/runs/transition-idle_to_on/contains onlyanchor-sheet.pngandprompt.txt— a run that was started and never finished, despite the changelog's P4 entry describing a finished strip. -
identity.json → pack.emit_juice() → juice.json
-
04d
Interaction feedback that is never art
farmer/pack.py:50Hover, press, release, invalid, disabled. None of these are generated as frames. They are emitted as a transform-space recipe anchored to the asset's stored pivot, so a runtime animates them and the art stays one PNG.
The lamp's recipe is derived arithmetically from its identity: pivot
(100, 339)on a201 × 341canvas givestransformOrigin: "49.75% 99.41%". Every transform in the file repeats that origin, so a press squashes the object into the ground rather than around its middle.objects/lamp/juice.jsonwritten by emit_juice(), regenerated on every state pass{ "object": "lamp", "pivot": { "x": 100, "y": 339, "convention": "bottom-center" }, "transformOrigin": "49.75% 99.41%", "press": { "scaleY": 0.92, "scaleX": 1.06, "duration": 0.09, "ease": "power3.out", ... }, "release": { "scaleY": 1.0, "scaleX": 1.0, "duration": 0.34, "ease": "elastic.out(1, 0.45)", ... }, "invalid": { "keyframes": { "x": [-4, 4, -2, 2, 0] }, "duration": 0.3 }, "emissive": { "on": { "layer": "overlays/on_glow.png", "blend": "screen", "pulse": { "opacity": [1.0, 0.72, 1.0], "duration": 2.4, "repeat": -1 } } } }Theemissiveblock is added for any state whose identity entry carries anoverlay, binding the pulse to the separated glow layer rather than to the object itself. Note the scope:emit_juice()is called fromadopt_object()andrun_state()only — characters never get a juice file.Registry shapeEach registered asset owns an
identity.jsonholding the anchor path, canvas spec, bottom-centre pivot, a ground footprint taken from the bottom 12% band of the subject bbox, a style pointer, and the states, transitions or loops ledger. Three declared fields —sockets, and for sceneslightRigandslots— are written as empty lists byregistry.pyand never populated by any code path in the package.