Commit Graph
516 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 b2b4cbad28 Keep a video's loading out of recordings and first frames (#417)
game-maker / gate (push) Successful in 4m21s
game-maker / suite (push) Failing after 17m16s
A Start recording right after Play composition still began with
empty frames. The composition's clock waited for its video, but the
recording went on and took every frame while the video loaded. A
clip that starts later in the video also slipped through, because
its player already had one picture while it still sought to the
clip's start.

- The view now counts every video sprite it leaves out as missing,
  not only one without any first picture.
- GameRecorder takes no frame while the game view misses a video,
  for at most VIDEO_WAIT_MS (3 s) at a time. The recorded time
  stands and the sound of that time is dropped, so it stays in sync.
- RecordingClock.start forgets a hold from before the first frame.
  Without that, the first real frames all got the time 0.
- A followed player starts at 1x until the first speed reading.
  Before, it paused and sought every frame for about 100 ms, so the
  first pictures after a wait stuck and then jumped. A step faster
  than 4x and a time running backwards do not count as a start.
- Test hooks: __gm.gameVideo(id) reads the game view's own player,
  TakeInfo.lastFrameAt shows when the last frame was taken.

Tests:
- Unit recording (clock after an early hold), video-follow (2 new,
  1 adapted): 449 green in tests/core/recording, tests/viewport and
  tests/core/composition*
