A video used to run on its own clock as soon as a sprite wore it. An
animation could switch the sprite on and off, but never say which
picture shows at which frame. Cutting a video in an animation needs
the opposite: the animation sets the time and the video follows.
- Sprites wearing a video or a clip get two rows, Video clock and
Video time. They show only while the texture holds a video.
- With Video clock on Driven the video does not play by itself. It
shows the picture at Video time, counted from the clip's start.
Past the end it goes round with Loop and holds the last picture
without.
- Video time is a number like any other, so keys, step, easing and
curves give stills, slow motion and ramps.
- A driven sprite gets a player of its own, keyed by the wearer's
name. The same clip can run twice at different times.
- Following has two ways (`videoFollow.ts`). Running forward at 0.25x
to 4x the video plays at that rate and is nudged. A still, a scrub
or a step backwards seeks, and only the newest seek counts.
- Sound is heard only near 1x. The animation tab now plays it too, so
a cut can follow the sound. Scrubbing is silent.
- Scene, game view, the inspector's PREVIEW and the shader preview
show the driven picture. In a running game Play animation sets it.
Found on the way:
- After a scrub the video in the animation tab kept running. The
preview redrew only while the playhead moved, so nobody asked for
the video once it stood. `AnimationEditor.tick` now redraws it every
frame while it shows a video.
- Every sprite has two more defaults (`videoClock: 'free'`,
`videoTime: 0`). Eight tests in five files pinned the old list and
are updated.
Tests:
- Unit: video-clock and video-follow new, 4 files with 42 tests green.
- e2e 66 (melektaus-atx): ALL PASSED, 22 checks, 3 new. 66i: two
sprites on one clip stand at 0.2 s red and 0.7 s green. 66j: a
scrub to frames 2, 7, 12 and 3 shows red, green, blue, red, and
each colour still stands 0.7 s later. 66k: playing runs 10-30 ms
behind at rate 1.00-1.03 and is not muted.
- Counterchecks: a seek that keeps the rate turns the seek test red.
Not seeing Driven turns 66i, 66j and 66k red.
- Gate (melektaus-atx): exit 0, 297 files, 4546 tests.
- Not tested in the browser: Play animation in a running game.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A camera could only show the whole scene, so a HUD drawn in the world
would run through a camera shader (#299) with everything else. Render
layers say which cameras see an object, and overlay cameras draw their
own picture over the main one.
- The project has a list of up to 16 render layers, edited in Project
settings next to the collision layers. It is a list of its own, read
by the same rules (`layerNamesOf`).
- Sprites, texts, tilemaps, particle sources and containers carry a row
`Render layer` under their switches. Its default `Inherit` takes the
parent's layer, at the top the first one.
- A camera has `Sees` (all layers by default) and `Overlay`. The main
camera is the first one switched on without Overlay, so two plain
cameras still draw through the first one only.
- Every view through a camera keeps only what that camera sees and lays
one see-through view per overlay camera over itself. That view is a
`WorldViewport` of its own, handed its view and frame from below.
- An overlay is fitted into the main camera's whole picture without
distortion. One of the same size and zoom covers it exactly.
- `z` sorts within one camera only. The pictures lie on top of each
other as a whole, which is what a shader per camera (#299) needs.
- The Game tab's preview now collects the scene once per redraw. Before
it collected it twice, with the cameras it would have been five times.
- `SetCollisionLayersCommand` became `SetLayerNamesCommand`, which
takes the list it sets.
Follow-ups: #304 (the mouse through a chosen camera) and #305 (change
the render layer at run time).
Tests:
- Gate: 226 files, 3487 tests (3480 + 7)
- e2e 63 (new): 4 checks. A green overlay sprite at z -10 covers a
red main sprite at z 1000, in the preview and in the running game.
A `UI` sprite in front of the main camera and a `Default` one in
front of the overlay stay away. Without Overlay the second camera
draws nothing. The settings dialog adds a render layer at the end.
- Counter-checks: inheriting from parents switched off fails 1 unit
test. Overlays never drawn fails the two overlay checks in e2e 63.
- e2e total 1720 (1716 + 4), computed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Load scene destroyed every object of the old scene. A player, a HUD
or a music object had to be built again in each scene, with its state
pushed through shared variables. A top-level object with the new switch
Keep on scene load now travels into the next scene as the same entity,
with everything below it.
- The switch is a common property right under Enabled, off by default,
on the asset and on a placed object. The inspector shows the row on
top-level objects of a scene only, as answered in the issue.
- runtime/keptObjects.ts holds the rules as pure functions.
keptEntities picks what travels, objectsToBring copies the stored
objects as the run parents them, and bringObjects puts them into the
next scene.
- World.instantiate takes a handed entity as it is instead of making
one. The next syncToModel then writes the run's values into the
scene.
- BehaviorSystem.release takes the running instances out before the
old world is destroyed, and the next system adopts them. They hear
no On destroy and no second On start, and their sounds play on.
- PhysicsSystem.resume gives a dynamic body the velocity and spin it
had.
- Duplicates: the object brought along wins. The next scene does not
build its own object with the same id, which is the way back into
the scene the object came from.
- Decided here: the next scene does not build a top-level object of
the same asset that is kept itself either. The issue only names the
id. With the id alone a level that places its own player would hold
two after a swap.
- Decided here: kept objects land behind the scene's own, the switch
is no animation row, and collisions are found anew.
A finding, fixed along with it: Load scene did not build a scene as it
is stored, but as the run had left it. A moved object stayed moved and
a destroyed one stayed gone. The way back into a scene needs a clean
build.
- GameRun copies a scene's objects and groups before it first builds
it and puts them back before it builds it again.
- GameRun.end puts every such scene back. The player calls it on stop,
so spawned objects no longer stay for "Click to play again".
- The consequence: an edit made in a scene during Play is gone the
next time that scene is loaded.
Tests:
- Gate: 199 files, 2944 tests (2929 + 15), exit 0
- e2e, full run: 1625 checks passed (1619 + 6), exit 0
- Smokes: 24 / 19 / 21, exit 0
- 15 unit tests in tests/core/keep-on-scene-load.test.ts
- Six e2e checks in module 35: the switch is set with a real mouse,
the child has no row, the object is in the new scene, its counter
still reads 1, its sound plays on, and the stored scenes are
unchanged after Stop
- Eleven tests with fixed row lists only count the new schema row
- 16 unit counter-probes all fail tests, run on stein2. One did not
fail at first (same id not left out), it has its own test now.
- The browser counter-probe fails 4 of the 6 new checks
- Not looked at in the browser: the switch as a port of the Properties
node, and a released game with a kept object
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An animated or set Z of -1 on a weapon put it behind everything on Z 0,
not just behind the player holding it. A child can now opt in to being
drawn at its parent's place, so its own Z only orders it against the
parent and its siblings.
- New property "Sort with parent" on sprites, texts and particle
emitters, shown in the inspector right under Z. It is a normal
property, set on the asset and overridable per object.
- With it on, the rest of the scene sees only the parent's Z and, where
the layer sorts by Y, the parent's feet. Below 0 the child is drawn
behind the parent, from 0 on in front.
- It works across levels: a grandchild with the switch hangs on the top
object of the chain.
- It has no effect under a parent that is not drawn itself (container,
behavior, camera, collider) or under a tilemap.
- New assets start with it on, saved ones read as off, so old projects
draw unchanged. For that a schema row now has "initial" next to
"default". The gold "changed" mark and reset follow "initial", so a
fresh sprite is not marked as changed. No load migration is needed.
- parentGroups() in drawOrder.ts gives a slot only to the top of a
chain and inserts the children there. Editor, play mode and picking
all use it, so they agree.
Tests:
- 10 new unit tests in tests/viewport/sort-with-parent.test.ts.
- 8 existing tests list the sprite rows or defaults and got the row.
- New e2e check 5b-ii clicks the switch and checks order and undo.
- Counter-checks, all originals restored by cmp: switch ignored fails
7, children not sorted by Z fails 6, no chain fails 1, "initial"
ignored fails 9, row missing in the inspector fails e2e 5.
- ./check.sh: 166 files, 2426 tests, CHECK EXIT 0.
- e2e 5: ALL PASSED, 13 checks. Check total 1398, computed only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four new rows on sprite, text, collider and container (block TRANSFORM):
scale, rotation in degrees clockwise, mirrorX, mirrorY. As properties they
get override/reset/apply, loading of old projects, write-back after play and
the Properties node for free; camera and behavior have none.
The hierarchy composes T*R*S per level (composeAlongChain replaces
sumAlongChain), so children rotate and scale with their parent. Reparenting
and grouping keep the look (carriedTransform). Drawing goes through
placeNode, the gizmo is a polygon, cull and objectRect use the AABB, drags
are computed in the parent's system.
Colliders are oriented boxes: SAT for box/box, box-circle in the box frame;
exitDistance replaces axisSeparation/axisCorrection, moveAndSlide moves along
the object's own axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A shader is an asset carrying a node graph (Texture color, Output, Property,
Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL
fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite
wears a shader through its Shader picker, and the rows below it are the
graph's Property nodes. Property schema, resolution and deviation take the
project because a sprite's rows now live in another asset. The built-in
registry of the prototype is gone; "Adjust colors" ships as a graph in the
fixture and the three example games.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A reference search over every export and method in src/ found code that
only tests still called, or that was never called at all:
- RemoveSceneObjectCommand: the product only removes through the
multi-object command; the tests now use that one too.
- NodeRegistry.requireNodeDef: a second function of the same name in
commands/require.ts raises a CommandError instead of throwing, and is
the one the product uses. The twin invited the wrong import.
- isTextureAsset, listContainers, AssetTabEditor.show,
FocusManager.destroy, KeyboardBridge.selectAll and the getters
ColorField.value, ListView.selected, NumberField.number.
Also: Theme.controlActive now matches the resting control colour, and a
new note asks what editing the graph during play would take.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Images are uploaded to a content-addressed blob endpoint and stored as
data/textures/<sha256>.<ext>; the project only keeps the file name. The
sprite points at a texture asset through an ordinary property of the new
kind `asset`, so instance overrides, "Apply to container", "Reset to
asset", undo and `Set property` work without any extra code. The new kind
also replaces the ad-hoc asset picker the graph nodes faked as an enum.
Import goes through + -> Texture, drag & drop onto the canvas or Ctrl+V.
Images wider than 2048px are downscaled client-side and re-encoded to
WebP if needed, because nginx caps uploads at 1MB; an image that already
fits is uploaded byte for byte so pixel art is never resampled.
The sprite colour default changes from red to white, since colour tints
the texture multiplicatively and a tint of white shows an image as it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
measured against the existing style); Markdown excluded because table-cell
padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
"this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
tests in one go.
Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.
Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
events now; the DOM handlers are thin adapters.
Compare fell back to == without a configured operator although the schema
default is <; aligned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Engine additions that the example game needed:
- `enabled` as a shared property of every placeable asset (default on, an
override on the scene object). A switched-off object is not drawn, does not
collide and its behavior does not run - and neither does anything parented
below it. Disabled objects are now hidden in the scene view as well, so the
editor shows what the game shows; they stay in the hierarchy panel, which is
where they are selected and switched back on.
- Asset type `text` (text, fontSize, color, align), rendered in scene and game.
- Pointer input in the game view: `On click`, `Mouse position`, `Mouse button`,
`Is hovered`, hit-tested against colliders, else sprite/text footprints.
- New nodes: Set/Is enabled, Set text, Is asset, Name of, Equals, To text,
Join text, Floor/Ceil/Round, Modulo, Random, Time, Restart game.
Tooling: shared CDP harness (tools/harness.mjs), central project registry with
a guard that keeps destructive scripts off the playground (tools/projects.mjs),
plus smoke suites for the demo scene and the jump-and-run game.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Position is now { x, y, z }; higher z draws on top, ties keep hierarchy order
- Viewport sorts the objects layer deterministically (syncDrawOrder)
- Inspector shows a Z field; asset properties that differ from the schema
default are marked bold with a reset button (ResetAssetPropertyCommand)
- isModified() helper, unit tests, E2E checks and seed updated
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Canvas-only 2D game editor/runtime built on PixiJS v8 + Vite + TypeScript.
- core: asset model (Scene/Camera/Sprite), scene objects with sparse
overrides, property schema + resolve, command stack (undo/redo),
serialization and project store interface (16 unit tests)
- ui: custom in-canvas widget toolkit (panels, fields, color picker,
list/scroll, popup) with a hidden <input> keyboard bridge
- editor: asset/hierarchy panels, schema-driven inspector, toolbar
- viewport: box rendering, camera frame, selection gizmo, drag/zoom/pan
- runtime: world/entity instantiation for play mode, input, camera follow
- api: PHP JSON project store with revision counter (409 on conflict)
- tools: CDP-based e2e smoke test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>