A graph could start, pause and stop an animation and hear it finish,
but it could not ask in between whether one runs or how far it is.
Two data nodes answer that now.
- Animation playing takes an object and an animation and gives
playing, progress and time left (s). It answers at the moment it is
read: right after Play animation in the same pass it says on, and in
the frame On animation finished fires it says off.
- With Animation at None it answers for the running animation started
last on the object.
- Animation length gives the seconds of one pass, from the timeline
and the frame rate alone.
- The rule is a pure function in src/core/model/animation.ts
(playerStatus, leadingPlayer). The nodes reach an animation's
length through a new field animation on GraphLookup.
- animationPort is exported so both nodes show the same picker as
Play animation.
Decided in the issue: a Once (reset) counts as running while it blends
out, and progress and time left also hold for Loop and Ping-pong, at
the place of the playhead. On Ping-pong progress falls again on the
way back and time left counts to the next turn.
Decided here, the issue says nothing on it:
- A paused animation does not count as running. Progress and time
left stand still.
- One that Stop animation fades out does not count as running, and
its time left is 0.
- Time left of a Once (reset) includes its blend out. While it blends
out progress is 1 and time left is the rest of the fade.
Finding, not fixed: on the node the name of the chosen animation
overlaps the output label progress in the same row. That comes from
the fixed node width in NodeView and was seen in the Fit view only.
Tests:
- Gate: 198 files, 2903 tests (2891 + 12), exit 0
- e2e 1-6: 282 checks passed, 06 has +2 for block 6z-ix
- Nine unit counter-probes fail, 1 to 4 tests each
- The browser counter-probe fails 6z-ix with ["false","false"]
- e2e total 1615 is computed, not measured in a full run. The smokes
were not run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A behavior variable switched private is hidden from the placed object
and from the Properties node, but the row dialog of an animation kept
offering it. Rows that exist already stay as they are, as decided in
the issue.
- offeredDefs in src/core/model/animation.ts is what the dialog offers
now: what the target can animate, less private variables and less
what another row animates already. The filter moved there from
RowDialog.ts, so it can be tested without the editor.
- animatableDefsOf is unchanged. The label and the runtime read it, so
a row on a variable that went private keeps playing and keeps its
label.
- Deviation from the proposal: under Change target the row's own
variable stays on offer, private or not. The dialog would open with
no property and a disabled button otherwise. Other private variables
are not offered there either.
- The command does not refuse a private variable. Only the dialog does
not offer it.
Finding, not fixed: e2e 56 fails when it runs alone, in 56a-ii from
#257 (typing "9" did not arrive: focus is BODY). With 55 before it the
module passes, so a full run does not show it. It was not measured on
HEAD without this change.
Tests:
- Gate: 198 files, 2883 tests (2882 + 1), exit 0
- e2e 55,56: 27 checks passed, 56 has +56e
- e2e 25: 31 checks passed, it uses the same dialog
- Three unit counter-probes each fail the new test
- The browser counter-probe fails 56e, mood shows up in the dialog
- e2e total 1610 is computed, not measured in a full run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recurring cleanup round over the code since round 7 (bc688f3). The
issue now also asks to fix bugs and small inconsistencies found on the
way, so unlike the rounds before, this one changes behaviour in a few
places. Duplicated code in api went from 23 clones with 228 lines to
none (jscpd, at least 50 tokens). In src it stays at 22 clones with
271 lines, the same before and after.
Bugs:
- nginx served project and snapshot files as they were, so anyone who
knew a project id could read it without signing in. Every file the
API writes now goes through AtomicFile (temp file, mode, rename).
Projects, snapshots, releases, accounts and sessions are 0640, and
nginx answers 403 for them.
- Any signed-in account could delete a shared image, font or sound,
which projects of other accounts may wear. Deleting one is now for
administrators only. The editor never deleted a file, so the
client's delete is gone.
- Two saves of one project could run at once. The lock file was
removed after each save, so a waiting save and the next one locked
different files. The lock file now stays, and deleting a project
takes the lock as well.
- Bytes that are no picture got a 500 instead of a 422, because the
error handler also threw for warnings silenced with @.
- A session file was rewritten in place. A request reading it at that
moment found it empty and was signed out. It is replaced atomically.
- Delete did nothing for a shape picked in the vector art list. Delete
now goes through removeSelection, one case per kind of selection,
and a kind without a case fails the type check.
Inconsistencies:
- Escape on a question does what its Cancel does. Cancel on "Delete
project?" and "Restore snapshot?" leads back to the picker or the
list like the other questions did, and so does deleting a project.
- Editor texts use American spelling throughout, like the port names:
48 British spellings in 45 lines (colour, centre, initialised).
- objectName and groupName replace eleven copies of "own name, else
the asset's" and three of "Group". One copy fell back to the object
id instead of "?".
Duplicates, dead code, comments:
- routeFiles serves images, fonts and sounds instead of three copies.
HashedFileStore holds what the three stores shared: keeping a file
under its hash with its name, listing and deleting. The stores keep
only which bytes they take and what a record says.
- AtomicFile replaces nine copies of temp file plus rename.
- removeShapes is the one way the canvas, the inspector button and
Delete take shapes out.
- GraphInterpreter.fire was called by tests only. The 54 calls use a
helper in tests/helpers.ts now.
- FileStore.delete, HttpFileStore.delete and the PluginApi type were
unused. A doc comment in actions.ts sat above the wrong function.
Checked and left as is:
- solidCells is used by tests only. It is the per-cell view before the
merge into rectangles, the one place the largest-scale rule shows.
- An undo checker ran all 124 commands of the unit tests and compared
the project before and after undo. The only differences are empty
ports, groups and notes lists where there were none, which the file
format drops anyway.
Docs:
- README: the check sum.
- Wiki, own commit: API (admin-only deletes, 422, file modes, lock
file), Home (the "Not yet" list named three things that exist),
Concepts, Keyboard, Vector art, Architecture and Development, and
the 38 characters Gitea flagged as ambiguous on eight pages.
- .gitignore: the lock files, and pasted-*.png in any folder, which
keeps the screenshot archive out.
Tests:
- ./check.sh: 195 files, 2817 tests, exit 0 (+2 for the name helpers).
- PHP: accounts 52 in the web container and 53 in the CI image with
the race test (+2), snapshots 15 (+1), releases 20.
- Full e2e run: ALL PASSED, 1586 checks (+6 in 12, 47, 49 and 50).
- Smokes jumprun, tetris, actor: 24, 19 and 21 checks. The example
project files were restored byte for byte afterwards.
- The file stores of HEAD and of this commit give byte-identical
output for the live folders and for store, list and delete in a
throwaway folder.
- Counter-checks, all fail as expected: a 500 for bad bytes fails 12,
no admin check fails 49, removing the lock file again loses saves
in 3 of 3 runs (151 to 170 of 321), Delete ignoring shapes fails 47,
Escape that only closes fails 50, and dropping the shape case fails
the type check (TS2366).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recurring cleanup round over the code since round 6 (056a622), 105
commits. Behaviour is unchanged. Duplicated code in src went from 29
clones with 229 lines (0.32 %) to 14 clones with 109 lines (0.15 %),
measured with jscpd and the same settings as round 6.
- BehaviorSystem.ts shrinks from 941 to 589 lines. Collecting the
pulses (keys, timers, collisions, animations) moved to
behaviorPulses.ts. The NodeContext the nodes receive moved to
behaviorContext.ts. Imports from outside stay the same, TimeScaled
is still exported from BehaviorSystem.ts.
- PointKindButton and drawPointGrip replace the "Point:" button with
its menu and the point and handle drawing that the curve and the
vector art tab each had on their own.
- lerpVec, cubicAt and splitCubic in src/core/util/vec.ts replace four
copies of the cubic curve and two de Casteljau splits.
- mapNested in Properties.ts is the one walk over lists and structs
that liveObjects and objectRefs did separately. withFieldRenamed
and withoutField replace three copies of renaming a struct field
and two of removing one.
- jsonBody and staleRevision build the JSON body and turn a 409 into
StaleRevisionError for all stores. The auth store turns a 401 into
its error in one place.
- Smaller helpers: structAssetFrom for the two "Make struct type"
commands, shownObject in the inspector, selectedItem for the size
gizmo and the polygon corners, solidTiles in the tilemap collision,
requireVariablesAsset and requireBehaviorVariable.
- Left as is on purpose: the plain forwarding from the collision and
physics systems to Solids, the pointer wiring of the canvases and
the identical last lines of the two make commands.
- Removed unused code: tilemapTileIds and tilemapsUsing were never
called since #128 and #131. EditorState.runtimeProject became unused
with GameRun in #266. The assetId getter of the make commands was
read only by a test.
- 60 values used only in their own file lost their export. The shader
and particle node catalogues stay exported as in round 6.
- Five comments that told history now describe the current state. The
doc comment of BehaviorSystem sits on the class again.
- README lists the struct, clip area and curve assets and mentions the
game link from #266.
Findings:
- Deleting a tile that is painted on a tilemap leaves its cells in
place, and they draw nothing. The delete dialog does not mention the
tilemaps. That was probably the job of the unused tilemapsUsing.
Left as is, because fixing it changes behaviour.
- The solid filter for tile surfaces was untested. A bent filter
stayed green. A new test in tests/core/surface.test.ts now fails
against it.
Tests:
- ./check.sh: 195 files, 2815 tests, exit 0 (one test removed with
tilemapsUsing, one added for tile surfaces).
- Full e2e run: ALL PASSED, 1580 checks.
- Smokes jumprun, tetris, actor: 24, 19 and 21 checks, all passed.
The example project files were restored byte for byte afterwards.
- Counter-checks, 12, all fail as expected: 10 in unit tests (among
them mapNested, splitCubic, cubicAt, staleRevision, the stay pulse,
the late timer and the time scale), 2 in the browser (Point button
always enabled fails 47, selectedItem without a hit fails 29).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A text could not wear a shader, and a pixel font was always smoothed.
Pixi's Text takes no custom shader. It also shares one texture between
texts of the same content and style, and the filter is not part of that
key, so two equal texts with different filters would overwrite each
other.
- TextPicture rasters a text into a texture of its own and draws it
like a sprite. Shader, time animation and edge mode work without a
special path. A new text, font or size rasters again and keeps the
shader.
- The old texture is dropped only after the shader holds the new one.
Before, Pixi warned about a destroyed texture that was still bound.
- A text has a Shader row with the shader's property rows below, as a
sprite has. Overrides, saving, animation and the Properties node
(type Text) know them. A shader parameter named like an own row of
the text loses to that row, as on the particle source.
- The font asset has a Filter row, Linear or Nearest. The row existed
three times already (texture, vector art, pixel art) and is now one
helper filterRow.
- Nearest rasters the text 8 times finer and cuts every 8x8 block to
one texel: at least half covered is full, else empty. Chrome on
Windows smooths canvas text sideways through DirectWrite whatever
filter we set later. The cut removes those half pixels on every
system, and the texture stays one texel per world unit.
Found on the way:
- On a font that is no pixel font thin strokes break apart under
Nearest. For a pixel font at its own size this does not matter.
Not done:
- The preview in the Shader tab does not take a selected text as its
picture, it shows the test picture then.
Tests:
- ./check.sh: 189 files, 2759 tests, CHECK EXIT 0.
- text-shader.test.ts with 10 tests: rows and name precedence, a
clamped override, save and load, animation, the font filter, and
sharpCoverage on a smoothed edge and at the texture border.
- e2e 30 has 7 new checks: red glyphs through the shader picked with
the mouse, the shader survives a new text, red in the game, soft
edges under Linear, clean 4x4 blocks under Nearest, and half covered
texels (Linear 238, Nearest 0).
- Full local run before the Nearest cut: all passed, 1543 checks.
GM_STEPS=1,30 on the final bundle: all passed, 32 checks. The sum
1544 in the README is counted from these two runs, not measured.
- Counter-checks fail as expected: in the browser no shader on a text
fails 30h-30j, an always linear sampler fails the Nearest check, the
old 1:1 raster leaves 107 half covered texels and fails 30k-ii.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The built-in eases cover the usual shapes, but a game often wants its
own: a coin that bounces twice, a door that overshoots and settles.
A new asset type holds such a curve, and keys and the Lerp node can
pick it instead of a built-in ease.
- New asset type "curve": points like in Vector art, x is t and y is
the value. A new curve is the straight line from (0, 0) to (1, 1).
- One pure evaluation in src/core/model/curve.ts serves the timeline
preview, the running game and the Lerp node.
- The curve always has one value per t. The end points stay at t 0
and 1, points cannot pass each other, and a handle that reaches past
its neighbour is shortened along its direction. Loading a hand-edited
file applies the same rules.
- Curve tab: click on the line to insert a point that keeps the shape,
click on free space to put one there, drag points and handles,
double-click or Delete removes, Alt-click toggles round and sharp.
Every step is one undo step.
- Presets: all 30 built-in eases plus Linear, fitted with adaptive
subdivision. Each stays within 0.4 % of the built-in ease.
- Preview: a dot runs along the curve once a second, a second dot
shows only the value. The Preview button stops it.
- Animation keys get "Curve" under Easing with a curve picker. Lerp
gets "Curve" under Ease, and its result may overshoot b.
- A missing or unpicked curve runs linear. The inspector says so, the
node shows it as a problem, and deleting a used curve warns with the
number of keys and Lerp nodes.
Tests:
- ./check.sh: 180 files, 2612 tests, CHECK EXIT 0
- GM_STEPS=1-6,25,53: ALL PASSED, 309 checks
- New e2e module 53 with 13 checks, all with the real mouse. Expected
values come from its own Bezier evaluation.
- Counter-checks bite: without Bezier evaluation 4 unit tests fail,
without handle clamping 2. In the browser, a key that cannot find its
curve, a click that sets no point and a handle drag that moves the
point all turn checks red.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The timeline offered X and Y as position rows but never Z. The list
of position rows was simply written without it when animations came
in with #97. A swinging weapon could therefore not go behind its
character.
- Z is the third position row, after X and Y, on every placed root
or object. A scene and a behavior still offer no position.
- Play and preview already read and write any position row by name,
so the one entry in POSITION_KEYS is all the code change.
- The draw order is sorted on every render, and play writes z back
into the scene each frame. A keyed Z therefore takes effect at
once. A blended 4.5 sits above 4 and below 5.
Tests:
- ./check.sh: 165 files, 2416 tests, CHECK EXIT 0.
- 3 new unit tests in tests/core/animation.test.ts: Z is offered
after X and Y and not on a behavior or scene, a running game moves
the object from behind to in front of another one, and play,
scene write-back and preview agree on z.
- e2e 25: ALL PASSED, 29 checks. The new check reads X, Y, Z as the
first rows of the row dialog. Check sum 1396 -> 1397.
- Counter-check: without z in the list all 3 new unit tests fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A key could only ease quadratically, which makes quick moves like a
sword swing feel soft. Animators know the Penner curves from CSS,
GSAP and Unity, so a key now offers all of them.
- Ten curves (Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back,
Elastic, Bounce), each in, out and in-out, next to Linear and Step.
- The inspector shows two fields instead of one long list: Easing,
and Direction only when a curve is picked. Switching the curve
keeps the direction. Coming from Linear or Step starts at in-out.
- Old projects keep working. ease-in/out/in-out now read as Quad and
are still saved under that name. New curves save as cubic-out,
back-in-out and so on.
- Back and Elastic overshoot on purpose. A number property with min
or max is clamped in play and preview, so a Back in from scale 0
does not flip the sprite. trackWriteAt now takes the property
definition, and animatedKind* is renamed to animatedDef*.
- The timeline marker still shows only the direction.
- In-out is built for every curve from its in-curve and its mirror.
easings.net uses own constants for Back and Elastic in-out, so our
Back in-out winds up about 5 % instead of 10 %.
Tests:
- ./check.sh: 165 files, 2413 tests, CHECK EXIT 0.
- 12 new unit tests in tests/core/easing-curves.test.ts: round trip
of all 30 values, start/end/midpoint of every curve, reference
values, only Back and Elastic leave 0..1, clamping in play and
preview, unknown values are rejected.
- e2e 25: ALL PASSED, 28 checks. New 25b-ii (3 checks) clicks both
fields and undoes the four choices. Check sum 1393 -> 1396.
- Counter-checks, each fails its test: no clamping (2), Quad saved
as quad-* (2), mirrored in-out wrong (2), curve lookup with `in`
instead of Object.hasOwn (1), no Direction field (e2e 25).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recurring cleanup round. Behaviour is unchanged. Duplicated code in
src went from 27 clones with 299 lines to 6 clones with 56 lines
(jscpd, the two identical OFL licence texts not counted).
- SheetCanvas is the new base of the texture, pixel art and vector art
canvases. It holds background, mask, hint, panning, wheel zoom, fit,
view save and restore and the screen mapping. Pixel art keeps its
whole-pixel panning and fixed zoom steps.
- mixColor in src/core/util/color.ts replaces three copies in
animation, gradient and particles. It clamps t like the particle one.
- fingerprint now lives once in generated.ts, next to the revision
contract from #185.
- MoveCommand next to RenameCommand is the base of the three reorder
commands for tile pictures, tilemap layers and vector shapes.
- The two raycast nodes share inputs, settings and filter via rayOf.
- canvasRect() lives once in AssetTabEditor.
- Removed unused code: findPixelArt, isDrawableShape,
GeneratedPictures.has and SnapToggle.snapping. Tests now check the
same things through equivalent paths.
- 38 values used only in their own file lost their export.
- Three comments that told history now describe the current state.
The bundle stayed md5-identical for those edits.
- README lists vector art, pixel art and palette, and links the two
wiki pages.
Findings:
- PaintTilesCommand.changes is meant to prevent empty undo steps but
has not been called since #128. The rectangle tool therefore records
a no-op undo step when it paints the same tile over itself. Left as
is, because fixing it changes behaviour.
- No test covered middle-click panning in any sheet tab before. 13f
and 48a now check it.
- The default values of the ray inputs were untested. Length 1000
could become 10 without a red test. tests/core/raycast.test.ts now
covers it.
- CI measured 1308 editor checks at d7c0b7c, not the 1307 counted for
#164. The sum is now 1310 (1308 + 2), computed.
Tests:
- ./check.sh: 144 files, 2143 tests, exit 0 (before 2142).
- e2e 1-16,37,39-43,47,48 fresh: ALL PASSED, 729 checks.
- e2e 1-13,47,48 with the new checks: ALL PASSED, 518 checks.
- Counter-checks, each restored via cp and cmp: halved panning fails
both new pan checks; unclamped mix fails 1 test, no blue channel 3;
coarser hash 3; broken reorder undo 2; triggers setting forced on 2;
bent ray length 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop animation gets a blend (s) port that fades the animation back to what
lies underneath. Play animation gets blend out (s) for Once (reset) and a
restart port that starts a running, paused or fading animation from frame 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Animation tab shows the root asset over the timeline, with every row's value at the
playhead written onto a copy of it – the sprite, text or container placed at the origin as a
scene would place it, a scene as itself. Rows reach the root through that placed object and its
children through the object's instance edits, the route a placed container's content takes,
so the ids match what the runtime gives them. The runtime player and the preview evaluate a row
through one function (trackWriteAt), relative rows included; past the end of a once-reset pass
the preview shows the starting values, as the player would. Play, pause and the ruler drive the
preview through the timeline's playhead; a drag pans, the wheel zooms, Centre frames it. The
document is never touched and undo never hears of scrubbing.
On animation finished no longer describes itself as hearing "a sprite of the owner".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Play, Pause and Stop animation take the animation on a port instead of a
fixed setting, fed by the new Animation literal or a list item tagged
`$animation`. Playing an animation already running keeps its frame but
counts it as started last, so turning back to a walk that never stopped
shows that walk. The actor demo walks through one Play animation over
`ItemAt(walk, facing)` again and stops every animation to stand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Part (a) of the animation rework: model, commands, saving, runtime and nodes.
- AnimationAsset holds a root (sprite, text, container or scene) and rows; a row keys one
property of the root or of an object inside it, keys sit on whole frames.
- The last key decides the length (its frame counts as a frame of its own). The playhead runs
between frames, so numbers and colors blend every tick even at 10 fps; everything else holds.
- Playback gains `once-reset`, which puts back what was there before the start. A row can be
relative, adding to where its target stood.
- Play / Pause / Stop animation act on an object; each instance plays on its own, the later
start wins on a shared property, and a behavior writing the same property still writes last.
- A sprite no longer wears an animation through `texture`. "Create animation from texture"
builds a sprite plus a texture row keyed frame by frame instead.
- The Animation tab shows a placeholder until the timeline editor (part b) lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `.gitignore` said `index.html` unanchored, which also caught the Vite source page;
a fresh clone could neither `dev` nor `build`. Anchored to `/index.html`, page added.
- Removed GM_REMOTE (tools/remote/gm-browser.ps1 and the ssh/scp branch in browser.mjs):
the Windows test machine is gone and the only flaky check that lived on it is closed (#37).
- Removed `npm run preview`, undocumented and unused.
- Comments: wrong number in collider.test.ts (250 → 200), the "declarations used to…"
history in listItems.ts and friends, the 24 px/28 contradiction, section banners in
nodes/index.ts, node-catalog.test.ts and api/index.php, name paraphrases, and the stale
step numbers in tools/e2e/11-…23- from the old global count.
- README: asset types include `animation`, kind order Number/Text/Boolean, tagged colour
and texture items, 6/5 kinds, the `Texture` node, `./build.sh`, PROJECT_FORMAT_VERSION,
and the file tree (kinds.ts, listItems.ts, propertyPorts.ts, Breadcrumb.ts, beads.ts,
tools/assets/).
- tests/helpers.ts takes the duplicated `findObject` and `sceneWithCamera`.
Proof: ./check.sh 797 tests, full e2e ALL PASSED 633 checks against this bundle.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An animation asset is a list of pictures (whole textures or frames) with
frames per second and Loop/Once/Ping-pong playback. A sprite wears it
through its texture property, so the picker, instance overrides and the
Properties node work unchanged; there is no dedicated play node.
A different animation starts from its first frame, the same one set again
runs on, so a graph may choose Run or Idle every frame. The clock lives on
the entity, not in props, which the world writes back as overrides. The
AnimationSystem ticks before behaviors, and On animation finished fires
once when a Once animation reaches its end.
The Animation tab plays a preview above a frame strip: reorder by dragging,
Delete, duplicate, + Frames, pause/resume, and wheel zoom about the pointer
with pan and Fit. Scene and Container tabs show the first frame; edit mode
has no clock. Right-clicking a texture creates an animation from its frames.
Button now measures its label with the bold font it draws in, which cut
the Animation tab and red dialog buttons short.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>