- e2e 68e (a clip from 0.8 s shows that picture first and then
  plays), 68f (a recording that already runs takes no frame while
  the composition's video loads)
- 68e is red without the start at 1x, 68f without the hold
- README: 2038 editor checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:38:37 +02:00
adminandClaude Opus 5.5 6e6b6a4d9a Wait for a composition's videos and copy and paste its items (#417)
A composition lost the first frames of a video item. Its clock ran
while the player was still loading, in the tab and in the game. The
game only waited for videos once, when it started. A composition that
a behavior starts later spawned its items after that wait.

- The view now records which video sprites it left out for lack of a
  first picture, one entry per object.
- The composition tab holds its playhead while the preview misses a
  video, for at most VIDEO_WAIT_MS (3 s) like the game.
- CompositionSystem holds a composition's clock while one of its live
  items misses its video. The rest of the game keeps running.
- The editor play and the published game pass their view to the run
  as RunServices.pictures.
- VIDEO_WAIT_MS moves to core/model/video.ts, because the runtime
  does not import from viewport/.
- Videos loaded ahead for coming items do not hold the clock.

Items can now be copied and pasted inside the composition tab.

- Ctrl+C keeps the timing of the copied items next to the objects.
- Ctrl+V pastes objects and items in one undo step. The earliest item
  lands at the frame under the pointer, on the track there.
- Without the pointer over the tracks it lands at the playhead, on the
  selected track or on the top track of the selected items.
- Several items keep their distances and tracks to each other.
- The same copy pastes into a scene as plain objects. Objects copied
  from a scene do not paste into a composition, they have no timing.

Tests:
- Unit composition-edit, clipboard, composition-play: 3 new for
  pasting, 1 new for the clock waiting in the game
- e2e 69i (play from the end waits for the video), 69j (paste under
  the pointer and at the playhead), 68d (a composition started from
  On start waits for its video)
- each new check is red without its fix
- README: 2036 editor checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:38:19 +02:00
adminandClaude Opus 5.5 c4650e43f0 Let composition items overlap and show where their video lies (#417)
Trying the tab turned up six things to fix. Items of a track could
not overlap, so a drag stalled as soon as the pointer reached another
item. An item's ends gave no hint they could be dragged, a trimmed
video showed nowhere, clip names were hard to read and vanished when
picked, and the track menu did not match the song tab.

- Items of a track may overlap and nothing is cut or pushed aside.
  The later one lies in front, in the picture and on the timeline.
  The overlap is shaded and a line marks the covered end.
- Edges win over bodies when hit, so the covered end of the lower
  item can still be dragged. Hovering an edge lights it up and shows
  the ew-resize cursor, as in the song tab.
- A clip in the overlap plays on both items. Clips themselves still
  do not overlap; a moved clip now runs up to its neighbour instead
  of stopping short.
- Fit to media and a new video no longer cap the length at the next
  item, which only made sense while overlaps were forbidden.
- The left edge can be trimmed past the start of the video. The
  offset goes negative and the sprite shows the first picture until
  the video begins.
- Video marks: a corner triangle where the item cuts into its video,
  a line with a triangle like the song loop where the video starts or
  ends inside the item. Edges snap to the video's ends within 6 px.
- Clip names sit in their own layer above the bars, in text colour.
- The track menu matches the song tab: add above or below, move up or
  down, delete. It opens on the head and on an empty spot of the
  track. The Remove track button left the inspector.

Tests:
- Unit composition and composition-edit: 67 green, several new or
  rewritten. Bending three spots in the rules turned their tests red.
- e2e 69: ALL PASSED, 12 checks. 69b and 69c now drop the second item
  over the first and drag the covered end past it. Without edges
  winning the hit, 69c fails with hover: null.
- Gate on melektaus-atx: CHECK EXIT 0, 302 files, 4672 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:57:26 +02:00
adminandClaude Opus 5.5 4f0272638e Load the next item's video before a composition cuts to it (#417)
Two sprites with videos back to back left a blank frame at the cut.
The video player only came into being once the item began, and a
sprite is not drawn until its video has a first frame. So the second
item was missing until its video had loaded.

- compositionAhead lists the items that begin within the next second.
  Near the end of a looping composition it also lists the items at
  its start.
- An item that is alive already is not loaded ahead. Two times would
  otherwise pull at the same player.
- The frame carries these videos hidden and paused on their first
  picture at the item's offset. Their players get the id the item
  will have, so the cut reuses them instead of opening a second one.
- This works the same in the tab's preview and in the game.
- One second of lead (AHEAD_SECONDS) is an assumption. It can go up
  if large videos still flash.

Tests:
- Unit composition: 27 green, 2 new. Unit composition-play: 14
  green, 1 new for the path in the game.
- e2e 69: ALL PASSED, 12 checks. The new 69h puts two video items
  back to back and checks the second player is open and paused at
  its offset before the cut, and drawn in the same redraw as the
  cut.
- Without loading ahead in the preview 69h fails with cut: null.
  Bending two spots in the model turned their unit tests red.
- Gate on melektaus-atx: CHECK EXIT 0, 302 files, 4665 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:07:46 +02:00
adminandClaude Opus 5.5 e435c54afb Edit a composition's tracks, items and clips in its own tab (#417)
A composition could be placed and played, but not filled. Its tab
puts the picture at the playhead above a timeline of tracks, so a cut
or a cutscene is laid out by dragging assets onto tracks.

- The timeline takes the song tab's geometry with frames for ticks:
  ruler in seconds, scrubbing, play and pause, + Track. Zoom and pan
  glide through viewTween like the other tabs.
- A placeable asset dropped on a track becomes an item, 3 s long or
  as long as its sprite's video. Dropped on an item it lands behind.
- Items move without overlapping. The right edge stops at the next
  item. The left edge trims into a video and not before its start.
- An animation dropped on a track becomes a narrower clip bar above
  the item it plays on. The asset panel now starts a drag for
  animations too.
- The picture is a scene view over compositionHolder. A click picks
  an item or its child, a drag moves the real object. It takes no
  assets, since an object there would have no item.
- The hierarchy lists the items alive at the playhead. Reparenting is
  off there, items stay at the top level.
- The inspector shows a TIMING block on an item (start, length,
  offset for a video, Fit to media) and edits tracks and clips.
- Delete, the context menu and Remove item take item and object in
  one undo step.
- fitMedia: an override that gives an item another video sets its
  length in the same undo step, capped at the next item. Dragging an
  edge or typing a length turns it off.
- A composition inside a composition is refused.

Tests:
- Unit composition-edit (new): 19 tests. Four counter-checks each
  turned their test red. The "back to 3 s" test got a step in
  between after one stayed green.
- e2e 69 (new): 11 checks, 9 own. The hierarchy check reads the rows
  shown, after a counter-check on the model's rows stayed green.
- Gate on melektaus-atx: CHECK EXIT 0, 302 files, 4662 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:47:22 +02:00
adminandClaude Opus 5.5 0334d2508d Place compositions in scenes and play them from behaviors (#417)
A composition can now sit in a scene like a container and play there.
Its items become real objects while they run, so behaviors, colliders
and events work on them as on any other object.

- A composition is placeable with its own symbol, transform and render
  layer. The scene view shows its picture at frame 0. A click on that
  picture selects the placement, not a single item.
- CompositionSystem spawns each item as an object under the placement
  when it begins, with the id <placement>~<item>. On start and the
  other events run. The object is destroyed when the item ends.
- The top track lies in front no matter which item started first.
  World.arrange() orders the entity list and the scene objects.
- Clips and driven videos follow the same rules as compositionAt().
  Loop, restart, stop and the game pause work.
- New nodes: Play composition (with restart), Stop composition and the
  event On composition finished. They were planned for step 4 but come
  now, because without autoplay nothing could play otherwise.
- The editor run ignores Play composition and says so in the log. It
  writes into the saved project, and the items would stay behind.
- contentId() moved to instance.ts. The animation system uses it for
  nested ids too.
- Compositions get no "Sort with parent" for now.

Tests: the gate passed with 301 files and 4643 tests, 18 more than
step 1. 13 of them are in tests/core/composition-play.test.ts. The
first gate run failed in event-order.test.ts, which counts the event
nodes and now finds 19. Five counter-checks each turned exactly their
test red. Two order tests stayed green without the fix at first and
were rebuilt. e2e 68 is new and passed with 5 own checks. Its
counter-check without arrange() failed as expected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:06:33 +02:00
adminandClaude Opus 5.5 73e675835e Add the composition asset with its tracks, items and clips (#417)
A composition is a timeline of scene objects for cuts and cutscenes.
This first step brings the model and the pure evaluation behind it.
The tab and placing it in a scene follow in the next steps.

- CompositionAsset holds objects, tracks, items and clips. Each item
  owns exactly one object with overrides and container edits.
- The composition is a third object holder next to scene and
  container. Delete cascade, undo, usages and references work with it.
- Duplicating gives the copy new object ids and moves the item ids
  along with them.
- compositionAt() tells which items live at a frame. The top track is
  drawn in front.
- A clip acts on the item of its own track that it overlaps. After its
  end it holds the last value until the item ends. The animation's
  play mode is ignored, a clip is one pass.
- Clip targets are found by template path, else by name path, like
  the runtime's animation system.
- The root sprite of an item plays its video driven at offset plus
  time. Children follow unless they are already driven by hand.
- fittedLength() gives the video or clip length minus the offset, or
  3 s without media.
- fps (60) and loop live in their own properties file. Keeping them in
  PropertySchema would close an import cycle.
- The type is listed under Visuals with its own icon. It is not
  placeable yet.

Tests: the gate passed with 300 files and 4625 tests. That is 25 new
tests in tests/core/composition.test.ts and 2 icon checks that run
for every icon. Four counter-checks (track order, overlap rule, child
videos, ids on duplicate) each turned exactly their test red. e2e 04
passed with 67 checks and now counts 33 asset types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:36:30 +02:00
adminandClaude Opus 5.5 597367c804 Keep a transition's pace through new steps and pans (#309)
A wheel step in the middle of a transition restarted the ease-out
curve at full speed, so a run of quick steps kicked at each one.
A pan called finish() first and made the zoom jump to its target.
Both made the zoom and the gliding lists feel jerky.

- A new step starts from the speed the view already has and then
  eases out. Quick steps in one direction move in one stroke. A step
  the other way still starts from rest.
- A pan takes the transition along. The zoom goes on while dragging
  and lands where it was heading, moved by the pan. This covers all
  eight pan places: scene and container, graph, plugin board,
  texture and pixel art, timeline, pattern, song and sound.
- finish() is gone, it has no callers left. Dragging the scrollbar,
  a jump to a row, another document or a new view size still end
  the transition as before.

Found: on a second step the old curve jumped to 2.47 times the
speed the view had just before.

Tests:
- npm run check on stein2: 299 files, 4598 tests (5 new)
- e2e 46 on stein2: ALL PASSED, 24 checks (1 new). A pan by 40/30
  px during a 1000 ms zoom step leaves it moving (2.33 on the way
  from 2 to 3), it lands on 3 with the world point moved by the pan.
- Counter-checks: with the pace reset to 3 the new unit test fails
  with 2.47 instead of at most 1.1. Without carry the pan test fails.
- melektaus-atx went offline mid-run, so both ran on stein2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:08:43 +02:00
adminandClaude Opus 5.5 322fc39d16 Zoom the video tab's time smoothly like the other tabs (#377)
game-maker / gate (push) Successful in 4m41s
game-maker / suite (push) Successful in 17m13s
The video tab jumped on every wheel step and on Fit. The Sound,
Pattern and Song tabs already ease the zoom since #377. The video
tab now takes the same transition, so all tabs feel alike.

- VideoCanvas runs wheel and Fit through `axisTween`. The duration
  comes from the General settings. Quick wheel steps add up, panning
  ends the transition, and the view that is stored is the one the
  transition lands on.
- Opening a video still fits at once without a transition. Only the
  Fit button eases, as in the Sound tab.
- `__gm.videoTab.zoom()` reports the zoom and whether a transition is
  on its way.

Tests:
- e2e 66 on melektaus-atx: ALL PASSED, 30 checks (1 new). The new
  check uses `sampleZoom` like the Sound tab. At 1000 ms the zoom is
  on its way two frames after the wheel (319 -> 321) and lands where
  the jump does (370.63). No counter-check against the old code.
- Gate on melektaus-atx: CHECK EXIT 0, 299 files, 4586 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:52:29 +02:00
adminandClaude Opus 5.5 e191dc4b2e Record the game view from a behavior into a video asset (#405)
game-maker / gate (push) Successful in 4m14s
game-maker / suite (push) Successful in 17m7s
A game can now film itself. A behavior starts and stops a take, and
the editor turns it into a video asset with the run's picture and the
sound the player hears. The exported game hands the file over as a
download, since there is no project to keep it in.

- New action nodes `Start recording` (Name, Resolution, FPS) and
  `Stop recording`. An empty name becomes `Recording <timestamp>`.
- Resolution is `Game view`, `720p` or `1080p`. For the fixed sizes
  the height is set and the width follows the camera.
- The picture is the camera only, without hints, overlays or the
  letterbox. It is drawn into a render texture of the target size,
  so a smaller window still gives full 1080p.
- `PictureReader` reads the texture back through pixel buffers with
  a fence. The editor never waits on the GPU, and a frame with all
  three buffers busy is dropped and counted.
- Nothing of the take reaches the visible canvas. A first version
  drew tiles into it and copied them out, and with the low-latency
  canvas that midway state flickered on screen.
- The sound is tapped behind the master limiter in the FX worklet
  and encoded as Opus.
- H.264 is chosen when the browser has it, VP9 otherwise. A small
  own Matroska writer packs both tracks, no new dependency. It names
  the frame rate and BT.709 so ffmpeg keeps both.
- A second `Start recording` is ignored with a warning in the log.
  A take goes on through `Load scene` and a restart, pauses with the
  editor and is saved when the run ends. It stops at 2 GB.
- The editor uploads the take on the existing video route and makes
  the asset once the run is over. The video tab gets a Download
  button for the converted MP4.
- `downloadBlob` in `src/ui` replaces the song editor's own copy.

Findings:
- `On start` runs before the game screen knows the world. A take
  therefore fixes its size on the first frame that has a camera.
- Without a frame rate in the file, ffmpeg turned 30 fps into 59.94.
- The worker's poster JPEG is off in colour for every upload (red
  255 comes out as 229, BT.601). Not touched here.

Tests:
- Unit recording + matroska: 21 green, plus the node catalog cases.
  The Matroska test reads the file back with its own EBML reader.
- e2e 67 (new) on melektaus-atx: ALL PASSED. 1080p30 from a
  1280x800 window gives a 1920x1080 MP4 with sound and no dropped
  frame. Every 4th pixel is checked, a green quarter catches an
  upside-down picture. A take ended by Esc is saved with the
  timestamp name.
- e2e 60j: the exported game downloads an `.mkv` with EBML magic.
- The flicker is gone on the user's machine without `?vsync`.
- Gate on melektaus-atx: CHECK EXIT 0, 299 files, 4586 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:11:05 +02:00
adminandClaude Opus 5.5 36ffd9a61a Land a driven video's seek without a frame so it is never empty (#404)
game-maker / gate (push) Successful in 4m9s
game-maker / suite (push) Successful in 17m18s
Since b9fcf48 a seek of a driven video only counted as done once
Chrome handed over the next frame. For some seeks Chrome hands over
none. The player then waited for good, the view left the sprite out
after 0.3 s, and the picture stayed empty until the next jump.

- A seek now lands after `seeked` with the next frame, or after
  150 ms at the latest. The worst case is a picture 150 ms late.
- The landing is set up in the `seeked` handler instead of `seek`.
  A frame from before the last seek of a chase no longer counts.
- `__gm.videos()` reports `landing`, so an empty picture can be told
  apart from a player that is still loading.

Findings:
- The empty picture could not be reproduced in Chrome headless.
  The test video is 320x180 and decoded in software. The user's
  videos are 1080p60 H.264, likely decoded in hardware.
- The user confirmed the fix in the editor.

Tests:
- Unit video-library + video-follow: 20 green (1 new). The new test
  plays a video that hands over no frame after a seek. With the old
  code it stays not ready, so it fails.
- e2e 66 on melektaus-atx: ALL PASSED, 29 checks. The new 66r cuts
  LATE ten times quickly and was also green with the old code.
- Gate on melektaus-atx: CHECK EXIT 0, 297 files, 4563 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:04:40 +02:00
adminandClaude Opus 5.5 8118b8042b Let Texture size feed the Vertex output to scale about the middle (#409)
game-maker / gate (push) Successful in 4m16s
game-maker / suite (push) Successful in 17m10s
A scale about the picture's middle moves each corner by its distance
to the middle times (scale - 1). In pixels that distance needs the
picture's size, and Texture size was locked at the Vertex output as a
per-pixel node. So the size had to be typed in by hand and was only
right for pictures of exactly that size.

- At a corner Texture size reads the uniform uPictureSize. It holds
  the size the Vertex output counts its pixels in, the same number the
  node gives in the fragment. The offset is divided by that size again,
  so a scale comes out right for every picture and sprite size.
- A node can say what it is at a corner through emitPerCorner. The
  walker takes it there and emit everywhere else.
- Fed a Pass's picture, Texture size stays locked at the Vertex output.
  A Pass has no picture at a corner.
- 9-slice, field and camera shaders do not move corners. Their size is
  0 there and nothing changes for them.

Tests:
- Unit shader-vertex: the node compiles at a corner to uPictureSize
  and the Output still reads textureSize. shader-passes: a Pass's
  picture into Texture size at the Vertex output is refused.
- Against the old perPixel flag the new unit test fails.
- e2e 28l-iv: width * 0.5 at x moves the 32 px picture as far as the
  constant 16 did, same three colours. With the size forced to 0 on
  the GPU the check fails and the picture stands still.
- The case "Texture size is refused at a corner" left the it.each
  that lists refused nodes, so the test count stays the same.
- e2e 28 on melektaus-atx: ALL PASSED, 101 checks.
- Gate on melektaus-atx: CHECK EXIT 0, 297 files, 4562 tests.
- tsc, eslint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:35:20 +02:00
adminandClaude Opus 5.5 b9fcf48097 Show a cut into the middle of a clip without its old picture (#404)
A switched-off driven video waits at the video time it has where it
is, before its first key mostly the start value. Switched on at
another spot, its player has to seek first. Until the seek is done the
texture still holds the old picture, so the start of the clip flashed
for a frame or two at the cut.

- A view holds its last picture while a driven video that just came
  into sight is still seeking, for at most 0.3 s. At a cut the old clip
  stays one frame longer and the right picture of the new one follows.
- A seek counts as done only when the browser hands in the next video
  frame (requestVideoFrameCallback), and the texture is uploaded right
  then. The seeked event alone was too early, one red frame was left.
- A seek that takes longer than 0.3 s leaves the sprite out until its
  picture is there. The wrong picture does not show either way.

Tests:
- e2e 66q switches a sprite off at 0.2 s (red) and on at 1.2 s (blue)
  and reads every frame in the browser. No red may show, blue must
  stand at the end.
- Against the old code 66q fails with 2 red frames before the blue.
  With only the seeked event it still fails with 1 red frame.
- e2e 66 on melektaus-atx: ALL PASSED, 28 checks, run twice.
- Unit video-library, video-follow: 19 passed.
- Gate on melektaus-atx: CHECK EXIT 0, 297 files, 4562 tests.
- tsc, eslint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:18:56 +02:00
adminandClaude Opus 5.5 3a1e420806 Read a driven video's speed on the frame clock so 1x stays 1x (#404)
game-maker / gate (push) Successful in 4m12s
game-maker / suite (push) Successful in 16m58s
A driven video measures how fast its wearer's time runs and sets its
playbackRate from that. The time moves on with the ticker, but the
reading took its clock from performance.now() while drawing. A frame
that had more to do before the draw shifted that clock against the
time, and a 1x animation read as up to 1.09. Outside 0.9 to 1.1 the
video goes mute, so the sound could drop out for a moment. In CI this
made e2e 66k red now and then.

- VideoLibrary reads the speed with the timestamp of the frame
  (document.timeline.currentTime), the same clock the time runs on.
  It falls back to performance.now() where there is no document.
- debugInfo() reports the measured speed of a driven player.
- e2e 66k checks the measured speed and the correction of the rate
  apart. The rate alone may differ from 1 by up to 10 percent on
  purpose, so checking it at +-0.1 was too strict.

Tests:
- Unit video-library: 8 tests green, 1 new. With performance.now()
  back in place the new test is red and reads 1.089.
- e2e 66 on melektaus-atx: all passed, 27 checks, speed 1.000 in
  all 5 readings.
- Gate on melektaus-atx: CHECK EXIT 0, 297 files, 4562 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:49:45 +02:00
adminandClaude Opus 5.5 8679dfbe65 Group timeline rows by object and edit values in the row (#408)
The rows of the animation timeline stood in the order they were keyed.
With several objects and properties the list was hard to read, and a
value could only be changed by picking a key and using the inspector.
The Play button also kept saying Pause after scrubbing had paused.

- Each object gets a header row with its name, indented by depth. The
  groups follow the scene hierarchy, the root first. Rows whose object
  is missing go to the end.
- Under an object the rows follow the order in which it lists its
  properties: X, Y, Z, then the order of the inspector. Only the view
  is sorted, the animation itself is unchanged and there is no undo
  step. After a move in the hierarchy the order is right by itself.
- The row head shows the same field as the inspector with the value at
  the playhead. Without keys it shows the value of the object, for a
  relative row the offset.
- A change in that field sets or changes the key at the playhead,
  selects it and is one undo step. A running playback pauses first.
- The Play/Pause button follows every change of the playing state, so
  scrubbing that pauses shows Play again.

Tests:
- Unit animation + timeline-view: 100 passed, 3 new.
- e2e 25 on melektaus-atx: ALL PASSED, 34 checks, 3 new (groups,
  order and values at frame 4, a typed 55 becomes a selected key on
  frame 4, Play then a click on the ruler shows Play and pauses).
- Gate on melektaus-atx: exit 0, 297 files, 4561 tests.
- tsc, eslint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:42:10 +02:00
adminandClaude Opus 5.5 dd60a795e1 Wait for driven videos before a run starts so no sprite flashes (#404)
game-maker / gate (push) Successful in 4m28s
game-maker / suite (push) Failing after 16m24s
The game view opens a player for each driven video only when it draws,
and it draws only while its tab is visible. Without a detour over the
Game tab the players came up with the click on Play. Until a video had
loaded, its sprite showed its base colour, white in the user's cut.
Sharing the players of the scene view would not have helped. From the
Animations tab those are discarded after two to four seconds.

- Play holds the run at second 0 until every driven video in the game
  view has its first picture, at most 3 s. The status line says
  "Loading the videos… · Esc stops".
- The game clock stands while it waits. Behaviors and sounds start
  only after it, so picture and sound stay together.
- Load scene in a running game waits the same way.
- A driven video without a picture is not drawn at all instead of
  white. That holds everywhere, also on a switch to the Game tab
  without Play and at a cut whose video is not ready.
- The exported player waits the same way.
- __gm.playInfo() reports waiting for the e2e check.

Tests:
- e2e 66p puts the sprite in front of the camera and waits 4.5 s so
  the game view has no player left. Then it reads every frame of the
  run: 14 frames of waiting on #22252c at time 0, then blue, no white.
- Against the old code 66p fails: 14 frames #ffffff, then blue.
- Unit video-library: 7 passed, 1 new for loading().
- e2e 66 on melektaus-atx: ALL PASSED, 27 checks.
- Gate on melektaus-atx: exit 0, 297 files, 4558 tests.
- tsc, eslint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:07:53 +02:00
adminandClaude Opus 5.5 843b110bcb Hold a driven video at the end of its file instead of restarting (#404)
A driven video plays on for about 100 ms after the last key, until the
speed reading says it stands. With the key close to the end of the
file, that runs the video past it. The browser then marks it ended,
and follow() called play() on it in every frame. Per the HTML spec an
ended video starts over from the beginning of its file on play(). The
sprite flashed the first picture one to three times before it held.

- An ended video now counts as standing at its end. follow() holds it
  and seeks to the wanted picture instead of playing it again.
- The end of the file counts as an end too, not only the end of the
  clip.

Tests:
- e2e 66o plays an animation in once mode to 1.95 s of the 2 s test
  video and records the player time in every frame. It must never jump
  back, and the last picture must stand.
- Against the old follow() 66o fails, the player jumps back to 0 once.
  A first version with the key at 1.9 s stayed green, the video never
  reached the end of the file.
- e2e 66 on melektaus-atx: ALL PASSED, 26 checks.
- Unit video-library, video-follow, video-clock: 31 passed.
- tsc, eslint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:46:03 +02:00
adminandClaude Opus 5.5 43fd22a763 Keep a driven video ready while its sprite is off (#404)
A cut between two videos flashed white: the sprite switched on had
no picture yet, because its player only opened then or had been
dropped meanwhile. A driven video now waits muted at its time while
its sprite is off, in the editor and in a run.

- A disabled sprite with Video clock Driven asks for its player
  without drawing it. A Free video still opens when shown.
- A run's draw list keeps disabled driven sprites, marked as off.
  Before, collectWorldItems dropped every disabled object, so the
  game view never held a player for them.
- VideoLibrary.wear() counts as asking. The sweeper dropped a held
  player after 2 to 4 s and it was loading again at the cut.
- The speed of the asked time is read over a window that grows to
  500 ms. Over 100 ms one 1/60 s step more or less read as 17 % off,
  the playback rate jumped and the sound dropped out.
- A stream feeds every packet it has loaded to the decoder at once
  and waits once. One round trip per 20 ms packet let the decoder
  fall behind on a busy main thread, which made e2e 34 flaky in CI.

Tests:
- Unit streaming, video-library, video-follow: green, 4 new
- e2e 34 + 66 (atx): ALL PASSED, stream skipped 0, 66m new
- e2e 66 (atx), two runs: ALL PASSED, 25 checks, 66n new
- Gate (atx): CHECK EXIT 0, 297 files, 4557 tests
- Counter-checks: without the draw list fix 66n finds no player,
  without the wear() fix and with the old speed reading the new
  unit tests turn red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:37:16 +02:00
adminandClaude Opus 5.5 774207208c Key a video to its end at 1x and zoom the timeline further out (#407)
To play a clip at its real speed the user had to know its length,
work out the last whole frame by hand and then scroll far to the
right to set the end key. The timeline now does the sum, and it
zooms out far enough to see a long clip in one piece.

- The context menu of a Video time row gets "Video at 1x to its end
  from frame N". It keys the playhead with the row's value there and
  the last whole frame before the clip ends with the matching time.
- The start key turns linear and keys in between are removed. The
  whole run is one undo step (KeyStraightRunCommand).
- The clip is the one the sprite wears at the playhead, also when
  the texture is keyed in the same animation (rowVideoAt).
- The entry is greyed out for relative rows, without a video, while
  the length is unknown, or when the playhead is already at the end.
- The status line names the result, e.g. "frame 572 shows 57.2 s of
  57.27 s".
- The timeline zooms out to 0.02 px per frame instead of 4 px. The
  ruler labels follow a 1-2-5 step without an upper limit.

Tests:
- Unit video-clock + timeline-view: 27 tests green, 8 new
- e2e 66 (atx, alone): ALL PASSED, 23 checks, 66l new
- Gate (atx): CHECK EXIT 0, 297 files, 4553 tests
- Counter-check: the command keeping the keys in between turns
  exactly the command test red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:51:20 +02:00
adminandClaude Opus 5.5 c9e8a3f6ac Let an animation drive a sprite's video picture by picture (#404)
game-maker / gate (push) Successful in 4m16s
game-maker / suite (push) Failing after 16m12s
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>
2026-10-11 14:23:07 +02:00
adminandClaude Opus 5.5 a62d1d0faf Give e2e 66 its own lane so the video sprite is not covered (#406)
game-maker / gate (push) Successful in 4m36s
game-maker / suite (push) Successful in 16m31s
In the full suite e2e 66f saw the same purple #cc00f4 fourteen times
instead of the clip's green and blue. Alone the module was green. The
video sprite stood at (0, 0), and there earlier modules keep objects
on purpose that later modules still need. Every other module has its
own lane for that reason, only 66 had none.

- scaffold.mjs: new lane `video` at y -7000.
- 66-video.mjs: the sprite and lookAt use that lane, and the cleanup
  looks back at (0, 0) like the other lane modules do.

Found with a diagnosis in a full run on melektaus-atx: under the
sprite's centre lay o3, otex (z 900) and filler40 to filler1 (z 5)
from e2e 10 to 12, only then vd66-object. Beside them the sprite
showed green as it should. The purple is otex's texture in the state
later modules leave it in. It has nothing to do with video seeking:
task 645 saw the same colour while seeking was still broken.

Tests:
- e2e 10-12 + 66, old state (melektaus-atx): 66f FAIL, otex in front
- e2e 10-12 + 66, with the fix: ALL PASSED, 81 checks, 66f sees
  #00ff01 and #0000ff
- Prettier, eslint: clean

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:40:43 +02:00
adminandClaude Opus 5.5 ce933a4f32 Let a drag change only the picked clip in the Sound and Video tab (#403)
game-maker / gate (push) Successful in 4m15s
game-maker / suite (push) Failing after 15m40s
A drag with a clip picked could span a new clip in the strip or span
the picked one anew over the wave. Both happened by accident while
working on a clip. Now a drag only makes a new clip while none is
picked. With one picked it moves its edges or the whole of it.

- With no clip picked, a drag in the strip or over the wave (in the
  Video tab the track) spans a new clip, as before.
- A click over the wave or in the track picks the clip at that point,
  as a click on its band does. Beside the clips it picks the sound or
  the video. The picked clip wins where clips lie over each other.
- With a clip picked, its handles move its edges as before. A drag on
  its band in the strip moves the whole clip, its length kept and
  inside the file. Any other drag does nothing.
- The rules live in waveView.ts (clipGrip, clipToPick, movedSpan) and
  both tabs use them. The cursor shows grab over the picked band.
- The Video tab's track draws the clips not picked faint, as the wave
  does, so a click there has something to aim at.

Tests:
- Unit wave-view.test.ts: 16 tests (4 new)
- Counter-check: a strip drag beside the picked clip spanning a new
  one again fails the picked-clip rule test
- e2e 34 + 66 (atx): ALL PASSED, 162 checks, 1 new check in each
- Gate (atx): 295 files, 4527 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:51:46 +02:00
adminandClaude Opus 5.5 c815d2c35f Keep a playing video when the window comes back to the front (#402)
Chrome draws no frames for a hidden page, so the scene stopped asking
for its videos. The sweeper of the VideoLibrary kept running and
dropped every player after 2 s. Back in the window, the video started
over, and Pixi warned that a texture source was destroyed while still
bound to a shader.

- The sweeper skips a hidden page. The time hidden does not count as
  idle, the 2 s start once the page is shown again.
- A dropped player only unloads its video source instead of destroying
  it. A shader that still holds the source no longer warns.
- CI serves videos through a new router for php -S. The built-in
  server answers no Range requests, so Chrome could not seek. e2e 66
  had 4 red checks in the suite job of d48f174 because of it. nginx on
  stein2 hid this.

Tests:
- Unit video-library.test.ts (new): 3 tests
- e2e 66 (atx): ALL PASSED, 18 checks
- Router checked with curl: 206 with the right bytes
- Counter-check: the old sweep fails both hidden-page tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:32:59 +02:00
adminandClaude Opus 5.5 d48f174caf Play videos on sprites, converted on the server with clips (#402)
game-maker / gate (push) Successful in 4m15s
game-maker / suite (push) Failing after 15m49s
A video becomes an asset like a sound: uploaded once, converted to one
format by a worker of its own, cut into clips in its tab, and worn by a
sprite through its Texture row. Only Chrome is a target, without alpha.

- Server: POST ?r=videos queues the upload under the hash of its bytes,
  so the MP4's name is known at once. The worker (VideoWorker over
  VideoFormat) writes H.264/AAC, yuv420p, constant frame rate, a
  keyframe every 15 frames, at most 1920x1080, with a poster and the
  details beside it. A failed job says why and is tried again on the
  next upload.
- An upload may be up to 2 GB. It is streamed from the request to disk
  and hashed there, so php-fpm holds none of it in memory. The other
  routes keep their 256 MB.
- The worker runs in the container game-maker-video on stein2, a
  compose project of its own (tools/video/), and in CI beside the PHP
  server. The CI image has ffmpeg for it.
- Model: asset type video with size, length, frame rate and sound, and
  clips like a sound's. The clip helpers and commands serve both.
- A sprite's Texture row offers videos and clips after the pictures.
  resolvePicture names a playing video by a key, and the texture library
  hands it to a VideoLibrary with one player per key.
- The editor shows a placed video as a quiet preview. Play starts it
  over at its clip's start and lets it be heard through the master bus,
  a pause holds it, Esc makes it quiet again.
- Video tab: the picture, a ruler, the clip strip and a track on the
  Sound tab's time axis. A drag spans a clip, a handle moves an edge,
  Play plays the picked clip.
- The picker shows a video on its way with its progress and adopts an
  upload from it once it is ready. Dropping a video file does the same.

Decided on my own:

- Two sprites wearing the same clip share one player and show the same
  picture. Control per instance belongs to the follow-up with nodes.
- A video plays in real time, the game's time scale does not slow it.
- The container may use 3 of the 4 cores of stein2.

Findings:

- The first real upload (279 MB) got 413: the API's own ceiling of
  256 MB for every request, not nginx or the Brandmauer.
- Chrome has no picture of a video that never played, and a Pixi
  VideoSource drawn before the first frame stays empty for good. The
  texture is made in the first frame callback (openVideo).
- A player nobody asks for in 2 s is let go. The inspector's PREVIEW
  asked only on an edit, so with the sprite not on screen its texture
  was destroyed under it and the batcher threw on every frame. The
  preview now redraws every frame while it shows a video, like a Time
  shader. A let-go texture keeps an empty source instead of none.
- VideoSource.load() reloads the element and cuts off an earlier
  play(). Playing starts after loadeddata.

Tests:

- PHP videos.test.php: 68 checks, with the real ffmpeg
- 300 MB upload through Brandmauer and nginx: 201 in 21 s, php-fpm at
  13.7 MB RSS at most
- Unit videos.test.ts: 20 tests
- e2e 66 (stein2, atx was off): ALL PASSED, 18 checks; e2e 04 with the
  new asset count green
- Gate (atx): 294 files, 4520 tests. serialize.test.ts used video as
  its example of an unknown type and now uses a made-up one.
- Counter-checks: a keyframe gap of 30 fails the PHP check, a key that
  keeps the video's end fails 2 unit tests, a clip without its end
  fails "plays only its stretch" in e2e 66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:12:39 +02:00
adminandClaude Opus 5.5 2f376262d3 Let the Reverb's screen ring out and its reflections drift (#401)
game-maker / gate (push) Successful in 4m13s
game-maker / suite (push) Successful in 16m5s
The round screen of the Reverb stood still. It now moves, only for
the look of it, and says nothing it did not say before.

- The sound rings out again and again in real time. The dot in the
  middle flashes, and a thin front runs out over the rings as fast as
  the tail does. It fades as the tail falls, then 0.8 s of quiet.
- A reflection lights up for a moment once the front has passed it.
- The reflections turn slowly round the dot, each at its own pace and
  way (at most 0.16 rad/s). They sway 1.5 px in and out and twinkle.
  Their drift comes from a noise of its own, so they sit where they
  always did.
- Rings, grid and dashed end stay one drawing. Only the dots, the
  front and the flash are drawn again each frame.
- Nothing moves without a tail to see: no sound picked, or wet at 0.

Tests:
- reverb-view: 13 tests green (3 new)
- e2e 34 on melektaus-atx: ALL PASSED, 144 checks
- gate on melektaus-atx: 293 files, 4498 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:32:49 +02:00
adminandClaude Opus 5.5 742f240a62 Make the Bitcrusher and Delay plates shorter (#399, #393)
Both devices were 280 tall, but their knobs end well above that. The
gap between the lamps or levers and the knobs was wide, and below the
knobs about 70 units stayed empty.

- The Bitcrusher is 680x220. Its knobs sit 14 higher, closer to the
  row of bit lamps.
- The Delay is 680x224. Its knob groups start 16 higher, closer to the
  two levers.
- The screen's frame is now worked out from the plate's bottom edge.
  It stays as far from the bottom as from the side, so the screen
  shrinks with the plate (116 and 120 tall).

Tests:
- crusher-view and delay-view: 30 tests green
- e2e 34 on melektaus-atx: ALL PASSED, 144 checks
- gate on melektaus-atx: 293 files, 4495 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:16:47 +02:00
adminandClaude Opus 5.5 6383cc0d8e Show a Sample node as a device that draws its waveform (#400)
game-maker / gate (push) Successful in 4m14s
game-maker / suite (push) Successful in 16m49s
The Sample was the last node of an instrument that still showed rows.
It now draws the sound or clip it plays, so the loop and what plays
after the note can be seen instead of read off two numbers.

- SamplePanel, SampleFace and sampleView: a compact gold plate of
  480x280 with the sound picker beside the name, a waveform screen,
  the four ways of playing it as buttons two by two, the root as a
  knob that reads as a note, a lever for following the note and the
  loop's start above its end.
- The waveform comes from the library's peaks of the played sound,
  a clip's own cut included, scaled by the sound's volume. It is
  loaded on demand as the Reverb's sound is.
- Each part has its colour: played once, looped, played after the
  note, never heard. The loop is held as playback holds it: an end
  of 0 or past the sound is the end, a start not before it loops
  all of it.
- Idle settings come from the node's own idleParams, so the device
  greys out what the rows did.
- A new instrument places its Output at x 840, past the device.
  At 460 it lay under the Sample.
- Hook __gm.sample.panel and a check in e2e 34 behind the first
  instrument check.

Tests:
- Unit sample-view: 11 tests, new. pattern and chip-presets green.
- e2e 34 (atx): ALL PASSED, 144 checks (1 new).
- Gate (atx): 293 files, 4495 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:54:17 +02:00
adminandClaude Opus 5.5 a6c00f7b8b Show a Bitcrusher node as a device that draws its pixels (#399)
game-maker / gate (push) Successful in 4m11s
game-maker / suite (push) Failing after 15m21s
A Bitcrusher only said what it does in rows of numbers. How coarse
its steps and holds are could not be seen. Now the node is a device
of its own size. Its screen sends a sine through the node's own crush
and draws what comes out in cells, as tall as a step and as wide as a
hold, so the coarser the crush, the coarser the picture.

- CrusherPanel draws a 680x280 plate in bubblegum pink. Bits and rate
  are large knobs, wet is small. Rate gives its low end more of the
  turn, 8 kHz sits just past the middle.
- crushTrace runs a 440 Hz sine at 0 dBFS through Crusher at 48 kHz
  and blends it as the worklet does. Where each hold starts comes from
  Crusher too: a ramp through it at 16 bits takes a new value per hold.
- cellOf shapes a hold. From 5 px a step or hold gets a grid line and
  a gap. Finer steps give a 2 px bar. A cell is never more than twice
  as tall as wide, so 1 bit gives bars at -1, 0 and 1.
- The screen shows two periods, or 6 holds where the rate is low. The
  sine going in is a grey ghost. Below wet 1 the heard line lies over
  the cells.
- Above the screen a word for how crushed it is, CLEAN to BROKEN.
  Under it the noise the bits leave and where the rate folds back.
- 16 lamps show the bits kept, the highest on the left. A CRUSH lamp
  beside the name glows the more it crushes.
- COLUMNS_OF gives the Bitcrusher 4 columns in the presets.
- Hook __gm.bitcrusher.panel(id) for the sound FX and instrument
  graphs.

Decided myself:

- The screen is still. Nothing on it moves, so tick() says false.
- How crushed counts whichever of bits or rate takes more, the rate in
  octaves, times wet.

Found: at 8 kHz the first hold is 7 samples long, the rest 6. Six
sixths add up to just under 1 in Crusher. The worklet does the same,
so the screen shows what is heard.

Every sfxr effect preset ends in a Bitcrusher and gets 3 columns
wider. The Laser preset still fits whole on Fit.

Tests:
- Unit: crusher-view 13 tests (new). The screen matches Crusher with
  its wet on three settings.
- Counter-test: without the wet in the blend the worklet test fails.
- e2e 34: ALL PASSED, 143 checks (1 new).
- Gate: CHECK EXIT 0, 292 files, 4484 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:05:47 +02:00
adminandClaude Opus 5.5 b6749cc43d Show Tremolo, Vibrato, Phaser and Flanger as devices that swing (#398)
The four effects that swing were still drawn as rows of numbers. Like
the Delay, the Saturation and the Limiter, each node now shows a device
of a fixed size. Its screen draws what it does over the first seconds
of a sound, and a light plays that in real time, over and over.

- One device for all four (ModulationPanel, ModulationFace and
  modulationView), as KnobPanel is for Gain, Pitch, Mix and Pan. A short
  description gives each its title, plate, buttons and knobs.
- Tremolo, 640x280 in cream: a wave its volume shapes, the volume as
  its outline. It says DOWN TO 50%, EVERY 200 ms and DIPS -6 dB.
- Vibrato, 640x280 in plum: the pitch in semitones, cool below and warm
  above. Under a wet below 1 the dry pitch stands beside it. It says
  how late the voice is (LATE 0.93 ms).
- Phaser, 760x280 in mustard, and Flanger, 680x280 in steel blue: the
  response from 20 Hz to 20 kHz, and under it a lane of the turn or the
  delay over time. The response is drawn where the light is, so the
  notches swirl and the comb jets. A comb finer than a column fills it.
- The light loops over 1, 2, 4 or 8 s, two LFO swings at least, for a
  Flanger as long as its sweep takes to reach 20 ms. The face only
  keeps the editor drawing while something swings and is heard.
- A Rate lamp beside each name goes from dark to lit with the swing.
- Nothing is listened to. The pictures use the worklets' own numbers:
  lfoAt, vibratoSpeedUp (now exported from lfo.ts), the allpass chain
  with its feedback, and the comb, reckoned at 48 kHz.
- Phaser and Flanger take 4 preset columns. Phased Pad was looked at
  in a picture, nothing overlaps.
- Exported from nodes.ts: LFO_SHAPE, lfoShapeOf, TREMOLO_ROWS,
  VIBRATO_ROWS, PHASER_STAGE_ROW and phaserStagesOf. FLANGER_MAX_MS is
  exported from phaser.ts.

Decided here, as the request left the look open:

- A notch counts where the response falls below -3 dB, not some dB
  under the loudest point. Feedback rings peaks far above 0 dB, and the
  first rule counted 7 notches on a Phaser of 12 stages.
- Rate and Depth of Tremolo and Vibrato are big knobs, Wet is small.

Tests:
  Unit modulation-view (stein2): 22 tests, new. Phaser and Flanger
    give the dB of their worklets on a sine within 0.005 dB, the
    Tremolo its samples, the Vibrato the pitch it reads at.
  Counter-check: with the sign of the Phaser's feedback flipped in one
    place, exactly the worklet test fails.
  e2e 34 (atx): ALL PASSED, 142 checks, 4 new. Each device has its
    size, its ports outside, a light that runs by itself, and a button
    and a knob as one undo step each. Picking 8 stages on the Phaser
    draws 4 notches.
  Gate (atx): CHECK EXIT 0, 291 files, 4467 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:02:50 +02:00
adminandClaude Opus 5.5 28909f0adf Show a Limiter node as a device that holds what it took away (#397)
game-maker / gate (push) Successful in 4m23s
game-maker / suite (push) Successful in 16m6s
A Limiter only said what it does in rows of numbers. How much it took
away, and when, could not be seen at all, and a short sound is over
before any needle could show it. Now the node is a device of its own
size, and its screen catches the reduction from the moment a sound
starts and holds it once the sound ends.

- LimiterPanel draws a 720x280 plate of brushed brick. The threshold
  knob is large, ceiling and release are small beside it, auto-gain is
  a lever. A LIMIT lamp lights while it takes something away.
- The screen hangs the reduction down from the top, 0 to 24 dB by the
  square root like the Compressor's meter. The level coming in runs
  along the bottom, orange above the limit. Time runs from the start
  of the sound over 1, 2, 4 or 8 s, and a longer sound rolls.
- ReductionCapture in limiterView holds the capture after 0.4 s of
  silence, without the silence, until the next sound. It writes the
  most taken away and when, and NOW or LASTED on the right.
- The Limiter is tapped now, like the Compressor. The face reads the
  loudest sample since the last frame on both sides.
- SpectrumTaps.levels takes a lateBy. The side in front is read 5 ms
  earlier, so a peak going in is met by what it became.
- limiterReduction leaves the auto-gain out and solves Wet's blend for
  the limited part. Less than 0.2 dB counts as none.
- A capture is also held when the node leaves the picture. Changing a
  knob or the lever leaves it as it is.
- Hook __gm.limiter.panel(id) for the sound FX and instrument graphs.

Decided myself:

- The capture stays held until the next sound, not for a fixed time.
- The screen has no wet knob, as the Compressor's has none.
- The Limiter's two taps cost two gains of 1 per voice. The analysers
  only exist while the node is in the picture.

Measured on stein2: the seeded blip at -4.4 dBFS against -12 dB showed
7.56 dB. Three hits at 0, -8 and -2 dBFS showed 11.9 dB at the first.
In e2e 34 on melektaus-atx, -10.44 dBFS in against -20 dB showed
9.56 dB, the difference to the hundredth.

Tests:
- Unit: limiter-view 13 tests (new), spectrum 6 (1 new), soundfx 67
  with the Limiter's taps.
- Counter-test: without Wet's share in the formula the blend test
  fails.
- e2e 34: ALL PASSED, 138 checks (1 new).
- Gate: CHECK EXIT 0, 290 files, 4445 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:30:59 +02:00
adminandClaude Opus 5.5 80b25488ce Show a Saturation node as a device that glows with its drive (#396)
game-maker / gate (push) Successful in 4m5s
game-maker / suite (push) Successful in 16m2s
A Saturation in the graph was a stack of rows. The user wanted a device
of its own style and size, like the Delay and the Reverb, that shows
warmth and heat the harder the sound is driven.

- The node is a copper plate of 720x280 with a button per type, the
  auto-gain lever, a big drive knob and small knobs for tone, output
  and wet. Each change is one undo step, wired numbers lock their knob.
- The screen draws a sine at -12 dBFS, the level auto-gain is reckoned
  at, as a grey ghost. Over it is the wave that comes out: bent by the
  type, its DC taken out, auto-gain and output after it, blended by
  wet. Under it stand the auto-gain and the peak.
- The heat is the drive over 48 dB, as far as wet lets it be heard.
  The wave goes from cold steel through ember red to white, a glow
  rises from below and flickers, and embers fly up from about 8 dB on.
  A lamp beside the name glows with it.
- The node keeps the editor drawing only while there is heat. Without
  drive it draws nothing, unlike the Delay, which always animates.
- The Reverb's private type button is now PickButton with its own
  colours, and the Saturation's type buttons use it as well.
- No instrument preset uses a Saturation, so no preset needs more
  columns.

Tests:
- Unit saturation-view: 14 new tests.
- Counter-check: without taking out the DC, exactly the DC test fails.
- e2e 34 (atx): ALL PASSED, 137 checks, one new for type, lever and
  drive with undo.
- Gate (atx): CHECK EXIT 0, 289 files, 4431 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:05:11 +02:00
adminandClaude Opus 5.5 8c53ce0cbe Replace the 5-Band-EQ with the Parametric EQ and remove it (#395)
game-maker / gate (push) Successful in 4m13s
game-maker / suite (push) Successful in 16m21s
The Parametric EQ does all the 5-Band-EQ did and more, so the editor
keeps only one EQ. The old node was used in one project only, with
nothing linked to its band inputs.

- "Garden of Sacrifices" was converted once on the project file, not
  by a load migration (#137). Its 5 nodes are Parametric EQs now with
  the same filters: low shelf 80 Hz, peaks at 250 Hz, 1 kHz and 4 kHz
  with Q 1, high shelf 12 kHz. A band at 0 became no band.
- Two values below -24 dB were held to -24, as the Parametric EQ goes
  no further. Both are in the asset "Delay" (high -60, low -36). The
  user will rework them by ear.
- The write went through the API's own Store (lock, revision 20181 to
  20182), so a tab with the old revision gets a 409 instead of
  overwriting it.
- Removed with the node: its stage kind "filter" and the type
  FxFilter, which nothing else made, and the player's branch for it.
  A Pan is now the last branch of fxNodes, a Flanger that of stagesOf.
- Tests that took the 5-Band-EQ as "some effect" use the Pan or the
  Parametric EQ now. The test for a parameter feeding a band now
  feeds the cutoff of a Filter, which had no such test yet.
- A project that still names the type, such as an old snapshot of
  "Garden of Sacrifices", loads without those nodes. Unknown types
  were always left out on load.

Tests:
- Unit (stein2): soundfx 67, instrument 36, envelope 13, sound-library
  127, all green
- Counter-check: with the cutoff not taken from the parameter, only
  the new Filter test goes red
- The converted project loads and compiles: Delay, Vocals and Drums
  play their bands (3+1, 4 and 5)
- e2e 34 (atx): ALL PASSED, 136 checks, the palette check now looks
  for the Parametric EQ
- Gate (atx): CHECK EXIT 0, 288 files, 4417 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:46:03 +02:00
adminandClaude Opus 5.5 3f7eb03143 Check what a streamed sound holds by the library's own count (#378)
game-maker / gate (push) Successful in 4m8s
game-maker / suite (push) Successful in 16m22s
The e2e check that a streamed sound costs less than a decoded one
compared how much the renderer process grew. Alone, module 34 saw it
grow by 1 to 8 MB while streaming. After the 33 modules before it in
CI it grew by 25 to 66 MB, and suite went red (CI tasks 611, 614 and
617, the last with 66.7 MB streamed against 95.9 MB decoded).

- SoundLibrary.stats tells decodedBytes, the samples of every file
  decoded whole, and streaming.held, every head of a copy and what is
  sent ahead to the worklet.
- StreamFile.headBytes and StreamFeed.ahead give their share.
- The e2e check now asks the library: streamed it holds less than
  4 MB more than before and nothing decoded whole, decoded it holds
  more than 40 MB, the five minutes being 50 MB at 44.1 kHz.
- The growth of the renderer stays in the log of that check. The
  counter-proof that the renderer grows by more than 40 MB when the
  sound is decoded stays as it was.

Tests:
- Unit sound-library: the streaming test also checks the bytes held.
- e2e 34 (melektaus-atx): 136 checks. Streamed 0.76 MB more held and
  none decoded whole, decoded 50.5 MB. The renderer grew by 4 MB and
  49 MB.
- Gate (melektaus-atx): 288 files, 4416 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:01:00 +02:00
adminandClaude Opus 5.5 d4a45c127b Show a Delay node as a device with its echoes over time (#393)
After the Chip, the Envelope, the Reverb and the one-knob devices, the
Delay is drawn as a device of its own size too. Its screen shows what
time, feedback, damping, wet and ping-pong make of a sound, and a light
runs over the echoes at the beat they come at.

- DelayPanel: a 680x280 plate in the Compressor's brushed aluminium,
  now exported as ALUMINIUM. A screen, levers for Sync and Ping-pong,
  knobs for Time, Feedback, Damping and Wet, and an Echo lamp.
- The screen draws the sound as a white bar and each echo as a bar one
  step later. A bar is as tall as the echo is loud, evenly in dB to
  -60. It turns from warm white to rust as the damping takes the highs.
  With ping-pong the bars alternate above and below, the first left.
- At most 24 bars are drawn, and the screen says how many more are
  heard. Under it stand the step and the end of the tail, above it
  where the highs of the first echo end.
- The running light: DelayFace.tick returns true while the node is in
  the picture, so the editor keeps drawing, as for the Particles tab.
  The light goes round the span in real time, a bar glows as it is
  passed, and the lamp blinks with every echo but not with the sound.
- On beats the screen counts at 120 BPM, as the tempo of the voice is
  not known in the graph. The time knob then names the note, dotted
  and triplet ones included. In seconds, 2 s and up reads "2 s max".
- delayView holds the rules apart from the drawing: layout, tapers,
  echoTrain, echoCutoff for k rounds of the Butterworth low-pass,
  sweepAt and the texts. At wet 0 the rest is drawn faint.
- delayEchoes in the core counts the echoes heard. The tail of a voice
  and the screen use it both. DELAY_ROWS, DELAY_SYNC and PING_PONG are
  exported.
- Knob.reformat writes the number anew when its format changes, here
  when Sync switches between seconds and beats.
- The test hook __gm.delay.panel returns knobs, levers, idle settings,
  the echoes drawn, the texts and where the light is.
- Instrument presets give the Delay 4 columns. Echo Lead and Bell use
  one. In the picture of Echo Lead nothing overlaps.

Found on the way: a node with a face draws no setting rows. The e2e
check of a bypassed Delay (#354) read "Bypass: true" from them. It now
checks the faded node and the stages heard, as before.

Tests:
- Unit delay-view: 17 tests, new. Counter-check: the first ping-pong
  echo on the right turns the side test red.
- e2e 34 (melektaus-atx): 136 checks, 1 new.
- Gate (melektaus-atx): 288 files, 4416 tests.
- Pictures looked at: default, ping-pong, beats with damping, 83
  echoes at 50 ms, wet 0, and the preset Echo Lead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:51:06 +02:00
adminandClaude Opus 5.5 389d9555fa Show a Pan node as a one-knob device in cyan (#392)
game-maker / gate (push) Successful in 4m13s
game-maker / suite (push) Failing after 15m23s
After the Gain, the Pitch and the Mix, the Pan is the next node that
is just one number. It now gets the same device in the graph instead
of a row for its pan.

- PAN_DEVICE: the Gain's 160x160 plate in cyan with one bipolar knob
  for Pan. The middle stands straight up, and the turn goes evenly to
  L and R, which are engraved at its ends like A and B on the Mix.
- PAN_TAPER in hardwareView is 1, so left and right are heard alike.
  Device and unit test read the same constant.
- PAN_ROW is exported from the core, so the knob has the node's range.
- A linked Pan greys the knob, as for the Gain.
- The test hook __gm.pan.panel returns the same shape as the Gain's.
- No instrument preset uses a Pan, so no preset columns change.

Tests:
- Unit hardware-view: 17 tests, 2 new (the turn, and L and R inside
  the plate and clear of the screws). Counter-check: PAN_TAPER 2
  turns the turn test red.
- e2e 34 (melektaus-atx): 135 checks, 1 new.
- Gate (melektaus-atx): 287 files, 4399 tests.
- Pictures looked at: Gain, Pitch, Mix and Pan in a row, Pan at -0.35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:26:17 +02:00
adminandClaude Opus 5.5 fe4e0d9f1a Snap to the grid from the start in every graph tab (#391)
game-maker / gate (push) Successful in 4m12s
game-maker / suite (push) Successful in 15m55s
Laying out a graph by hand mostly means lining nodes up, so the
magnet was switched on in nearly every session. Every graph tab now
starts with it lit: Behavior, State machine, Shader, Particles,
Sound FX, Instrument, the song's Routing and the Routing tab.

- SnapToggle takes the state it starts in and lights its button to
  match. GRAPH_SNAPS_AT_FIRST sets it for the toggles and for
  GraphCanvas alike, so button and canvas cannot disagree.
- Curve and Vector Art are no graphs and still start without it.
- New hook __gm.graphSnapping(on?) reads every graph tab's magnet,
  or "mixed" where button and canvas differ, and switches them all.
- The e2e harness reads the magnets after each navigate() and then
  switches them off, as it already sets the zoom transition to 0.
  Most checks drag a node to an exact spot. e2e 45 does the same
  itself, because the editor comes up there through the sign-in.
- e2e 06 checks that all eight graph tabs started with the magnet on.
  The checks in 06, 26 and 65 that read it off now say they read
  what the harness left.

Tests:
- Unit snap-toggle: 4 tests, 1 new (lit at first, nothing asked of
  the target before a click).
- e2e 06 (melektaus-atx): 150 checks, 1 new. e2e 34: 134 checks.
- Gate (melektaus-atx, together with #390): 287 files, 4397 tests.
- Pictures looked at: none, the state comes from the hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:37:13 +02:00
adminandClaude Opus 5.5 e70954115c Show Pitch and Mix nodes as one-knob devices in green and blue (#390)
The Gain has been a small device with one big knob since #384. The
Pitch and the Mix are just as much a single number, so they now look
and work the same way. The Mix also gets a lever for its curve, which
until now was only a row in the inspector.

- GainPanel and GainFace become KnobPanel and KnobFace. Each node is
  described by a KnobDevice: its plate, its row, how the knob turns,
  and an optional lever. The Gain looks as before.
- Pitch: a green 160x160 plate with a bipolar knob for Semitones. 0
  stands straight up and taper 2 puts an octave at 43 % of each half,
  so small steps near 0 are easy and the full +-64 is still there.
- Mix: a blue 180x200 plate, the lever on top and the knob below.
  The knob has A and B engraved at the ends of its turn. The lever
  reads "crossfade" down and "both at center" up with its lamp lit.
  A flip is one undo step.
- A wired Semitones or Mix greys the knob, as for the Gain.
- The instrument presets give Pitch and Mix two columns. In the graph
  they are 281 and 261 units wide, more than one column of 240.
- The test hooks __gm.pitch.panel and __gm.mix.panel return the same
  shape as __gm.gain.panel, plus the lever.

Tests:
- Unit hardware-view: 15 tests, 3 new (Pitch turn, lever above the
  knob and ends on the Mix plate). Counter-check: taper 1 turns the
  Pitch test red.
- e2e 34 (melektaus-atx): 134 checks, 1 new.
- Gate (melektaus-atx, together with #391): 287 files, 4397 tests.
- Pictures looked at: Gain, Pitch and Mix in a row, Pitch at -24.5,
  Mix with both curves, the Bell preset without overlaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:37:03 +02:00
adminandClaude Opus 5.5 e8021b1fd3 Show a Reverb node as a device with a round screen of its tail (#389)
game-maker / gate (push) Successful in 4m5s
game-maker / suite (push) Successful in 15m49s
The Compressor, the Gain, the Chip and the Envelope are devices in the
graph. The Reverb was still a list of rows, and how long and how dark
its tail rings was left to the numbers.

- The node is a plate of 640x280 in pale lavender. Its inputs are on
  the left, its output on the right.
- A round screen draws the tail as rings out from the sound. A ring's
  radius is its time, as the root of its seconds, on one scale up to
  10.5 s for every Reverb. So a Room is a small disc and a Cave fills
  two thirds.
- A ring is as bright as the tail is loud there, evenly in dB. Its
  colour is how much of the highs is left, from ice to violet, with
  the cut-off of the impulse response (reverbCutoff, now shared).
- The pre-delay is a dark gap round the dot, a dashed ring marks
  -60 dB. Seeded dots stand for the reflections. Wet fades the tail
  against the dot.
- A type of Sound draws the peaks of its sound from the library, each
  against its loudest. It says LOADING until they are there.
- A button for each type, a list of the sounds and four chrome knobs:
  Decay, Damping, Pre-delay and Wet.
- A knob the type does not take is greyed but turns. It goes over to
  Custom with the type's numbers, as one undo step. Picking a sound
  makes the type Sound, also one step.
- NodeParamDrag can write several settings as one drag for that.
- Test hook __gm.reverb.panel(nodeId), for the sound FX and the
  instrument graph.

Tests:
- Unit reverb-view: 10 new tests for the plate, the knob turn, the
  numbers per type, the scale, the tail and the texts. A screen that
  ignores damping turns the highs test red.
- e2e 34 (atx): 1 new check (device between its ports, decay knob of
  a Hall goes to Custom as one undo step, Cave button, a sound from
  the list makes it Sound and draws that sound).
- Probe pictures on stein2: Room, Hall, Cave, Plate, Custom with
  damping 1 and pre-delay 0.3 s, Sound.
- Gate (atx): CHECK EXIT 0, 287 files, 4393 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:35:59 +02:00
adminandClaude Opus 5.5 c75e72d4ee Show an Envelope node as a device with its ADSR curve (#388)
game-maker / gate (push) Successful in 4m24s
game-maker / suite (push) Successful in 15m50s
The Compressor, the Gain and the Chip are devices in the graph. The
Envelope was still a list of rows, and its shape was left to the
imagination of whoever read the numbers.

- The node is a plate of 520x280 in pale sea green. Its inputs are
  on the left, out and value on the right.
- A dark screen draws the curve over a note in amber. A letter sits
  above each stage, its time below it, and the sustain in percent
  above its line.
- The time on the screen is squeezed. Each stage is as wide as the
  root of its seconds, at least 3 %, and the hold has a fixed fifth.
  So 5 ms of attack still shows beside a release of 3 s.
- The falls use SETTLE, as the player does. With sustain 0 there is
  no hold and no release, and the screen says NOTE ENDS.
- Attack, Decay, Sustain and Release are chrome knobs. The times use
  taper 4, so 5 ms sits an eighth of the way and 1 s not quite half.
- From and To have no range a knob could turn through. They are
  NumberFields on the plate, typed in or scrubbed.
- The decay at sustain 1 and From/To while nothing takes value are
  faded. While a link takes value, the bezel says what it moves
  between, as VALUE 400 TO 3500. The UI font has no arrow.
- Instrument presets give an Envelope 3 columns of 240.
- Test hook __gm.envelope.panel(nodeId).

Tests:
- Unit envelope-view: 6 new tests for the plate, the knob turn, the
  idle settings and the curve. A linear decay turns two of them red.
- e2e 34 (atx): 1 new check on the Laser preset (device between its
  ports, curve ends with the decay, decay knob and scrubbed To field
  each one undo step).
- Probe pictures on stein2: Synth Brass preset, pad, pluck, organ
  and an Envelope whose value moves a Chip.
- Gate (atx): CHECK EXIT 0, 286 files, 4383 tests. The first run was
  red on the font test (#214) for an arrow in the bezel text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:06:15 +02:00
adminandClaude Opus 5.5 ab37d3423a Let graphs zoom out to an eighth (#387)
With the Chip and the Filter drawn as devices, a preset such as the
Laser is about 3060 units wide. In a window of 1280 px a fit then hit
the floor of 0.25 and cut the Notes node off on the left.

- ZOOM_LIMITS.min goes from 0.25 to 0.125. The graph zooms without
  steps, so one more step is the floor halved. 0.125 is also the
  smallest step of the scene view.
- It holds for the wheel, for Fit and for the node board of plugins.
- The grid still hides itself below 8 px, so it does not flicker.

Tests:
- Unit plugins and graph-framing: 55 tests green, the floor test now
  expects 0.125.
- e2e 06 (atx): ALL PASSED, 149 checks. Only a comment that named 0.25
  changed there.
- Gate (atx): CHECK EXIT 0, 285 files, 4377 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:09:05 +02:00
adminandClaude Opus 5.5 eb78f3cbf3 Show a Chip node as a device with its wave and its pitch (#386)
game-maker / gate (push) Successful in 4m5s
game-maker / suite (push) Failing after 16m5s
The Chip had eleven numbers in rows and nothing that showed what they
do to a note. Its node is now a device of one size like the
Compressor's, and two screens draw the wave and the pitch from the
settings, worked right in the instrument graph.

- ChipPanel is 840x280 in the warm grey of a handheld console. A
  button per wave, the "follow note" lever and a chrome knob per
  number, grouped Oscillator, Slide, Vibrato, Arpeggio and Repeat.
- The wave screen draws two periods as loud as the Chip plays them,
  a square at its duty. toneValue and periodicSteps come out of
  chip.ts, so the screen and the worklet share one formula.
- The pitch screen draws what slide, delta slide, vibrato, arpeggio
  and repeat make of the pitch over a note. It ends where the note
  falls below "min frequency" and starts from A4 while the Chip
  follows the note.
- Idle settings are faded, wired ones greyed and locked, as the rows
  were. Every change is one undo step.
- Knobs take a bipolar range: 0 straight up, the taper bends each
  half. Slide, delta slide, duty sweep and arpeggio use it.
- The Compressor's auto-gain lever is now a Lever with a name of its
  own, which the Chip uses for "follow note".
- Presets give a Chip five columns, as a Filter has four since #385.

Found on the way:

- With the Chip this wide the Laser preset is about 3060 units. In a
  1280 px window Fit stops at the least zoom of 0.25 and cuts the
  Notes node off. The zoom limit is left as it is.

Tests:
- Unit: chip-view (9 new), knob (1 new), chip, chip-presets and
  hardware-view, 53 green
- e2e 34 on melektaus-atx: ALL PASSED, 131 checks (1 new). The
  first run was red on the check itself: a Laser's "min frequency"
  can lie above A4, and the screen then rightly says the note ends
  at once.
- Gate on melektaus-atx: CHECK EXIT 0, 285 files, 4377 tests
- Counter-check: a square drawn with its duty mirrored turns the wave
  test red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:55:35 +02:00
adminandClaude Opus 5.5 c087993000 Edit a Filter in its node and resize EQ and Filter nodes (#385)
A Filter sounds like one band of the Parametric EQ, but it keeps what
the EQ cannot: linked numbers, the all-pass and +-40 dB. So it stays a
node of its own and gets the EQ's editor with itself as the one band.
The editors also stop running out of their room: what does not fit is
left out instead of being cut.

- The curve of the EQ is now BandCurve with a host the EQ and the
  Filter both fill. CurvePanel holds what their panels share.
- FilterPanel shows the Filter as one band. A drag of its handle sets
  frequency and gain as one undo step. A number fed by a link is
  greyed out and the drag leaves it alone.
- A double-click on the Filter's title opens the same in a tab of its
  own. Delete there does nothing, the node stays.
- The Filter is tapped like the EQ, so its node and its tab draw the
  spectrum of what passes it. A tap is two gain nodes per voice, the
  analysers are made only while someone listens.
- EqFace and FilterFace are one CurveFace now, given the panel.
- EQ and Filter nodes have a corner to resize them, drawn like the
  floating window's. The size is saved on the node as `size`, each
  drag is one undo step, the least is 240 x 120.
- In the node the help line is gone. The row of numbers comes in only
  once the curve keeps 120 px of height. A group of the row that does
  not fit is not drawn, the one on the right first.
- A frequency or dB label that would run into the one before it is
  left out.
- The all-pass is drawn flat. The curve's maths takes every shape a
  Filter has.

Found on the way:
- The instrument presets set nodes 240 apart. A Filter with its
  editor is about 700 wide and covered the Bitcrusher, so a Filter
  now takes four columns.

Tests:
- Unit: 8 new (filter band, resize command and save, fitting rules),
  stage lists with a Filter now hold its taps
- e2e 34 (atx): ALL PASSED, 130 checks (4 new)
- Gate (atx): CHECK EXIT 0, 284 files, 4367 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:30:00 +02:00
adminandClaude Opus 5.5 17f47f6956 Drop the Gain's engraved name and centre its knob (#384)
The plate was taller than its one knob needs, and the engraved
"GAIN" only repeated the node's title. Without it the plate is
square, and the knob sits in its middle.

- The plate is 160x160 instead of 160x220, without the "GAIN"
  engraving.
- The knob is centred on what it shows: the "DB" caption and the
  dial. Since #382 its number hangs inside the dial's open bottom,
  so it adds no height.
- CAPTION_AIR (4) takes out the empty space above the caption's
  capitals. Without it the knob sat a little too low.

Measured on a picture at zoom 0.8: 19 px of air above "DB" and
19 px below the number. The user's screenshot showed about 14
above and 35 below.

Tests:
- Unit hardware-view: 12 tests green. The layout test now asks
  for the same air above and below.
- e2e 34 (melektaus-atx): ALL PASSED, 126 checks.
- Gate (melektaus-atx): CHECK EXIT 0, 283 files, 4359 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:29:28 +02:00
adminandClaude Opus 5.5 01ee61868b Show a Gain node as an orange device with one knob (#384)
A Gain node now draws a small device in the graph, as the Compressor
does since #380: the brushed plate in orange, 160x220 graph units, with
one chrome knob between its inputs and its output.

- GainPanel and GainFace join the node faces. The knob drags, resets
  on a double-click and keeps the wheel for the graph (#383). Each turn
  is one undo step, a linked Gain (dB) greys it.
- The knob turns with a taper of 0.26, so 0 dB stands straight up and
  the range below -24 dB takes a fifth of the turn.
- drawPlate, engrave and the brushed look move out of CompressorPanel
  so both devices share them. The Knob takes an arc colour, because
  the orange arc vanished on the orange plate.
- No meter and no tap. Gain sits in every voice of an instrument, and
  a tap would add two audio nodes to each note for a display nobody
  asked for.

Found on the way:
- Knob.dialGlobal added half the dial unscaled. In a zoomed graph it
  pointed beside the dial. The Compressor's e2e only hit by luck.

Tests:
- Unit: 2 new for the Gain plate and its taper.
- e2e 34 on atx: ALL PASSED, 126 checks (1 new).
- Gate on atx: CHECK EXIT 0, 283 files, 4359 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:07:44 +02:00
adminandClaude Opus 5.5 79a04e6764 Edit a Parametric EQ in its node, let the wheel only zoom graphs (#381)
The small curve below the EQ node's rows (#375) only showed the bands.
Like the Compressor since #380, the node now shows the editor of its
tab between its inputs and its output, and is worked right there. A
double-click on the node still opens the same editor in a tab.

Two wishes for the nodes with a panel came on top and share the same
files, so they are in this commit as well: the numbers below the
Compressor's knobs read as plain labels (#382), and the wheel over a
node zooms the graph instead of turning a value (#383).

#381, the EQ in its node:
- EqPanel holds the curve, the band's numbers and the help line, taken
  out of EqEditor. The tab puts its one panel under a header, each EQ
  node has a panel of its own at 720 x 360 (EqFace).
- A press on the curve neither picks nor moves the node. A
  double-click on the curve adds a band, one on the title or the edge
  opens the tab.
- In the node a press on the Type or Slope button stays with the
  button, so the node is not picked or moved.
- The spectrum runs in the node while it is in the picture. The tab
  lets go of the tap once, like the Compressor's, and Editor.tick lets
  every unseen tab and graph go before any seen one listens.
- The count of bands moved from the tab's header into the band row,
  so the node shows it as well.
- The node picture of #375 is gone with its only user: NodePicture,
  EQ_PICTURE and NodeView's drawing of it.

#382, the numbers below the Compressor's knobs in its node:
- A Knob with typed: false shows its number as a centred Label without
  a box, in the panel's ink. The ratio reads 4:1 (ratioText).
- The label is as high as its text and sits up in the dial's open
  bottom, right under the ends of the scale.
- Label takes a colour of its own.

#383, the wheel in a graph:
- GraphCanvas takes the wheel on the way down and stops it there. It
  zooms over a node too, and no knob or curve in a node sees it.
- The tabs keep their wheel: a knob's step, a band's Q and the zoom
  of the EQ's dB.

Found on the way:
- A Menu opened below a button in the zoomed graph would have been
  placed by the button's own height. openBelow now takes the bottom
  edge as drawn.
- The band row did not lay out its labels after a text change when
  its own size stayed. In the tab a later resize hid it, in the node
  "Band 1" and the count were missing. The row now lays out itself.
- Handle and point positions for the checks were unscaled. They now
  go through toGlobal and hold at the graph's zoom.

Tests:
- Unit parametric-eq: 16 tests. The #375 tests now check
  eqCurvePoints and eqRangeOf directly. compressor-view: ratioText.
- e2e 34 (atx): ALL PASSED, 125 checks (3 new: the EQ panel and its
  ports, editing in the node, the spectrum in the node with the tab
  open). The Compressor node's check reads its numbers as labels and
  8:1. Both node checks now expect the wheel to zoom the graph and
  leave the ratio, the Q and the dB zoom alone.
- The unseen-tab check now leaves for the Scene tab, since the node
  in the graph listens to the same tap.
- Counter-check: the tab letting go of the tap every unseen frame
  stalls the node's spectrum with the tab open (1 frame against 22,
  probe on stein2).
- Gate (atx): CHECK EXIT 0, 283 files, 4357 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:40:11 +02:00
adminandClaude Opus 5.5 1f2b64fb4c Show a Compressor's panel in its node, its tab on a double-click (#380)
game-maker / gate (push) Successful in 4m2s
game-maker / suite (push) Successful in 16m14s
The panel of #379 opened in a window of its own and replaced the tab.
The user wanted the device in the graph instead, and the tab of #378
back on a double-click. The switch Hardware look goes again.

- A node can show a face instead of its rows (`NodeFace`). Its inputs
  sit on the left of it and its outputs on the right, in the middle of
  the height. Each side is as wide as its longest port name.
- A Compressor node shows the panel at full size, 820x280 graph units.
  Its settings and numbers are not listed as rows, the panel has them.
- The knobs and the lever work in the node. A drag, a notch of the
  wheel and a double-click are one undo step each. A press on them
  neither picks nor moves the node, and the wheel does not zoom the
  graph. The number below a knob is only shown, typing stays in the
  tab and the inspector.
- The meter and the LEDs run while the node is in the picture of a sound
  FX or instrument graph that is seen. Out of the picture, folded or
  hidden, the node lets go of its taps.
- The window of one size, `fixedSize`, `refit`, `keptSize` and the rule
  that kept a Compressor out of the tab row are gone again.
- A knob's drag as one undo step lives in `nodeParamDrag.ts` now, for
  the tab and the node. The numbers of a node and what auto-gain says
  moved to `compressorView.ts`.

Found and fixed on the way:
- The tab and the node listen to the same taps. The tab let go of them
  on every frame it was not seen, so the node's meter stood still while
  the tab was open. Now the tab lets go once, and in a frame everything
  unseen lets go before anything seen listens.

Tests:
- Unit: compressor-view (2 new), documents (the test of `keptSize` is
  gone with it)
- e2e 34 (stein2, melektaus-atx was off): ALL PASSED, 122 checks. The
  four checks of #379 are now four of the node: ports beside the panel,
  knobs and lever in the node, the needle when played (21.7 dB with the
  tab open), and the double-click opening the tab.
- Gate (stein2): CHECK EXIT 0, 283 files, 4356 tests
- Counter-checks: an old auto-gain number read as off turns exactly its
  unit test red. A tab that lets go on every frame leaves the node's
  needle at 0 dB and no LED lit (probe on stein2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:41:28 +02:00
adminandClaude Opus 5.5 9917253751 Open a Compressor as a device in a window of one size (#379)
The tab from #378 shows a Compressor as an editor. The user wanted it
to look like a device as well, after a brushed aluminium plug-in, with
a meter of the gain reduction instead of the pictures. The tab stays
and is switched to per node.

- Hardware look is a switch under Settings, on by default for new and
  existing Compressors. Only the editor reads it.
- The panel is 820x280 px and opens straight in a window of that size.
  The window has no sizing corner and no Dock. A pane says so through
  `fixedSize`, and `keptSize` keeps the window's middle when it
  changes to that size.
- A Compressor in this look never sits in the tab row. Opening one
  floats it, and switching the look on moves its tab into the window.
- The panel has chrome knobs (threshold and makeup large), a meter
  with a needle that moves like a VU's, an LCD with the name and the
  reading, a lever with a lamp for auto-gain, and LEDs for the level
  going out. Everything is drawn in code.
- The Knob takes a size and a chrome look, and leaves out its caption
  when it has no name, so the panel engraves its own.
- `autoGainOn` is now `switchOn`, because the new switch reads the
  same way.
- Where parts sit, the meter's scale, the needle's speed, the LEDs and
  the plate's grain are in `hardwareView.ts`, apart from the drawing.

Decided myself:
- The meter's scale runs by the square root, so the first dB is about
  as wide as the last nine. It is red from 10 dB on.
- The LEDs light one every 3 dB from -39 to 0 dB and fall by 20 dB a
  second.
- Switching the look changes the graph's fingerprint, like any other
  setting of a node. The sound FX is built again once.

Findings, fixed on the way:
- Pixi's `arc` goes on from the last point of the path. The meter's
  scale had a line from the corner of the face until each arc got a
  `moveTo` to its own start.
- A text put into a Graphics makes Pixi warn that only Containers take
  children. The LCD is a Container now.
- The first browser check read the needle at one moment and missed the
  short sound. The panel keeps the most its needle and LEDs showed.

Tests:
- Unit: hardware-view (10 new), documents (1 new)
- e2e 34 (melektaus-atx): ALL PASSED, 122 checks (4 new). Played,
  the needle reached 20.9 dB and 5 LEDs lit.
- Gate (melektaus-atx): CHECK EXIT 0, 283 files, 4355 tests (11 new)
- Counter-checks: a window that keeps its corner instead of its
  middle, and a linear meter scale, each turn exactly their test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 02:06:56 +02:00
adminandClaude Opus 5.5 2d4909d248 Fit the Compressor's tab to any size, make auto-gain a switch (#378)
Two things came up on the new tab. Floated and sized down, it spilled
its knobs out of its window, and sized up it left the window empty.
Auto-gain lifted far too much: it brought 0 dB back to 0 dB, which at
-40 dB, 4:1 and knee 6 is +28.1 dB, so a sound at -24 dB came out at
-6 dB. And as a knob from 0 to 1 it blended between makeup and
auto-gain, so at 1 the makeup knob did nothing.

Layout:

- The knobs stay on the bottom line, centred, with the help line
  above them.
- The levels fill the height left above. The curve is a square at
  their right, centred up and down. It grows with the tab from 180 to
  360 px and takes no more than 40 % of its width (picturesLayout).
- A floated window cannot be sized below what its pane needs. A pane
  says so in its minWidth and minHeight, and the Compressor's is every
  knob and the smallest pictures. Other panes keep 320 x 200.
- A pane put into an open window that is too small grows the window.

Auto-gain, as decided in #378:

- A Compressor's auto-gain gives back what it takes from a sound at
  -18 dB, so that one comes out as loud as it went in. At the numbers
  above that is +14.6 dB.
- Auto-gain is a switch on the Compressor, the Limiter and the
  Saturation alike, a setting and no longer an input. The makeup or
  output is added on top of it instead of being blended with it.
- A project from before reads a number as on above 0. Nodes never
  set keep their defaults: off on the Compressor, on on the other two.
- The tab shows the switch at the right of the knobs, with the dB it
  gives below it. With it on, the curve has a line at -18 dB from the
  curve up to the diagonal.
- Every level reading keeps the gain of its time. Before, the whole
  picture was read by the current makeup, so after a change the
  tinting of the last seconds was wrong or missing.

Findings:

- My first e2e run of the layout clicked Dock where it was before the
  window was sized up, so the window stayed open and the next check
  went red. The check reads the button again.

Tests:

- Gate (melektaus-atx): 282 files, 4344 tests (6 new)
- e2e 34 (melektaus-atx): ALL PASSED, 118 checks (2 new)
- Offline in Chrome: a sine at -18 dB through -24 dB and 4:1 came out
  at -17.92 dB with auto-gain on and -22.42 dB with it off.
- Counter-checks: the curve set at the top instead of the middle, and
  every reading read at a gain of 0, each fail exactly their test.
- Looked at the window in screenshots: at its least every knob and
  the switch show. At 1280 x 797 the levels fill the height, the
  curve is centred at the right and the knobs are at the bottom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:57:26 +02:00
adminandClaude Opus 5.5 1df38bd430 Edit a Compressor in a tab of its own, with knobs and levels (#378)
A Compressor had only its number rows in the node. Its numbers are
easier to set by feel on a dial, and what it does is easier to judge
when the level going in and the level going out are seen side by side.

- Knob is a new widget in the toolkit: a slider bent into a circle,
  its name above and a number field below. A drag up or down turns
  it, Shift a tenth as far, a notch of the wheel a fiftieth of the
  turn, a double-click sets the default, the number takes a typed
  value. A taper gives the low end more of the turn.
- The Compressor's tab opens on a double-click, one per node, like
  the Parametric EQ's. It has a knob for each of the seven numbers,
  greyed while its input is linked.
- Its curve shows level in to level out from -60 to 0 dB. The point
  where it bends sets the threshold, the end of the curve sets the
  ratio that ends there.
- Its levels show the last 4 s on both sides of the node. Where it
  takes more than 0.5 dB away, the stretch below the level coming in
  is tinted. The makeup is left out of that.
- The Compressor is tapped like the EQ. SpectrumTaps listens to more
  than one node at once and reads the loudest sample of the last
  25 ms on each side.
- The node tabs share one map in the editor (nodeTabs), so a third
  kind needs no new plumbing.

Findings:

- CI task 614 for 23922ee was red on the memory check of the stream:
  the tab's renderer grew by 94 MB with nothing decoded. Play mode
  made garbage of its own and was read before a collection. Both
  readings in play now come after one, and the check compares the
  growth streamed with the growth decoded (3.7 MB against 59.8 MB).
- The ratio handle sat on the right edge of the picture, where a
  widget's hit area ends. The curve is inset by the grab radius.

Tests:

- Gate (melektaus-atx): 282 files, 4338 tests (17 new)
- e2e 34 (melektaus-atx): ALL PASSED, 116 checks (6 new)
- Counter-checks: an inverted ratio search, stop() letting go of
  every node and a drag without its least step each fail exactly
  their test.
- Looked at the tab in a screenshot: a blip at -4.4 dB came out at
  -16.7 dB, the gap tinted, the threshold line at -24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:40:12 +02:00
adminandClaude Opus 5.5 23922ee4a2 Show the zoom of the music tabs and the EQ as a transition (#377)
game-maker / gate (push) Successful in 3m58s
game-maker / suite (push) Failing after 14m51s
The zoom transition of #309 left out the Pattern, Song and Sound tabs
and the Parametric EQ's curve. There a wheel step or Fit still jumped,
whatever "Zoom (ms)" in the general settings said.

- axisTween wraps ViewTween for a single axis. The music tabs zoom
  their time across and their rows, tracks or height down apart, so
  each keeps one tween per axis and either goes its own way.
- Fit by its button is shown as a transition too, as in the other
  tabs. The fit on opening a document still jumps.
- A middle-button pan first lands a transition under way, and saving
  a document's view lands it on the step it was heading for.
- The EQ's range is saved on the node at once. While the zoom is on
  its way the curve draws a range of its own and writes nothing.
  Its dB lines are those of the new range, sliding to their place,
  so no line shows a crooked number.

Found on the way, not part of #377:
- CI task 611 for 82e96e6 failed the stream check of e2e 34. The
  renderer had grown by 60.1 MB with nothing decoded. The check
  summed the RSS of every Chrome renderer, and the test Chrome runs
  four of them. It now measures the largest one, the tab's own, and
  writes all of them into the check's detail.

Tests:
- view-tween.test.ts: 18 tests, 2 new. They fail when axisTween
  writes the wrong offset.
- e2e 34, 64, 65 on melektaus-atx: ALL PASSED, 202 checks, 4 new.
  At 1000 ms each zoom is between its two steps a frame after the
  wheel and lands on the step a jump reaches.
- Stream check: the tab's renderer grew by 8.6 MB streamed and by
  66.9 MB decoded.
- Gate on melektaus-atx: CHECK EXIT 0, 280 files, 4321 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:28:50 +02:00