Commit Graph
27 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 bf27e42e8a Let a shader declare a color the sprite sets (#298)
game-maker / gate (push) Successful in 3m1s
game-maker / suite (push) Successful in 14m11s
A shader could only ask its sprites for numbers. A tint, an outline
colour or a glow colour had to be built from three Property nodes.

- New node Color property next to Property: Name and Default (a
  colour, white unless picked), output `color`. It becomes a colour
  row below the Shader dropdown and a vec3 uniform in the program.
- Number and colour properties share one set of names. A clash is
  the existing "Another property is named" problem.
- Like Color it cannot feed the Vertex output.
- Placed from the palette it is called tint, then tint2.
- The preview in the Shader tab shows a colour field for it. Like
  the sliders it starts at the sprite's value and never writes into
  the project.
- `declaredRows` takes the row builder, so the particle graph keeps
  its number rows unchanged.

Tests:
- Gate: 222 files, 3452 tests (3445 + 7)
- e2e 28: 89 checks (28l-v: default colour drawn, then the colour
  the sprite sets)
- e2e total 1704, counted (1703 + 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:43:54 +02:00
adminandClaude Opus 5.5 4716726b6b Let a shader move the corners of the picture it wears (#79)
A shader could only say what colour a pixel has. Where the picture
is drawn was fixed to the four corners of its rectangle, so nothing
could wave, bend or wobble.

- The new node Vertex output takes x and y, in pixels of the
  picture, and moves every corner of the wearer's grid by them. The
  UVs stay with the corners, so the picture bends along.
- Subdivisions on that node says how fine the grid is: cells per
  axis, 1 to 64, 8 by default. 1 is the plain quad. A shader without
  the node keeps the quad every sprite shares.
- A graph holds one Vertex output at most. UV, Time, Property,
  Number and the math nodes may feed it, and one node may feed both
  outputs.
- Texture color, Texture alpha, Sample texture, Texture size and
  Color cannot feed it. The node says "Not available per corner"
  and the shader does not compile.
- Sprites, texts and the preview in the shader tab wear the grid.
  A nine-slice sprite and particles draw as if the node were not
  there.
- Picking, the selection frame, the size and colliders stay with
  the rectangle. A wearer with a Vertex output is never culled, as
  with Padding, and with Padding the grid covers the room around
  the picture too.
- tools/perf-shaders.mjs takes GM_PERF_SUBDIVISIONS to measure a
  grid against the plain quad.

Measured on stein2 (Intel HD 630), frames per second, one run per
row: at 800 sprites 58 with a shader on the quad, 46 with 8 cells,
41 with 32. Up to 400 sprites a grid hardly shows. Every sprite
holds its own grid.

Tests:
- Gate on melektaus-atx: 220 files, 3416 tests, CHECK EXIT 0.
- 15 new unit tests for the compiler and the grid.
- Four new e2e checks in modules 28 and 30: a fixed offset moves
  the picture, 8 cells bend only the lower half where 1 cell shears
  it all, Texture alpha is refused at the node, a text moves.
- e2e 28,30 on melektaus-atx: 110 checks, all pass.
- The e2e total of 1699 is counted (1695 + 4), the full suite did
  not run.
- No counter-probe, no screenshot. Not looked at: the preview, Time
  in the browser, flipped sprites, Padding together with the grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:14:00 +02:00
adminandClaude Opus 5.5 e933fad446 Give a shader room to draw around the picture it wears (#69)
A shader ran only inside the rectangle of what wears it: the quad
has the size of the sprite, and outside of it no fragment runs. An
outline or a glow built with Sample texture was cut off at the
border. The issue still spoke of Filter.padding, but since #77 the
shader is no filter any more, the limit is the geometry.

- The Output node has a number Padding next to Edge: so many pixels
  of the picture per side the wearer draws beyond it. 0 is the
  default and draws as before, 256 is the most.
- The quad grows and its UVs grow with it, so the picture keeps its
  place and size. In the margin the UVs run on below 0 and above 1.
- Transparent is a fourth Edge mode: outside of the picture there
  is nothing. It is cut in the fragment (texelAt), so it needs no
  copy of the picture with an address mode of its own.
- Edge holds everywhere alike, also for Texture color, Texture
  alpha and unconnected inputs.
- Padding above 0 with Edge on Clamp puts the hint "Padding with
  Clamp smears the border" on the Output. The shader still
  compiles. A chosen Clamp and the default cannot be told apart.
- All four wearers draw the margin. A sprite counts in pixels of
  its frame, a text in its units, a particle source grows every
  particle about its centre.
- A nine-slice sprite gets a ring around its nine fields, as wide
  as so many border pixels. The fields themselves stay.
- Without a picture a pixel is one unit of the wearer's own size,
  or of the particle size.
- Particles now take the Edge mode of their shader. It had no
  effect on them before.
- An object wearing a padded shader is never culled, or its margin
  would vanish at the edge of the screen.
- The preview in the shader tab shows the margin and leaves room
  for it.
- Picking, the selection frame, the size and the collider stay with
  the rectangle without the margin.

Tests:
- Gate on melektaus-atx right before this commit: 216 files,
  3336 tests (3319 + 17), exit 0.
- e2e 28,30,31,37,41 on melektaus-atx: 178 checks, all pass.
  e2e 1-37,47,48,58 ran before the last change (no culling with
  padding): 1281 checks, all pass.
- 17 new unit tests: the compiler (padding, Transparent, the hint)
  and the geometry (quad, nine-slice ring).
- Seven new e2e checks: four at the sprite in module 28, one each
  for text, particles and nine-slice.
- 17 unit counter-probes all fail their tests. Three browser
  counter-probes fail their checks (5, 3 and 1 of them).
- Three screenshots looked at: the sprite with Clamp and margin in
  the scene, the preview in the shader tab, the text.
- The e2e sum of 1687 is counted (1680 + 7), the full suite and
  the smokes did not run.
- Not seen: the hint at the node in a picture, particles and
  nine-slice in a picture, the game view while playing, sprites
  without a picture, vector and pixel art pictures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:33:20 +02:00
adminandClaude Opus 5.5 f02adabcf9 Show the type an arithmetic shader node really puts out (#219)
The result of Add, Multiply, Mix and the other arithmetic nodes of
a shader graph was drawn as any, whatever went in. The compiler
already lifts a colour on any side to a colour result, so the pin
said less than the code knew. A colour result could also be wired
into a number input, and the shader then failed to compile.

- liftedType in core/shader/nodes.ts is the one rule. The compiler
  reads it when it lifts two operands, and the editor reads it
  through liftedPortType for the ports it draws.
- A SharedPortGroup may bring its own rule as agree. Without one
  it is agreedType, as before. The thirteen arithmetic nodes share
  their type by liftedPortType.
- Only the any inputs decide. An input nothing feeds counts as the
  number typed into it. t, edge, exponent, min and max have no say.
- The type travels through a chain of such nodes without code of
  its own, because #210 hands a shared type on.
- In a ring the result stays any, unless a colour has settled it.
- A number may feed a colour input. Without that a number result
  no longer fitted Output.color, although the compiler spreads it
  over three channels. The rule sits in portsCompatible, but only
  the shader graph has colour ports.
- A colour result no longer fits a number input. An edge laid
  before the colour came in stays and is drawn in the danger
  colour, and the node at its end names the problem.
- The nine shaders in the stored projects (108 edges) all stay
  valid under the new rule.

Tests:
- Gate on melektaus-atx: 212 files, 3260 tests (3240 + 20), exit 0.
  The gate right before this commit ran on the tree of this and
  the next commit together, with the same numbers.
- e2e 28 on melektaus-atx: 81 checks, all pass.
- e2e 1-31: 1059 pass, 1 fail in module 18 (the editor clock read
  1.97 s after a stop). A rerun of 1-18 passed with 602 checks and
  0.18 s. 1-31 was not run in full again.
- No full run and no smokes. The sum of 1676 (1673 + 3) is reckoned.
- Twenty unit tests in shader-port-types.test.ts, one of them runs
  all thirteen nodes through every mix of number and colour and
  compares what is drawn with the code made.
- Two tests in shaders.test.ts now lay the edge first and bring the
  colour in after, the only order that still wires such a graph.
- Three e2e checks in module 28 with the real mouse: a colour into
  Multiply, the red edge with its message, and the colour result
  refused at alpha and taken at color.
- Fifteen unit counter-probes and two browser counter-probes all
  fail their tests.
- Not checked in the browser: the one-input nodes, Mix and the
  ring, and a number wired straight into a colour input. Nothing
  was looked at after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:50:50 +02:00
adminandClaude Opus 5.5 393caadc9d Head the groups of the inspector with bands that fold (#290)
game-maker / gate (push) Successful in 2m39s
game-maker / suite (push) Successful in 13m10s
The groups of the inspector had a small grey label as their title, in
the same grid as a property row, so with many rows one group hardly
stood apart from the next. Now every group has a band as its head,
from edge to edge of the panel, and a click on it folds the group.

- A band runs across the padding of its column in the header colour,
  with the title and a mark that says whether the group is open. The
  titles stay as they are, as answered in the issue.
- A click on a band folds its group: the rows go, the band stays and
  the next band moves up against it. A control in the band (Open,
  + Variable, + Property) does its own job and folds nothing.
- A placed object has a group SWITCHES under its name, asset and
  parent: Enabled, Keep on scene load and the switches of a behavior.
  An asset has SWITCHES and PROPERTIES, and a group without rows is
  left out.
- The same bands head the tilemap panel, the Variables tab, the side
  of the Pixel art tab and the shape list of vector art. A list with a
  title (Layers, Frames, Shapes) folds with a click on its header,
  clear of its plus.
- ui/sections.ts holds the rules as pure functions, SectionStack is
  the column and SectionHeader the band. Every title of the inspector
  goes through addTitle or sectionHeader in inspector/host.ts.

Decided here:

- A fold is kept for good, in localStorage of this browser. The issue
  asked whether for the session or for good and the answer left it
  open.
- A fold is remembered by title, per panel. PROPERTIES folded once is
  folded on every object, asset and node. A number in a title does
  not count.
- A behavior asset has all its rows under SWITCHES. A Variables asset
  and a state machine get a group SCOPE.
- A greyed out control in a band takes the click too. Without that the
  click fell through to the band and folded the group.
- The Curve tab has no column of this kind and is unchanged.

Found on the way:

- The inspector of a sprite is taller than the 800 pixel window of
  the checks now. The hooks that hand out a row scroll it into view
  first, and module 28 scrolls before it looks for a row by its text.

Tests:
- Gate: 202 files, 3008 tests (2982 + 26), exit 0
- e2e: no complete run for this state. Modules 28 to 61 passed alone
  (749 checks). A full run was cut off in the accounts module at 1492
  passes without a failure, the modules after it and the smokes are
  missing. The sum of 1639 (1635 + 4) is reckoned, CI measures it.
- 26 unit tests in tests/editor/sections.test.ts
- Four e2e checks with a real mouse, three in module 02 and one in
  module 48
- 25 unit counter-probes all fail tests, run on stein2
- Screenshots of the bands looked at before the last build, none
  after it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:08:28 +02:00
adminandClaude Opus 5.5 17a6fd0339 Move the shader test sprite off a name plate so CI passes again
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Successful in 9m24s
Every suite run since 50bddf6 failed the same 14 pixel checks in step
28. The test sprite sat right of the camera, where the Crate pair
container from an earlier step has its symbol. Since 50bddf6 names
stand on dark plates, and that plate reaches into the sprite's upper
left corner. The thin grey name before let the sampled pixels through.
The plate covers them.

- The sprite now sits up left of the camera (-190, -105 instead of
  +180, +100), a spot no earlier step fills.
- The guard that samples a 9x9 grid over the sprite stays. It named
  the plate as -0.4,-0.3:#1b2f57, the sprite blue under a 70 percent
  near-black plate.
- The app is right: symbols and their names draw above objects.

Tests:
- Reproduced in the CI image with an empty data folder, GM_STEPS=1-28:
  the same 14 FAILs with the same colours as CI tasks 303 to 313.
- Full CI job in the CI image after the move: ALL PASSED, 1385
  checks, smokes 24 / 19 / 21, E2E EXIT 0, SMOKES EXIT 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:34:10 +02:00
adminandClaude Opus 5.5 2cf72022eb Ping an asset from its inspector link instead of selecting it (#221)
game-maker / gate (push) Successful in 2m52s
game-maker / suite (push) Successful in 9m2s
A single click on an asset link used to select the asset. That swapped
the inspector over to the asset, so the object being edited was gone
and the second click of a double-click landed on a different row. A
click now pings the asset in the asset panel, like Unity does in its
Project window, and a double-click opens it.

- state.pingAsset(id) emits a new `ping` event. It runs no command and
  leaves the selection alone, like openAsset.
- ListView.ping(id) draws an accent overlay on the row. It stays full
  for 200 ms and is gone after 1 s, driven by onFrame. destroy cancels
  the animation.
- The asset panel expands the group, scrolls to the row and pings it.
- The capture listener and followedLink in the inspector are gone.
  The inspector is no longer rebuilt, so pressCount on the label is
  enough. followsPress and pressOf are no longer exported from ui/.
- The "Asset" and "Part of" rows of a scene object were a ghost button
  that selected the asset. They are now a link label with the same
  ping and open behaviour, next to a read-only field with the name.
- The link logic lives in linkLabel() in fieldFactory.ts. createFieldRow
  and the asset row share it.
- TextField gets a readOnly option: no focus, no caret, no hover.
- New hook __gm.assetPing(). inspectorLink also reports the field
  next to the label (value, readOnly).

Findings:
- Renaming an asset while its object was selected left the old name
  in the inspector. This was already the case with the old button.
  The refresh now updates the name.

Tests:
- ./check.sh: 151 files, 2228 tests, CHECK EXIT 0 (before 150 / 2226).
  New: tests/ui/list-ping.test.ts for pingAlpha and pingAsset.
- e2e 1-16,28 on the final build: ALL PASSED, 575 checks.
  28l-ii has 9 checks instead of 5.
- Counter-checks, each failing exactly at its check: ping also selects,
  no follow-up frame (alpha stays 0.55), inspector never opens, field
  takes focus, name not refreshed, label not a link.
- Check total 1345, calculated. CI last counted 1336 at fd6af84.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 17:55:31 +02:00
adminandClaude Opus 5.5 1aae97b335 Open a sprite's texture or shader from the menus and inspector (#221)
game-maker / gate (push) Successful in 2m52s
game-maker / suite (push) Successful in 8m58s
A sprite's texture and shader were only reachable by hunting for them
in the asset panel. Every place that shows such a reference now leads
straight to it.

- Object menu (hierarchy and scene view): "Open texture" and "Open
  shader" sit right below "Open asset" when the sprite has one. The
  object's own values count, so an overridden shader opens the
  override.
- Asset context menu: the same entries at the top.
- Inspector: every asset field with a value shows its label as a blue,
  underlined link. One click selects the asset, a double click opens
  it. An override stays gold and gets the underline too.
- The first click rebuilds the inspector, so the second one lands on a
  different row. The inspector catches that second click before any
  row sees it.
- New pure rule `references.ts`: the asset behind a reference field. A
  frame leads to its sheet. Kinds without a tab (palette, font, sound)
  get no menu entry.
- `followsPress` in `clicks.ts` keeps the double-click window and
  tolerance in one place.
- `activateAsset` in `actions.ts`: opening means the same everywhere
  as a double click in the asset panel, so a scene also becomes the
  active one. The asset panel uses it too.

Tests:
- ./check.sh: 150 files, 2226 tests, CHECK EXIT 0 (before 148/2214).
- 12 new unit tests: 6 for the reference rule, 4 for the link in
  createFieldRow, 2 for followsPress.
- 5 new e2e checks in 28l-ii: both menus, the link's look, click
  selects, double click opens.
- e2e 1-16,26,28,35,39,40 on the final build: ALL PASSED, 732 checks.
- Counter-probes: no "Open ..." entries, no underline and a second
  click never recognised each fail exactly at their checks.
- Check total 1341 (CI counted 1336 at fd6af84), computed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:44:19 +02:00
adminandClaude Opus 5 7063c8d074 Shader preview sliders show again after a property edit (#168)
game-maker / gate (push) Successful in 2m13s
game-maker / suite (push) Successful in 8m1s
Changing min, max or default of a property rebuilt the slider rows of
the shader preview, but the rows stayed at 0 x 0. Only the labels were
visible until the preview was resized by hand.

- Widget.resize only lays out when the size changes. The row count
  stays the same on such an edit, so the row stack kept its height and
  never laid out its new children.
- syncParams now relayouts the row stack itself after the column
  around it.

Tests:
- e2e 28n: a new max rebuilds the slider at 136 x 24, a click at its
  right end sets level 2. Red against the old build (w 0, h 0).
- GM_STEPS=28,31 fresh: ALL PASSED, 106 checks (was 105)
- ./check.sh: 132 files, 1952 tests, exit 0
- Check sum 1210 in README, computed (1209 + 1), CI measures it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 09:28:02 +02:00
adminandClaude Opus 5 73a71473f1 The shader tab previews the effect on the last picked image (#168)
game-maker / gate (push) Successful in 2m11s
game-maker / suite (push) Successful in 7m43s
A shader could only be judged by placing it on a sprite and running
the scene. The shader tab now shows its effect next to the graph, on
the image the user picked last, without a picker to maintain.

- The preview follows the last selection that has an image: a placed
  sprite with its overrides, size and tint, a sprite asset, a texture
  or a frame. Clicking the shader, a node, a behavior or a scene keeps
  the picture.
- Without such a selection, or when the picked image is deleted, a
  built-in 64 px test image is shown. Its colour runs with u and v, a
  checkerboard shows where a UV lands and the border fades for alpha.
- Each property gets a slider. The sliders belong to the preview and
  never write to the project. They start at the sprite's values when
  it carries this shader, otherwise at the default.
- The preview has its own clock. Hold and Restart are only enabled
  when a Time node is in the graph.
- The graph | splitter | panel layout of the particles tab moves into
  PreviewSplit, and both tabs use it.
- A nine-sliced frame is previewed unsliced. This is documented.

Findings:

- The slider row stayed squeezed to a few pixels when the properties
  changed while the tab was hidden. The layout was triggered one level
  too high. The slider check in block 28n holds this.
- The graph canvas is narrower now, 385 px at a 1280 px window, the
  same as in the particles tab. Block 28b clicked at a fixed x+420 and
  now uses the measured width.

Tests:

- ./check.sh: 132 files, 1952 tests (was 131 / 1941), exit 0
- npm run build: exit 0
- GM_STEPS=28,31 freshly seeded: ALL PASSED, 105 checks
- 11 new unit tests in tests/editor/shader-preview.test.ts
- 7 new e2e checks in block 28n
- Counter-probes: start values ignoring the sprite fail 1 unit test,
  a preview ignoring the selection fails 2 e2e checks
- Check total 1209 in README is computed (1202 + 7), CI measures it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:55:02 +02:00
adminandClaude Opus 5 4f546c0e08 Number fields drop trailing zeros: formatNumber still rounds to at most 3 decimals and then removes zeros at the end, so 100.000 reads 100 and 0.500 reads 0.5, everywhere (inspector, graph and shader nodes, duration and fps in the animation and tile tabs); a field's own decimals is now an upper limit, stored values are unchanged, -0 still shows as 0, and the graph's separate integer rule is gone (#172)
game-maker / gate (push) Successful in 1m55s
game-maker / suite (push) Successful in 6m50s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:29:00 +02:00
admin 7e5003b590 A test that waits for a deadline somebody guessed is waiting for the wrong thing, so it waits for the editor (#93)
game-maker / suite (push) Successful in 6m48s
The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.

The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.

Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.

564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.

Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
2026-09-16 08:08:10 +02:00
admin c990dbcb26 A name that a sprite already owns cannot name a property, and a division by zero cannot answer (#63, #64)
game-maker / suite (push) Failing after 11m29s
#63: a shader Property node named "width" quietly took over the sprite
row of the same name. isSpriteProp lives in PropertySchema now and is
derived from FULL_SCHEMAS.sprite instead of being a second list that
drifts. isValidPropertyName rejects those ten names, and the compiler
reports it at the node as its own problem: "width" is a sprite
property, told apart from "not a word" and from a duplicate.

#64: Divide emitted (a / b) and handed NaN to the driver. It emits
divide(a, b) now, with the guard in the prologue next to straight(),
scalar and per channel: zero divided by zero is zero. That was already
the house rule in the behavior and particle graphs - only the shader
had never been told. Clamp comes along as a shader node for the same
reason: the behavior catalog has had it all along.
2026-09-15 18:43:40 +02:00
rootandClaude Opus 5 0e0a21dee1 One clock under everything that moves by itself (#90)
game-maker / suite (push) Successful in 10m4s
Particle age and shader time each read performance.now() off their own
library, and the world got the frame's delta straight from the ticker. Three
clocks, so a global time scale would have been three multiplications and a
pause three separate freezes.

core/time.ts holds one clock instead: it accumulates dt x scale per frame
rather than reading the wall clock, and Editor.tick hands the very step it
took to the play session, the editor run and the animation preview. Delta
time, particle age, shader time and that preview are the same quantity now,
so the scale is a single multiplication and a pause is that scale at 0 —
nothing underneath needs rebasing, because the clock itself stands still.
The Math.min(dt, 0.1) caps in PlaySession and EditorRun are gone: the cap
(MAX_FRAME) now sits on the raw frame before scaling, and capping twice would
make the world run slower than the particles at scale 2.

A pause gates the scale rather than overwriting it — effectiveScale() answers
0 while paused, and for anything that is not a positive finite number, while
gameClock.scale keeps its value. A slider later survives a pause that way;
the issue asks for none yet.

The clock goes back to zero at both ends of a run, so a run is reproducible
from second 0 and the scene view looks freshly loaded after Stop. Resetting
only on Stop would not do it: the editor clock runs on afterwards, and the
next Play would start at "seconds since the last Stop".

The trap in that: ParticleField keeps its origin as an absolute second of
this clock, so after a reset it lies in the future and every particle would
age backwards. One line in sync() starts such a field over, which covers the
scene, the game and the tab preview at once — and fixes an older bug of the
same shape, where releasing a particleTime() freeze left the preview empty
for good.

Found while measuring that: ParticlePreview stood at second 0.5 by computing
the clock reading that would mean 0.5 since the field started. Over 200000
random origins the round trip misses the last bit in 18.8% of them and shows
one particle too few in 9.4%, because floor(0.5 * 20) comes out 9. The check
had been flaky since it was written. The panel hands the field the age itself
now (ParticleField.hold), and the faked clock is gone.

833 checks (824 + 9), 931 unit tests (921 + 10).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 09:31:53 +02:00
admin e0c459ce92 Shader graph: Clamp is the Output's default edge, and the sheet's own mode is a constant of its own (#81)
game-maker / suite (push) Successful in 9m16s
The default of the Edge param and the address mode the shared sheet physically
carries were the same constant; changing the value would have turned every
sentinel with it and given each shader sprite a full copy of its texture.
SHEET_EDGE ('mirror', what TextureLibrary uploads) is now separate from
DEFAULT_EDGE ('clamp'). A graph whose Sample reads nothing but its own pixel
draws with the plain sheet whatever the Edge says, so the common shader costs
no second copy.

Without a shader the quad's coordinates never leave 0..1, and mirror-repeat
reads like clamp within a texel - so clamp is what a sprite shows anyway.
2026-09-13 21:33:41 +02:00
rootandClaude Opus 5 86b5363a01 Fonts: a new asset type, picked from the server or dropped in, worn by text objects (#74)
game-maker / suite (push) Successful in 9m10s
A font asset holds a file and nothing else, the twin of a texture: the server keeps it
content-addressed under data/fonts/{sha256}.{ext}, with the format read from the file's own
four-byte signature (finfo reports fonts inconsistently across libmagic versions). A text
wears one through its ordinary Font property, so overrides, reset, undo and the Properties
node come for free; no font means the editor's own typeface.

The picker lists everything the server holds and renders each row in the face it offers.
The family name is gm-font-<hash16>, never the name inside the file, so two "Regular" cannot
collide. WorldViewport.syncText is the single place that draws it, which covers scene, game
preview and play at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:09:59 +02:00
rootandClaude Fable 5.1 28c0789b19 Shader graph: Abs node, the distance from 0 per channel
game-maker / suite (push) Failing after 8m10s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 18:46:20 +02:00
rootandClaude Fable 5.1 a399795772 Shader graph: Pow node, base held at 0, exponent broadcast for colours (#80)
game-maker / suite (push) Failing after 8m13s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 18:29:04 +02:00
rootandClaude Fable 5.1 102a649a35 Shader library: fingerprint without node positions, a drag no longer recompiles (#65)
game-maker / suite (push) Successful in 9m5s
shaderFingerprint(graph) covers node id/type/params/ports and edge endpoints only;
x/y, collapsed, folded and edge ids stay out. ShaderLibrary keys on it and counts
compiles (__gm.shaderCompiles()). Measured before: one compile per preview step of a
node drag (12 for a harness drag); after: 0, a param change exactly +1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 18:00:48 +02:00
rootandClaude Fable 5.1 016e83c9b5 Transform: scale, rotation and mirror as properties, world transform as a matrix chain, oriented colliders (#76)
game-maker / suite (push) Successful in 9m5s
Four new rows on sprite, text, collider and container (block TRANSFORM):
scale, rotation in degrees clockwise, mirrorX, mirrorY. As properties they
get override/reset/apply, loading of old projects, write-back after play and
the Properties node for free; camera and behavior have none.

The hierarchy composes T*R*S per level (composeAlongChain replaces
sumAlongChain), so children rotate and scale with their parent. Reparenting
and grouping keep the look (carriedTransform). Drawing goes through
placeNode, the gizmo is a polygon, cull and objectRect use the AABB, drags
are computed in the parent's system.

Colliders are oriented boxes: SAT for box/box, box-circle in the box frame;
exitDistance replaces axisSeparation/axisCorrection, moveAndSlide moves along
the object's own axes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 17:25:06 +02:00
rootandClaude Fable 5.1 f85606427b Shader graph: UV, Sample texture, Texture size, Time, Sin/Floor/Fract/Step; edge mode on Output (#78)
game-maker / suite (push) Successful in 8m38s
Fragment-side coordinate nodes instead of vertex nodes: the quad stays the
quad, a UV read plus a sample at a computed coordinate covers flip, scroll,
wave, pixelate and scanlines. Time is a uniform only when a Time node reaches
the Output; a viewport that drew a Time shader redraws from the ticker so the
Scene tab animates without a running world. Edge (Mirror/Repeat/Clamp) is a
second texture variant in TextureLibrary because WebGL binds no TextureStyle
resource of the shader. The compiler now emits #version 300 es first: Pixi
only compiles as ES 3.00 when the fragment says so, and textureSize needs it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 16:18:22 +02:00
rootandClaude Fable 5.1 0e5e5ef925 Shaders draw in the sprite's own pass: a Mesh with the graph's fragment, no filter (#77)
game-maker / suite (push) Successful in 8m24s
A sprite's picture is now a Mesh (unit quad, sibling of its Graphics node, which keeps
only the hit area); a worn shader replaces Pixi's fragment in that draw, so there is no
intermediate texture and no extra render pass. Measured on the HD 630: 400 shaded
sprites hold 60 fps (was 34), 800 hold 56 (was 20).

Two Pixi facts the code leans on: a mesh's texture is bound only for Pixi's own shader,
so GraphShader.wear hands it over; and MeshPipe caches whether a mesh batches and
re-checks it only in a frame without structure changes, so switching a shader on or
off calls mesh.unload().

The console panel mirrors console.group/groupCollapsed/groupEnd and fills the %s %d
%o %c directives, so Pixi's deprecation warning shows its head, not only its stack.
E2E 12 guards against the addChild-on-Graphics warning, 28 counts shaders instead of
filters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 15:31:08 +02:00
rootandClaude Opus 5 82effb2c03 Shader performance: measure one filter pass per sprite (#67)
game-maker / suite (push) Successful in 8m23s
The wiki carried an estimate ("fine for dozens, not for thousands"). Measured
on the Intel HD 630 that CI uses, 48x48 sprites in a 640x360 camera, neutral
values, three runs agreeing to 1 fps:

  sprites  25  50  100  200  400  800
  fps      58  56   54   48   34   20

The same scene without shaders holds 60 fps at every one of those counts, 800
included. Quadrupling the sprite area (96x96) measures the same, so the cost is
the render target switch, not the pixels: roughly 20 us per pass up to 200,
40 us at 800.

tools/perf-shaders.mjs drives it (not run in CI, frame times are machine
dependent). New hook __gm.filterCount() counts visible nodes carrying a filter
and proves one pass per sprite; e2e 28 checks it.

Two traps the tool guards against, both cost a wrong answer first: the median
frame time is useless here (the distribution is bimodal under vsync, so the
rate comes from frames/duration), and --disable-gpu-vsync has no effect in
headless, so what is measured is the point where the frame rate breaks down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:41:35 +02:00
rootandClaude Fable 5.1 ccc6dc5b6e Shader graph: Number node, a typed constant like Color (#72)
game-maker / suite (push) Canceled after 6m55s
No input, one Value param (step 0.05, drawn on the node), output `value`
as a float literal via floatLiteral (2 -> 2.0, NaN -> 0.0). No uniform,
no sprite row - that is the difference to Property.

The e2e negative check for "no behavior node in the palette" now uses
Boolean as its marker, and step 28 types the palette search first
because the palette scrolls from twelve entries on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 12:26:53 +02:00
rootandClaude Fable 5.1 3da0ef1be2 Shader graph: alpha input on Output, Texture alpha node (#70)
game-maker / suite (push) Successful in 7m55s
Output gains a number input `alpha`; unconnected it reads the texel's
alpha, like `color` reads the texel. The epilogue premultiplies with the
resolved alpha. New `Texture alpha` node exposes texel.a for maths.

PortDef.inline=false keeps the alpha port without an inline field: every
number port is inline by default, and a typed default of 0 would have made
every shaded sprite invisible. The compiler's texel fallback is typed:
colour ports get rgb, number ports get texel.a.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 11:51:59 +02:00
rootandClaude Fable 5.1 a32b7c6daa Adjust colors: brightness adds instead of multiplying (#62)
game-maker / suite (push) Failing after 4m1s
The shipped shader graph gains a `brightness - 1` offset node, so 1 stays
neutral and brightness lifts every channel by the same amount; contrast keeps
multiplying, saturation keeps mixing with the luma. Fixture, seeds, unit and
e2e expectations follow (twelve nodes, 1.25 -> #80c0ff).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 22:58:50 +02:00
rootandClaude Fable 5.1 42cafa7543 Shader assets: graph-built shaders, the Shader tab and sprite rows from the graph (#62)
game-maker / suite (push) Failing after 3m59s
A shader is an asset carrying a node graph (Texture color, Output, Property,
Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL
fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite
wears a shader through its Shader picker, and the rows below it are the
graph's Property nodes. Property schema, resolution and deviation take the
project because a sprite's rows now live in another asset. The built-in
registry of the prototype is gone; "Adjust colors" ships as a graph in the
fixture and the three example games.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 22:41:14 +02:00