5fde3e975ee70b2451718a886dd8ddac64ad7672
516
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5fde3e975e |
Reset a track slider with a double-click in the Song tab
The heads of the Song tab had four rough spots. Panned down, the first track had no line above it. The M and S letters sat high in their buttons. A volume or pan slider could only be dragged back to the middle by eye. Muting a track paled its volume slider. - The first track draws a border along its top, in the lanes and in its head, like the one below every track. - M and S sit in the middle of their buttons. A label of the heads can now carry a height and is centred in its box. - A double-click on a head's volume or pan sets it back to 0: the volume to 0 dB, the pan to the middle. It is one undo step of its own after the step the first click set. - Mute only lights its own button. The volume slider keeps its colour, so a muted head no longer looks out of step. Tests: - e2e 65 on melektaus-atx: ALL PASSED, 27 checks (+1) - typecheck, ESLint, Prettier: clean (also after the mute change) - screenshot on stein2: border above track 1, M and S with 4 px free above and below the letter in a 16 px button Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5b96d0b329 |
Paste under the pointer and pull song items out left in music tabs
A round of fixes to the Pattern and Song tabs: where a paste lands, what Shift does on a drag, how far the rows zoom, and how an item of a song is sized at its ends. - Shift does what Alt does on a drag: off the grid, on the tick. A Shift click on a note or an item still adds or unpicks it. A Shift press only decides on letting go: no drag toggles, a drag moves. - Ctrl+V lands under the pointer. The earliest copy goes on the grid line before it, the highest note on its row or the topmost item on its track, held to the keyboard or the tracks there are. Off the grid or left of bar 1 it lands at the start of playing as before. - The rows of the piano roll zoom to 64 px instead of 28, the tracks of a song to 320 px instead of 160. - Fit in the Pattern tab zooms the rows as well, so the notes fill the height, no longer held to the default of 14 px. - An item of a song can be pulled out by its left end past the start of what it plays, which then repeats. The offset goes round to the end of a round. A sound not loaded yet still stops at its start. - Both ends of an item snap to the start of a round of what it plays as well as to the grid, whichever is nearer. The end of a round between two ticks is cut to the tick before. A start put within half a tick of a round goes right on it, so it plays from the very beginning. - An offset past a whole round is not folded back to 0, so a pattern that grows later still plays from where the item said. - A middle-drag pans past the first and the last row, as it pans left of bar 1: the highest note or the first track up to half the height down, the other end up to half the height up. The room past them is dark in the piano roll and empty in the song. Tests: - Unit: song-edit 14, piano-roll 19, song 14 tests. New cases for the paste spot, pulling out left, the round snap and Fit. - e2e 64+65 on melektaus-atx: ALL PASSED - 54 checks (+4). New: Shift drag and Shift click, Ctrl+V under the pointer in both tabs, Fit zooming the rows, pulling a pattern out left and snapping a sound to the end of its second round. e2e 64 zooms the rows out again after Fit, so the keys it plays later are on screen. - Gate on melektaus-atx: 251 files, 3912 tests, CHECK EXIT 0. - Then the pan down the rows: e2e 64 on melektaus-atx ALL PASSED - 30 checks. Its pan check now also pans the top row down, half the grid's height, and a click above it draws nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a3101c590c |
Size notes by either end and pan before bar one in music tabs
A round of small fixes to the Pattern and Song tabs: what the ruler and a pan do in both, and a few things the tabs carried that were in the way. - A middle-drag can take bar 1 up to half the grid or lanes in. The time before it is drawn dark on the ruler, the grid and the lanes. Nothing lands there: a click draws no note, a drop shows no frame. - A press or drag on the ruler puts the start on the nearest grid line, no longer on the one before the pointer. - A new song has one track instead of four. The "+ Track" row under the heads is gone, a head's menu adds tracks above or below. A song still keeps at least one, so that menu is always there. - A note can be sized by its left end too: the start goes to the nearest grid line and the end stays put, never before tick 0. - An end of a note under the pointer lights up and shows a resize cursor, as on an item of the Song tab. Tests: - Unit: piano-roll, song-edit, song, song-play 52 tests. Two new cases for the left end and the pan, the song fixtures add their tracks themselves now. - e2e 64+65 on melektaus-atx: ALL PASSED - 50 checks (+2). e2e 65 adds its tracks through the head's menu and clicks the ruler three quarters into a bar, which only the nearest line passes. - Gate on melektaus-atx: 251 files, 3909 tests, CHECK EXIT 0. - A probe shot of the piano roll panned in: the time before bar 1 dark, the left end of a note lit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aa097bd076 |
Zoom with the wheel and set the loop on its own strip in music tabs
The ruler of the Pattern and Song tabs did two things with one drag,
and the wheel did three things with Alt and Shift. Both now do one
thing each, and the Song tab gets a few edits it was missing.
- The ruler has a loop strip along its top. A drag there sets the
loop, a press or drag below it moves only where playing starts.
- The wheel only zooms: the time over the grid, lanes and ruler, the
rows over the keys, the tracks over their heads. Alt and Shift no
longer change it. A middle-drag still pans.
- Tracks can be zoomed from 28 to 160 px. Below 46 px a head shows
only its name, M and S. An old saved view gets the old height.
- A click on nothing in the lanes picks the song itself.
- A right-click on an item opens a menu with Delete instead of
deleting it. The inspector has no delete buttons any more.
- A sound item goes up to +12 dB, a track up to +12 dB. A pattern
item stays at 0 dB, since a note is held to unity.
- An end of an item under the pointer lights up and shows a resize
cursor. A long item marks every point where it starts over.
- The Pattern tab has an Audition switch in its header, off at first.
Only with it on does a note sound when drawn, grabbed or moved.
The keys down the left sound either way.
- A looped sound item draws every round alike. Each round is drawn
from its own start, and each column shows the loudest part of the
sound it covers instead of one sampled bucket.
- A song is at least one bar long instead of four. An empty song
fits that bar across, and Play song ends after the last item.
- The ghost notes held back in #334 got their own issue, #339.
Finding: suite 542 for
|
||
|
|
f97961c8ff |
Play and stop a song from a behavior in the game #336
A song could only be heard in its tab. Now a game plays it through Play song and ends it through Stop song, with its tracks as they are set. A playing pattern or song also no longer undoes the pause of the toolbar. - Play song plays from the start of the song and then round the loop of its ruler, so an intro is heard once. Without loop it plays once to its end. Inputs are song, volume, fade in, loop (on by default) and detached (off by default). It gives out playback. - Stop song stops one song, every song for none, or one playback by its number, over its fade out. Stop sound with no sound named stops songs too. Stop pattern with no pattern named stops only patterns. - Songs play side by side, as patterns do. They are owned like a sound, so leaving the scene ends one unless it is detached. Pause game and the time scale leave the music alone. - Each playing is heard through a master of its own, silent until its first note. The fades move it, and a song's tracks sit inside it on buses of their own. They go once the playing and its voices have. - Notes and clips are struck the same way in the editor and the game (strikeNote, playClip). What a sequence needs comes from one place (core/music/needs). Findings: - Reading the audio clock resumed a suspended context. A playing pattern read it every 25 ms and so undid the toolbar's pause. The library now leaves a context alone while the toolbar holds it. - A pattern played once to its end asked the list of playings about its voices, and that list asked the pattern again. Once its last voice was gone this ran until the stack overflowed. The scheduler of a playing now asks only after voices. - e2e 64 reads the playings through __gm.sound.playbacks() now. Tests: - song-play.test.ts (new): 8 tests, node catalog +8 cases - sound-library.test.ts: 66 tests, 4 new - e2e 64 (melektaus-atx): 23 of 23 checks passed - e2e 65 (melektaus-atx): ALL PASSED, 22 checks (+4) - Gate (melektaus-atx): CHECK EXIT 0, 251 files, 3903 tests - Counter-checks: the old way of asking after voices, a clock read that wakes a held context, and a start after a fade out began each fail 1 test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8f9c250eb6 |
Lay patterns and sounds out on tracks and play them as a song #334
Patterns could only be heard one at a time, in their own tab or from a behavior. A song lays patterns and sounds out on tracks along one time axis and plays them through the scheduler of #332, the third piece of the music tools of #330. - New asset type song under the heading Songs, with four empty tracks, 120 BPM in 4/4 and a grid of a bar. Tempo, meter and grid are rows in the inspector. Tracks, items and the loop are the asset's data, held to their rules on load and in every command. - A track has a name, volume, pan, mute and solo. Each is heard on a bus of its own, so a change reaches what is sounding at once. - A pattern item repeats its pattern for as long as it is, with a transpose and another instrument if wanted. A sound item plays its sound as a clip at its own speed, round and round, through a sound FX if wanted. Both have a volume on top of their track's. - Cut back at the left end or split, an item starts as far into what it plays as the cut is: ticks into a pattern, seconds into a sound. - The tab shows the heads with M, S, volume and pan, the ruler of the Pattern tab and the items, a pattern with its notes, a sound with its waveform, a missing asset red. Patterns and sounds are dragged in from the Assets panel. Body, right and left end move, lengthen and cut back, Alt leaves the grid. Ctrl+C/V/D/A, Ctrl+E splits where playing starts, Delete, arrows. A double-click opens a pattern item in its tab. - What is picked is a new selection of its own, so the inspector shows the item or the track, with a button to delete it. - The scheduler plans clips beside notes. Where the playhead starts or jumps back inside a clip, it is taken up that far into its sound. A clip fades out at the end of its item or of the loop. - The editor plays one piece at a time: a pattern started stops the song and the other way round. - The rules are pure functions in core/music/song, editor/song/ songView and songEdit. Findings: - The suites of |
||
|
|
2a3b90b017 |
Play and stop a pattern from a behavior in the game #336
A pattern could only be heard in its tab. Songs come with #334, and only then can #336 play them in a game. Until then a game plays a single pattern, looped or once, through two new nodes. - Play pattern plays the whole pattern through its instrument at its own tempo. The loop on the ruler is only for editing. Inputs are pattern, volume, loop (on by default) and detached. It gives out playback, 0 where there is no instrument or no note. - Stop pattern stops one pattern, every pattern for none, or one playback by its number. Let ring lets notes with an Envelope fall away in their release. - A playing is numbered and owned like a voice. Destroying the object, Stop sound with nothing named and the end of the run stop it. Stop sound with a voice takes a playback too. - Each playing runs on a scheduler of its own (core/music/playbacks). It waits for its files first, and a stop that came earlier wins. - The scheduler has an end for a sequence that does not loop. It stops waking there and is done once its last notes are. - The worker clock moved to viewport/wake.ts, so SoundLibrary can use it without importing music.ts. Tests: - pattern-play.test.ts (new): 8 tests, node catalog +7 cases - e2e 64 (melektaus-atx): ALL PASSED, 23 checks (+3) - Gate (melektaus-atx): CHECK EXIT 0, 248 files, 3854 tests - Counter-checks: no end in the scheduler, and no owner for the playing, each fail 1 test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4ea752feef |
Draw notes for an instrument in a piano roll and loop them #333
Notes could only be played one at a time, by a key or a behavior. A pattern is a stretch of notes for one instrument, drawn in a piano roll and played in time through the scheduler of #332. Songs (#334) will lay patterns out on tracks. - New asset type pattern under the heading Patterns. Instruments are listed under Instruments now, since a heading is one per type. - Notes are a start and a length in ticks, a note number and a velocity, kept to their rules on load and in every command. The instrument, the length in bars, the grid, the meter and the tempo are rows in the inspector. The loop is the asset's own data, the whole pattern while none is set. - Grids from 1/1 to 1/32 and the triplets 1/4T to 1/32T, all whole ticks. The meter only moves the bar lines, the notes stay put. - The tab shows the keys down the left, a ruler with bars, beats, loop and start along the top, the notes between them and a velocity lane below. Keys the instrument plays nothing for are grey, the names of a drum kit stand on its keys. - A click draws a note on the grid line before the pointer, as long as the last one drawn or lengthened and as hard as the last velocity set. The body moves a note, the right end lengthens it, Alt leaves the grid. A drag on empty grid picks with a band, Shift adds, a right-click deletes. - Keys: Space plays, Delete, Ctrl+C/V/D/A, the arrows by a semitone or a grid step and with Shift by an octave or a bar. A paste lands where playing starts. Every gesture is one undo step. - A click on the ruler sets where playing starts, a drag the loop or one of its ends, its menu loops a bar or the whole pattern. - The wheel scrolls, Shift across, on the ruler or with Alt it zooms. Ctrl+wheel stays with the browser, Pixi listens passively. - Play runs the loop through the scheduler. An edit goes to it on the next window without a restart, a new loop or tempo counts from what is planned. Another pattern shown or a project opened stops it. - The time axis and the ruler are their own pieces in editor/music for the song tab. The rules of the roll are pure functions in rollView and rollEdit. Findings: - Music.play could start after a stop that came while it was still loading its files. A run counter makes the late start give up. Music.edit now loads what a new note needs as well. - A click on an end of the loop would have set the loop again. A press there only moves the end once it is dragged. Tests: - pattern.test.ts (new, 9) and piano-roll.test.ts (new, 16): model, grids, meter, held notes, sequence, commands, loop, round trip, drum names, time lines, rows, hits, bands, lane, edit rules. - e2e 64 (new) on melektaus-atx: ALL PASSED, 20 checks. e2e 4 with 29 asset types: ALL PASSED, 62 checks. - Gate on melektaus-atx: CHECK EXIT 0, 247 test files, 3839 tests. - Counter-checks: the move without its clamp, the sequence without the end of the pattern, the duplicate offset without whole grid steps each fail 1 test. Without passing edits to the scheduler the e2e check for a note drawn while playing fails, the rest passes. - Probe on stein2: the tab with a drum kit, before and while playing, looked at in two screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9c71eb1a0d |
Shape notes with an Envelope and move numbers with its curve #335
Without an envelope a note set in hard and broke off 10 ms after its end. An instrument now has an Envelope node with attack, decay, sustain and release. On the way of the sound it is the volume. Its output "value" moves a number of the note with the same curve, from "From" to "To", for a filter sweep among others. A new Filter node gives that sweep something to move. - Envelope (instrument only): attack a straight line, decay and release ease out and are 60 dB of the way at their time. A release takes 10 ms at least. Defaults 0.005 / 0.1 / 1 / 0.1. - The end of a note goes to the Envelope instead of the 10 ms fade. A Gate or Loop Sample under it plays on through the release, "Loop, then release" leaves its loop too, and the voice ends with the release. A One-shot under an Envelope of its own plays through. - A note ending before attack and decay releases from the level it has reached. Chrome holds it with cancelAndHoldAtTime, Firefox gets the level worked out from the shape. - New port type "envelope" (coral). It fits only a number input that says it can follow one (PortDef.followsEnvelope): Filter cutoff and resonance, Pitch, Gain, the EQ bands and Mix. Not the Delay, not the Envelope's own numbers. portsMeet() decides it for ConnectCommand, the drag and the drawn edges. - On the audio thread every Envelope of a note is one ConstantSourceNode. A number it moves gets the curve through a WaveShaper, in equal ratios for a cutoff and a gain. A number an Envelope moves is no longer reached by a parameter. - Filter (sound FX and instrument): low-, high- or band-pass, cutoff in Hz, resonance 0 to 1 as a Q. A low- or high-pass is flat at 0 and rises 20 dB at 1, a band-pass goes from Q 0.7 to 20. - Stop sound with "let ring" lets a note with an Envelope fall away in its release. Without it, and with Stop in the tab, it is cut. - The same note again during a release is a voice of its own. - The Envelope's numbers are set when the note starts. They can come from velocity or a parameter, later changes do not bend the note. Tests: - envelope.test.ts (new, 12): curve, ratios, Filter Q, compiling an Envelope, its value into each number, a Delay refused - sound-library +7: attack and decay on the clock, release from the level reached, One-shot held, Stop sound with and without let ring, a cutoff and a pitch moved by the curve - e2e 34: ALL PASSED, 50 checks (+2). Offline in Chrome the curve rendered 0.5 / 1 / 0.5005 at 0.05 / 0.1 / 0.3 s, released at 0.5 s it was 0.0158 at 0.65 s and 0.0005 at 0.8 s, released mid-attack it fell from 0.5 to 0.05 in 0.1 s. A Gain moved 0.25 to 1 was 0.5 halfway. A looped note of 0.2 s with a release of 1 s was gone after 1.32 s. - Five counter-checks (release end, One-shot hold, the follows check, ratios, pitch sum) failed 3, 1, 1, 2 and 2 tests. - Gate on melektaus-atx: CHECK EXIT 0, 245 files, 3812 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fb4d65a2ab |
Keep both sounds full at the middle of a Mix with a curve #331
A Mix crossfaded: at 0.5 each sound was at half. Layering two sounds in an instrument now goes through a Mix ( |
||
|
|
076c564415 |
Take one sound at the Output of an instrument, mixed by a Mix #331
The Output summed any number of edges as layers. That made it the only sound input of its kind, and it read as if a note could be several voices. A note was always one voice, but the graph did not say so. Now the Output takes one sound like every other sound input, and several are put together with a Mix in front of it (decided by the user after #331). - The Output's input is no longer `many`. A second edge replaces the first, as at any data input. - With only one drum sounding for a note, a Mix is its other side. A drum kit keeps the volume of each drum. - An instrument saved with several edges into the Output keeps them but does not compile. The Output says "Takes one sound: put a Mix in front of it to play several". No saved project has one: the only instrument there (cbbf160bb7e8) has a single edge. - An Output with nothing in it is a problem now, not silence. - The compiler gathers only triggers from several edges. The player still takes a list of layers, which an instrument fills with one. Tests: - instrument.test.ts: drum kit through a Mix, a second edge replacing the first, an old Output with two edges, an empty Output (33 tests) - Counter-check: `many` back on the Output turns 15 tests red, the new one about the second edge among them - e2e 34 on melektaus-atx: ALL PASSED, 47 checks - Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3789 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
23bdc3f775 |
Play notes in time on the audio clock with a scheduler #332
Patterns and songs (#333, #334) have to play sample-accurate in time,
whatever the frame rate does. What is counted in game time runs away
from the sound. This lays the ground: musical time, a scheduler that
plans notes ahead on the audio clock, and a player that starts and ends
a note at a given second. Nothing in the editor plays a sequence yet,
the check scripts drive it through `__gm.music`.
- Ticks, 480 to the quarter as in MIDI files (decided in #332).
Triplets and quintuplets land on whole ticks. Any meter, a beat being
the note value under the line. The tempo always counts quarters.
- `core/music/sequence.ts` is a pure rule: the notes starting in a
window of playing time. Across the end of a loop it looks into the
next round, so the seam has no gap. A note past the loop end ends
there. Windows are whole ticks, and the seam is stepped exactly, so
no rounding can drop the first note of a round.
- `core/music/Scheduler.ts` plans the next 100 ms every 25 ms through a
host, so it is tested with a clock of its own. The first note sounds
50 ms after the start. Changes to the sequence are heard from the
next window on. Stop and seek fade what it started. A new tempo or
loop counts from what is planned, and the playhead keeps its old
pace until the clock gets there.
- The position is the audio clock less `outputLatency`, as bar, beat
and tick.
- The beat comes from a Web Worker (`viewport/musicTimer.ts`), which
keeps its pace in a hidden tab.
- The sound library starts a note at `timing.at`: a buffer source by
`start(when)`, the pitch worklet by counting down inside the render
block it falls in. A note's end is set on the clock at once, its
fade to the sample. Leaving a loop still waits for the page.
- Delay synced to beats (decided in #332, noted from #327): a setting
`Sync` (Seconds / Beats), `time` then in quarter notes, held to the
2 s of the line. Play sound and Play note have an input `tempo`,
120 BPM where nothing is connected. A scheduled note carries the
tempo of its sequence.
- Swing is left for #333/#334, as decided.
Also fixes the red suite of
|
||
|
|
6f1f31516f |
Find asset types below the fold of the + menu in e2e #331
Since the Instrument joined the + menu, its last entries can sit
below the fold of the test window. CI for
|
||
|
|
144dee0a97 |
Let a looped sample play out past its loop when the note ends #331
With "Mode" and "Loop" as two fields, One-shot plus Loop ran until Stop was pressed: letting go of the key did nothing. The two fields are now one, "Play", with four entries that each say what happens while the note is held and when it is let go. - Gate and One-shot play the sound once, as before. Loop repeats it and fades out with the note. - "Loop, then release" is new: the sampler's sustain loop. Let go, it finishes the round it is in and plays what comes after "Loop end" to the end of the sound. - A buffer source stops looping and plays on by itself. The pitch worklet gets a "leave" message. The phase vocoder then lets the round finish that its newest frame has read into, so nothing read already changes under the frames still sounding. - Samples saved with "Mode" and "Loop" are turned into "Play" on load. Gate and One-shot stay, either with Loop becomes Loop, since playing until stopped is gone. - The keys of the Instrument tab stay half lit while a note let go is still heard. Esc on the keys stops everything, like Stop. - Striking a ringing key again plays a new note beside it rather than stopping the old one, so a drum can be hit quickly. This differs from what was proposed in the issue discussion. Tests: - instrument + pitch-player + sound-library + node-catalog: 477 tests, green (12 new). - e2e 34 (melektaus-atx): ALL PASSED, 45 checks (+1). Let go after a loop of 0.05 s, the note played out for 0.31 s on the audio clock, the key half lit meanwhile. - Counter-check: a release that does not leave the loop fails the 2 new player tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8a4e4a6762 |
Play sounds as notes of an instrument asset #331
A sound could only be played as it was, one at a time. An instrument
is a node graph that makes a note a sound: Notes nodes bring the note
in, Samples play sound assets for it, the effects of a sound FX shape
it and the Output sums the layers. Its tab plays it from a keyboard,
a behavior with Play note. It is what patterns and songs (#330) will
play.
- New asset type instrument, under the heading Music, not placeable.
A new one starts as Notes into a Sample into the Output.
- Notes are counted as MIDI does: 60 is C4, names like C#4, sharps
only, the lowest C-1. Notes has From and To, outputs any, note,
frequency and velocity, and with Single notes a trigger per note,
each with an optional name for a drum kit.
- New port type trigger, only into the trigger of a source. Inputs
can take any number of edges now (PortDef.many): a trigger, and
the Output, which plays everything into it side by side.
- Sample plays a sound asset. Follow note shifts it by note - root
through the pitch worklet of #317, so it keeps its length. Gate
fades it out at the end of the note, One-shot plays through. Loop
runs between Loop start and Loop end, also in the worklet.
- Parameters declares the instrument's own parameters with a
default. Sound FX sends the sound through a sound FX asset, its
parameters as inputs, the sound set in where its Input was.
- compileInstrument walks the graph once per note. A trigger is on
only for the notes it stands for, a silent source silences what it
alone feeds, a Mix with a silent side is the other side. velocity
and the parameters stay open for the voice. Builds are cached per
note until the graph or a sound or FX it names changes.
- The player takes a note as one voice of several layers, each way
with its own file and volume. release fades the gated ways out,
one-shots play on. playNote can release after a duration.
- Play note plays one note with velocity, volume, duration and
detached, and hands out voice for Set sound FX parameters and
Stop sound. Velocity is always loudness. Instrument parameters
picks values for the instrument's parameters like Sound FX
parameters does for an FX, renames follow.
- The tab has the graph, a header field per parameter, Stop, and
128 keys below. Keys without sound are grey. A key plays while it
is held. Focused, the computer keyboard plays as in a tracker by
KeyboardEvent.code, the arrows move the octave.
Findings:
- A label of the keys set fill on the shared small text style and
darkened every small label of the editor. Caught in a screenshot
before the commit, the keys have a style of their own now.
- Suite task 523 on
|
||
|
|
cf6799a784 |
Let echoes ring on with a Delay node in sound FX #324 #327
A voice used to end the moment its sources did, and Stop sound cut it dead. An echo has to outlive its sound, and a stop must not click. - Delay node: a delay line with a damped feedback loop, added at mix next to the untouched sound. Time 0.01 to 2 s, feedback up to 0.95, damping 20 kHz to 500 Hz evenly in octaves, all fed by parameters and moved by Set sound FX parameters like any other number. - Ping-pong is a setting of the node. It takes the sound into the left of the line and swaps the sides every round. - A voice with an echo rings on for its tail: until the last echo is 60 dB below the sound, from time, feedback and mix (fxTailOf). It stays reachable meanwhile, and a change counts the tail again. - The end of a tail is timed on the audio clock with a silent ConstantSourceNode, so a paused run holds it. Then the voice is disconnected, since an echo is a loop of nodes. - Stop sound fades a voice out in 10 ms instead of cutting it. Let ring fades only its sources and keeps the voice for its echoes. Where the context is suspended it cuts at once. - Stop sound takes a voice. Connected, it stops only that play, and a 0 from a Play sound that played nothing stops nothing. - Play sound hands out tail: length plus the echoes at its start values. - Destroying a behavior and ending a run still cut, echoes included. - No limit on voices. The emit of a sound FX node now also gets the node's settings. Finding: in Chrome every round of a feedback loop comes one render quantum late (128 samples, 2.7 ms at 48 kHz), measured as 0, 128, 256, 384 for the first four echoes. Left so, since a browser without it would then hear the echoes early. Written in the wiki. Tests: - sound-library, soundfx, node-catalog, sounds, keep-on-scene-load: 489 tests green. New: fade on stop, cut while suspended, echo and ping-pong wiring, ringing until the tail, tail counted again, let ring, stop by voice, echo knobs, delay compile, tail sums. - Counter-checks: no tail, a stop without fade, a connected voice 0 read as unconnected. Each fails its tests, files back by cmp. - e2e 34 (melektaus-atx): ALL PASSED, 37 checks. A blip through a ping-pong Delay (0.3 s, 0.5, 0.5) rang 2.97 s, 2.95 s worked out. Let ring kept the voice, a stop after it cut it. Rendered offline with the player's own stageNodes the echoes are 0.25 left, 0.125 right, 0.063 left. 64 stereo voices of it took about 0.3 s for 2 s, so about 400 play at once. - Gate (melektaus-atx): CHECK EXIT 0, 242 test files, 3713 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6e70421b92 |
Keep the length of a sound whatever its pitch does #317
A Pitch used to keep the length only for the pitch a voice started at. Set sound FX parameters changed it by speed, so a sweep made the sound longer or shorter, and length into time no longer spanned the sound. Now a way with a Pitch is played by an AudioWorklet, a phase vocoder whose pitch is an AudioParam in semitones. The length never changes, and a pitch glides, ramps and eases like a Gain. - PitchPlayer (core/soundfx) is the vocoder as a plain class, so it runs in a unit test. Frames of about 43 ms, four overlapping, a real FFT done as a complex one of half the size. - Peaks are moved with the bins around them (Laroche and Dolson's phase locking), and frames are turned by half their size so a phase is the one at the middle. Without both, a tone beat against itself and dipped to 0.54 of its loudness while the pitch moved. - It reads the sound itself rather than being fed it. It looks ahead as far as a frame reaches, so a voice starts with its first sample and every way of a Mix stays in step. A change of pitch is heard about half a frame (20 ms) late. - At exactly 0 a frame is the sound itself and costs next to nothing. Coming back to 0, the phases are pulled round to the sound's for about half a second before it switches, so the switch does not dip. - pitchWorklet (viewport) is the processor, bundled by Vite through ?worker&url. It is loaded with the first play that has a Pitch, which waits for it like for its bytes. A file's samples are sent with its first voice only and kept in the worklet's scope. - A way has the worklet when its plan has a Pitch, a typed-in 0 excepted (FxChain.shifted). A way without one stays a plain buffer source. Without AudioWorklet a Pitch is not heard, and the console says so. - The pitched copies (WSOLA, pitchShift.ts) and their cache are gone. - Decisions of the user: the player at the source rather than an effect in the chain, a phase vocoder rather than WSOLA, no limit on voices, no range tighter than the node's -64 to +64. - stats() says whether the worklet is there, levels() gives a pitch in semitones, and __gm.sound.pitchWorkletUrl() lets a check run it. Tests: - pitch-player.test.ts (new, replaces pitch-shift.test.ts): 11 tests. Pitches from -30.5 to +12 within 2 %, the length to the sample, 0 exact to 1e-5, a sweep half a frame behind, 8 kHz sounds, loops, ramps there and back without a dip. - sound-library.test.ts: 23 tests (+4), soundfx.test.ts: 31 (+1). - Counter-checks: no worklet in the player, shifted taken from the value, no pull back to 0. 15 tests fail, files restored by cmp. - e2e 34 on melektaus-atx: ALL PASSED, 34 checks (+1). In real Chrome the worklet is there and the pitch param glides to 3.0007. Offline: 880.0 Hz for 440 an octave up, 48000 of 48000 samples, a sweep 0 to -12 at 392 then 248 Hz, still 48000 samples. - Gate on melektaus-atx: 242 test files, 3690 tests, CHECK EXIT 0. - Cost: 53 ms per second of a stereo voice at +5 in Node on stein2, 5 ms at 0. In headless Chrome 16 voices for 2 s took 1.3 s, about 24 fill the audio thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
804a97b0a9 |
Hand out the length of a played sound from Play sound
Wired into the time of Set sound FX parameters, a change then spans exactly one run of the clip, so a fade or a filter sweep ends with its last sample. - Play sound has a second output, length, in seconds. It is 0 where nothing was played or the sound has not loaded yet. - The AudioSink gives the length of a file, the SoundLibrary takes it from the decoded buffer. A pitch the voice starts on keeps it (#312). - A pitch changed while the voice plays still changes its length, so a pitch sweep over length does not end with the sound. A pitch that keeps the length in real time is #317. Tests: - soundfx, sounds, keep-on-scene-load, node-catalog, sound-library: 462 tests green. - Counter-check: with length always 0, the catalog case and the new soundfx case fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
297fca3e94 |
Show Direction and Curve of a node only where they matter
An animation key shows Direction only for one of the ten curves and the Curve row only for Curve. Lerp and Set sound FX parameters hid them on the node body, but the inspector still offered both rows for every easing, so a linear Lerp had a Direction to pick that did nothing. - A node definition can name `idleParams`: settings that do nothing at the current values. The node body and the inspector both leave them out. - `hiddenParams` stays as it was. It only trims the node body, the inspector keeps those rows (the Order of an event, the mode of Find), so it could not take this job. - Lerp and Set sound FX parameters use `idleEasing` (formerly `hiddenEasing`) for it. - The idle settings are part of the node signature of the inspector. Picking an easing that needs Direction or Curve rebuilds the section, and the row appears. Tests: - easing-curves: 14 tests (+1), node-catalog: 381, green - e2e 06 (149 checks, green) checks that a new linear Lerp has no Direction and no Curve row in the inspector. Picking Cubic brings the Direction row, and Out is picked there as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e7a360db73 |
Offer the easings of an animation key on Lerp and sound FX ramps
A timed Set sound FX parameters ran only linearly, and a Lerp had four quadratic eases of its own. An animation key already had the full set. Now all three offer the same choice: Linear, Step, the ten curves from Sine to Bounce with In, Out or In-out, and a drawn Curve asset. - easingParams in src/core/graph/nodes/easing.ts gives a node the fields Easing, Direction and Curve from the key's own lists, and easingOfParams reads them back as a key interpolation. - There is no migration. A Lerp saved with the old in, out or inOut runs linear until its easing is picked again. - Set sound FX parameters hands the easing to the player. Linear and Step stay ramps of the audio clock, every other easing goes to setValueCurveAtTime as 200 samples a second, in semitones and dB. - Step now reaches its end at t 1. A key never asked for that, it reads the next key itself, so animations play as before. - The curve-in-use count and the warning for a gone curve cover the sound node too. Tests: - node-catalog: Lerp with cubic in, quad out and in-out, a curve without a direction, step, an unknown easing as linear. The sound node hands on a curve, step and a gone curve as linear. - sound-library: an eased change becomes a curve in semitones and decibels, a step a jump at the end. - node-catalog and curve after dropping the old values: 399, green. - e2e 6, 34, 53 on melektaus-atx: ALL PASSED, 198 checks. In Chrome a 2 s Quad in from depth 7 to -5 read speed 0.864 early and 0.5 at the end, a 1 s Step held 1 and then jumped to 0.5. - Counter-check: with curves sent as linear ramps, the curve test of the sound library fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
501a1524ed |
Ramp sound FX parameters over a time on the audio clock
A pitch sweep counted down in On update or On fixed update runs in game time, while the sound runs on the clock of the audio device. A short sound magnifies any offset between the two, so the sweep came out different on every play and most of all on the first. - Set sound FX parameters has a new input time in seconds. The values run evenly to their target in that time, scheduled on the audio clock with linearRampToValueAtTime or exponentialRampToValueAtTime. - Evenly in their own unit: a pitch in semitones and a gain in decibels, an EQ band and a Mix share linearly in their value. - A time of 0 or below keeps the quick glide of about 0.1 s. - Each change starts from where the voice is now. Whatever was still planned is dropped, so a change never queues behind an older one. Firefox lacks cancelAndHoldAtTime, there the value is held by hand. - A voice still waiting on its bytes starts a timed change with the sound, so the whole sweep is heard. Tests: - node-catalog: time is handed on, a time below 0 is a quick glide. - sound-library: semitones and decibels ramp in their own steps, a timed change on a waiting voice starts with the sound. - sound-library + node-catalog + soundfx: 419 tests, green. - e2e 34 on melektaus-atx: ALL PASSED, 31 checks. A 2 s ramp from depth 7 to -5 read speed 0.979, 0.926 and 0.778 half a second on. - Counter-check: folding a timed change on a waiting voice into its start values fails the new test. - The gate did not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ed2b58b3c0 |
Load the sounds of a project ahead of their first play
A sound was fetched and decoded on its first Play sound. That play started late, its beginning was not heard, and a graph that tuned the voice in the meantime had moved on by the time it started. Images and fonts were already loaded when a project opens. Sounds now are too. - projectFiles lists the sounds of a project, and preloadProjectFiles hands them to the new soundLibrary.preload. That covers the editor and the published game. - Preloading opens no audio device. It decodes with an OfflineAudioContext at 48 kHz. The real AudioContext is still made on the first play, and a source resamples where the device runs at another rate. - A run started right after opening a project can still find a sound loading. It then starts late as before. Tests: - sound-library: a preload makes only the offline context, and a play after it starts at once, opening the device only then. - sound-library + project-files: 18 tests, green. - e2e 34 on melektaus-atx: ALL PASSED, 30 checks. - Counter-check: decoding with the real AudioContext again fails the new test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ded0022903 |
Run behaviors on a fixed step with an On fixed update node
Adding up the frame's dt in On update makes anything that counts in steps depend on the frame rate. A pitch sweep sounds a little different on every run. On fixed update runs 60 times per second of game time with dt exactly 1/60, so the same graph gives the same result every time. - fixedSteps in src/core/time.ts is the one rule for the fixed step: spend the frame in steps of 1/60, keep the rest, catch up 8 steps at most after a hitch. - The physics now counts its steps with fixedSteps too. It behaves as before, and On fixed update runs exactly as often as the engine steps. - The steps run after On mouse wheel and before On update, each as a kind of its own with the usual Order setting. - Each step gets its own context, so a node inside that reads dt (Time, Move, Apply force) gets 1/60 rather than the frame. - A paused game has no steps, also for a behavior that runs while paused. - The engine still takes all of a frame's steps before the behaviors run. A force set in On fixed update acts from the next frame on. Tests: - fixed-update.test.ts (new): 5 tests. Step counting, uneven frames adding up to 60 steps a second, the hitch cap, two steps at 30 Hz before On update with dt 1/60 for the event and for Time, one step every other frame at 120 Hz, none while paused. - event-order, node-catalog, physics-system, game-pause: 430 tests together with the new file, all green. - Counter-check: with the frame's dt inside a step and without the pause guard both world tests fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c7e9769f90 |
Change the sound FX parameters of a sound while it plays #315
Until now a value handed to a sound FX counted from the start of the voice only. An engine whose pitch follows the speed, or a loop that gets quieter, could not be built. Every play of Play sound now gives out the number of its voice, and a new node lets the knobs of that one voice glide to new values while it plays. - Play sound has a new output voice: the number of this one play, or 0 where nothing was played. Kept in a Number variable it reaches the voice from another event. Two plays of one sound are two voices. - New action node Set sound FX parameters (exec, voice, fx params). It reaches exactly the voice it is given, a detached one too. A voice that has ended, or voice 0, is not reached. - AudioSink.play returns the voice number and takes the plan and the values (FxVoice) instead of a finished chain. AudioSink.tune takes a voice number and new values. A name the voice's FX does not have is left out, one not given keeps its value. - SoundLibrary hands out the number at once, before the bytes are there, and never twice. A voice still waiting for its bytes starts with the values handed in meanwhile. - Every knob of a voice moves with setTargetAtTime (30 ms time constant). A knob whose value did not change is not sent again, so a tune every frame costs next to nothing. - A pitch on a playing voice glides by playbackRate. A new WSOLA copy in the middle of a sound is not worked out, so the voice gets shorter or longer while it is moved. On a loop that is not heard. - fxChainOf now gives a chain whose shape depends on the plan alone. A stage fed by a parameter stays at 0 dB, and a Mix fed by a parameter keeps both ways. A typed-in 0 or Mix at 0/1 is still left out by the compiler. - Every way of a voice has a source of its own, also at the same pitch, so each can glide on its own. The pitched copy is shared. - The tab keeps the numbers of what it plays, so moving a parameter field is heard at once. - Test hook __gm.sound.levels(file) reads the knobs of every voice of a file, oldest first, and stats() counts the voices tuned. Tests: - soundfx.test.ts: 29 tests (+2 for the node in a running world: the voice of the first of two plays of one sound, and a play that played nothing) - sound-library.test.ts: 15 tests (+3: glide, reach by number, pending voice) - node-catalog.test.ts: 370 tests (+2, Play sound checks its voice) - sounds + keep-on-scene-load: 31 tests, unchanged - Counter-test: a tune that reaches every voice of the same file makes the reach test fail. - e2e 34 (melektaus-atx): ALL PASSED - 30 checks (+1). In Chrome two looping voices of one sound start at depth 7. The one whose number sits in a variable glides to 0.7938 (2^(-4/12) = 0.7937), the other stays at 1. Both gains stay at 0.5012. - The gate ran before voices got a number (CHECK EXIT 0, 241 files, 3658 tests), not after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
072537b2e8 |
Blend two ways of a sound FX with a Mix node #316
A sound FX was a single line from Input to Output, so two versions of one sound could not be heard together. The new Mix node joins two ways again, and each way can carry its own effects, its own Pitch included. - Mix has the audio inputs a and b and a number input mix (0 to 1, default 0.5). At 0 only a is heard, at 1 only b. Like every number it can come from a parameter of the Input and is held to its range. - The compiled plan and the chain a voice gets are now a tree. A source carries a pitch and stages, a mix carries a, b and the stages after it. A Mix that ends at exactly 0 or 1 collapses to the one way heard, so the silent way is never worked out. - A Pitch is still worked out on the sound as it starts (#312). Behind a Mix it goes to both of its sounds. An effect before a Pitch therefore acts as if it came after it, the node now says so. - The player starts one source per pitch, all at once and all of the same length, and two ways at the same pitch share one. A mix becomes two gain nodes with 1 - mix and mix into a sum. A voice leaves the count when all of its sources have ended. - stats().last reports pitches (one per source) instead of pitch. Tests: - soundfx.test.ts: 27 tests (+5), sound-library.test.ts: 12 (+2), node-catalog.test.ts: 368, all green. Typecheck, ESLint, Prettier clean. - Counter-check: a Pitch behind the Mix given to a only, and b mixed at 1 - mix, each fails its new test. - e2e 34 on melektaus-atx: ALL PASSED, 29 checks (+1). The new check plays the pitched way mixed with the dry Input in the browser: two sources at 5 and 0 semitones, as long as the sound itself. - The full gate did not run before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
790e483198 |
Pitch a sound FX without changing its length #312 #313 #314
The Pitch node changed the playback speed, so a sound pitched up was also shorter and one pitched down longer. A pitch now keeps the length, takes fractions and reaches further. Gain and the EQ bands reach further down, and the EQ is renamed so a parametric one can come later without a mix-up. - Pitch shifting is the pure function pitchShift in core/soundfx/pitchShift.ts. It reads the sound faster or slower and stretches it back by WSOLA: 40 ms frames under a Hann window, each moved within 12.5 ms to where it continues the frame before it. A looping sound reads on across its end. - SoundLibrary plays a pitched copy of the decoded buffer instead of setting playbackRate. Copies are kept per file, pitch and loop, the last 32 of them. - stats().last tells the duration of what was played, so a check can see that the length is kept. - Pitch: -64 to +64 semitones, scrubbed in steps of 0.1 (#312). - Gain: -120 to +24 dB (#313). - EQ is now labelled "5-Band-EQ", its bands go -60 to +24 dB (#314). The node type stays SoundFxEq, so saved graphs load unchanged. Findings: - A plain granular shifter was tried first and missed the pitch: at +0.5 semitones it measured 438 Hz instead of 452.9 Hz, because every grain restarts at the phase of the original. WSOLA measures 452 Hz. - Far ends: +64 semitones from 440 Hz lands above half the sample rate and folds back, -64 is below what a 40 ms frame can follow. Nobody has listened to either. - Speed: 3 s of a 48 kHz channel take 47 ms at +3 and +64, 171 ms at -64 (measured in Node). Tests: - pitch-shift (new): 7 tests. A 440 Hz sine at +12, -12, +7, +0.5 and -30.5 semitones is within 2 % of the target and keeps its length. Level stays at most 1, a loop does not fade at its seam. - sound-library: 10 tests (+1), soundfx 22, node-catalog 368. - Counter-check: with the pitched copy switched off the new sound-library test fails. - e2e 34 (melektaus-atx): ALL PASSED - 28 checks. Its octave-down voice is now also checked for the length of the sound. - Typecheck, ESLint and Prettier clean. The gate did not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
45e45fb1df |
Count the Sound FX type in the + menu check
The suite run on |
||
|
|
183f356c99 |
Let a game hand values to the parameters of a sound FX #311
A sound FX played the same every time: its numbers were settings of the nodes. Now the Input declares parameters with a default, every number of Gain, Pitch and EQ is an input they can feed, and a Play sound hands in values for them. - Input declares number parameters in the inspector, each with a Default (DeclaredPort.default, SetPortDefaultCommand). They are outputs of the Input. Input and Output count as terminals now, one of each per graph. - Gain, Pitch and EQ take their numbers at inputs under the keys the settings had, so saved graphs sound as before without a migration. Each value is held to its range however it arrives. - The compiler gives an FxPlan with the parameters still open. fxChainOf fills them in when a voice starts. The player still only sees an FxChain. - New behavior node Sound FX parameters: names an FX and picks its parameters like a Variables node picks variables. Each picked one is a number input that starts at the FX's default. Its output has the new port type fxparams, which only fits Play sound's fx params. - Play sound warns when the parameters name another FX than the one it plays. Names the FX does not know are left out. - Renaming a parameter at the Input renames it in every Sound FX parameters node naming that FX, edges and values included. - The Sound FX tab has a number field per parameter in its header. Play uses those values, they are never written into the asset. - A value counts from the start of a voice. Changing a playing voice is #315, a Mix node is #316. Tests: - soundfx.test.ts 22 (+11): parameters in the plan, held values, a 0-by-default stage, EQ band, save with defaults, wrong port types, default command with undo, Sound FX parameters into Play sound, defaults of picked ones, node problems, mismatch, rename with undo - node-catalog 368 (+3): Play sound fx params, Sound FX parameters - e2e 34: ALL PASSED - 28 checks (+2: header field plays depth 5, a Sound FX parameters node gives the game's voice depth 7) - Gate on melektaus-atx: typecheck, lint, prettier clean, 240 test files, 3636 tests green - Counter-check: with fxChainOf ignoring the values and the rename not followed, 5 of the new tests fail. - Browser probe: header fields, inspector Default and the picked parameter looked at in two screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
de14f0f9e2 |
Play a sound through a graph of gain, pitch and EQ effects #311
A sound could only be played as it was uploaded. A Sound FX asset is
a node graph like a shader: Input, effects, Output. Play sound has a
new input fx that sends the voice through it.
- New asset type soundfx ("Sound FX"), not placeable, with a graph
and its own tab. The heading in the asset panel is "Sound effects",
so it does not take the click meant for the + menu entry.
- Nodes in core/soundfx/nodes.ts, category sound, port type audio
that only fits audio: Input, Output, Gain (dB), Pitch (semitones),
EQ (80 Hz and 12 kHz shelves, peaks at 250 Hz, 1 kHz and 4 kHz).
- compileSoundFx walks back from the Output with the shared graph
emitter. An input takes one edge, so the result is one line: a
pitch and a list of gain and filter stages. A node without input
or a graph without Output reports a problem on the node.
- Pitch is the playback rate of the source. It applies to the whole
sound and also changes its length, the node description says so.
- The BehaviorSystem resolves fx and hands the line to the AudioSink.
An fx that is missing or does not compile plays the sound dry.
- SoundLibrary wires source, stages, volume, destination in that
order. stats().last now also names pitch and stage count.
- The tab header picks one of the project's sounds and plays it
through the graph as it stands. Stop there ends only its own voice.
Tests:
- soundfx.test.ts (new, 11), sound-library (9, +2 for the wiring),
node-catalog (+1 for fx), graph and asset-groups adjusted
- e2e 34: ALL PASSED - 26 checks (+5: + menu opens the tab, palette,
2 effects, tab Play and game Play through the FX at -12 st)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 239 of 240
test files green. The red one was sound-library (its fake
AudioContext had no playbackRate), fixed afterwards, gate not
re-run in full.
- Counter-check: without the audio rule in portsCompatible the port
test fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
93c0a80b7d |
Keep a grip from laying out before it has a size
The gate of
|
||
|
|
a5184b3368 |
Let the rows of a Variables node be dragged into a new order
The Variables and Properties nodes listed their picked rows in the inspector, but unlike Start, End and struct rows they could not be reordered. The drag dots also sat right on the edge of the row and touched the kind mark. - buildPickedPortSection gathers each row with its warning line as one block and hands the blocks to sortBlocks. A move reorders the ports on the node and is one undo step. Properties uses the same section and gains the same reordering. - Grip draws the dots 3 px in from the left edge and moves the kind mark 3 px clear of them (DOTS_PAD). The column keeps its width, so the name fields do not shift. Rows without dots are unchanged. Tests: - block-drag, reorder, variable-choices: 3 files, 13 tests, all green. block-drag now expects the mark 11 px in with dots, 0 without. - e2e 23 on melektaus-atx: ALL PASSED, 32 checks (+3 for dragging vy before vx and undoing it). It ran before the padding change, which touches no checks there. - The gate did not run before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7b1796baac |
Let a container animation reach alike-named objects in another one
An animation over a container aims its rows at template ids. Played on another container, those ids are not there, so every row below the root was skipped without a word. "Enemy - Hit" built over the Slime flashed the slime's Sprite but never the skeleton's. - When the template id is not found below the object, the runtime takes the names on the way to the row's target in the root and walks the same names below the object, level by level. - The id still comes first, so the root's own instances bind as before. - A name missing at some level, or taken twice, skips the row, as a gone target always did. - The rule is the pure function targetNamePath in animation.ts. The AnimationSystem only walks the children. Tests: - animation.test.ts: 84 tests, all green (+2). One plays over a second container built alike, renamed and with the name twice. The other names a row two containers deep. - Counter-check: with the name fallback off, the new runtime test fails (1 of 84). - Project cbbf160bb7e8 (revision 14407), Demo scene: "Enemy - Hit" and "Enemy - Die" change Flash and scale on the skeleton's Sprite. Without the fallback the skeleton stays unchanged. - The gate did not run before this commit. CI covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2da07599d8 |
Let Get position give only the local x and y
Get world position now covers the world coordinates. Two nodes that both offer world x and y made it unclear which one to take. No project uses the world outputs of Get position any more. - Get position drops its world x and world y outputs and its description points to Get world position. - Old snapshots with edges on those outputs still load. The edges hang on ghost ports and the node warns about them. - The unit tests in hierarchy and raycast and the graphs in e2e 43 use Get world position where they read the world coordinates. Tests: - node-catalog, hierarchy, raycast: 3 files, 417 tests, all green - e2e 43 on melektaus-atx: ALL PASSED, 17 checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
526b1da7f4 |
Add a Get world position node with just the world x and y
Get position hands out four numbers, local and world. A graph that only needs where an object is in the world had to pick the two world ports out of four and leave the local ones dangling. - Get world position has one object input and the outputs x and y. It reads the same worldPositionOf as Get position, so a child adds every parent's position in. - Without an object it reports the origin, like Get position. Tests: - Node catalog: a top-level object, a child under a parent and no object (3 cases). - node-catalog and hierarchy: 2 files, 378 tests, all green. The full gate was not run for this commit, CI covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ed7872c893 |
Let a called behavior share the caller's local state machines
A local state machine started over in every Run behavior call, so a state set in a sub-behavior was gone by the next call. A character controller split into sub-behaviors could therefore never remember that it was attacking. Scene or global scope would keep the state, but then every character shares it. - A call now runs on the states of the instance that runs it, however deep. A state one call sets is still set in the next one, and the call begins in the start state like the instance does. - A move made inside a call is reported like one the caller made. The caller's On state entered and On state exited hear it, the event nodes of the called graph stay silent as before. - Two objects running the same behavior still hold a state each. - This reverses the rule from #229 that local machines of a called behavior are not kept. Keep stays a switch for variables only. Tests: - Two unit tests that pinned the old rule now expect the shared state. - New world test: a sub-behavior closes a door per object, the caller's events hear it, and the next call finds it closed. - Counter-check: with the old call context the 3 tests fail. - Gate on melektaus-atx: 239 files, 3600 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d80d5cce26 |
Let a Start input or a variable name the state machine it carries
A sub-behavior that gets its machine through a Start input had no state ports on Is in state, Set state and the other state nodes. Their ports came only from an inline pick or a State machine literal, so a machine handed in from outside left them empty. - A statemachine port of a Start node can now name a machine (DeclaredPort.machine). The inspector shows the pick next to the port name. SetPortMachineCommand sets it with undo. - machineIdOf follows an edge from such a port to that machine, so the state nodes it feeds offer its states. - A Run behavior node has the machine picked in advance on that input. - A different pick there is allowed, but the node warns, because the called graph asks for the states of its own machine. - A statemachine variable of a Variables node can name a machine the same way, picked next to the variable's row. - The inspector signature of a node now includes the picked machine. Before, the pick changed the ports but the dropdown kept its text until the node was selected again. - A local machine handed in still starts in no state every call. That rule from #229 is unchanged. Tests: - tests/core/start-machine.test.ts (7): ports with and without a machine and undo, save and load, the preset value moving a shared machine through the call, the warning, a port of another kind, the inspector signature, a Variables node. - Counter checks: without the new branch in machineIdOf 2 tests fail, without the machine in the signature 1 test fails. - Gate on melektaus-atx: 239 files, 3599 tests, CHECK EXIT 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3f12a77aea |
Preview a sprite at the end of its inspector, placed or as an asset
A sprite's inspector listed its rows but never showed what they add up to. The picture, its frame, nine-slice, tint and shader could only be judged in the scene, often far from the object being edited. - SpritePreview draws the sprite through syncSpriteMesh, the function the scene draws it with. Slices, padding, tint and shader come out the same without a second path. syncSpriteMesh now takes only the three fields of a DrawItem it reads. - The subject is resolved by previewPicture from the Shader tab, so a placed sprite shows its own overrides. - previewFit is the rule for size and place: whole device pixels per unit from 1 on, at most 8, and the corner on a whole device pixel. Pixel art stays sharp. A sprite larger than the group shrinks freely. - A shader with a Time node keeps the preview redrawing once a frame while it is on screen. A folded group or a hidden panel stops it. - New test hook __gm.inspectorSpritePreview(). Finding: the preview redrew on a model change in a microtask at first. A drawn picture whose revision moved on is retried once, in a microtask queued by the scene's synchronous redraw. That retry ran before the preview had let go, and the old pixel art sheet stayed waiting (e2e 48l-iii read waiting [1,1,1]). The preview now redraws at once on a model change, as the scene does. Tests: - unit: 238 files, 3592 tests (+5 in preview-fit.test.ts), and the group list of a placed sprite in sections.test.ts ends in PREVIEW - e2e 48: ALL PASSED, 47 checks (+2: 48l-i asset preview with the frame, 48l-iii placed preview with its shader values) - counter-checks: without whole-pixel zoom 2 unit tests fail; with the microtask redraw 48l-iii fails with waiting [1,1,1] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1665576fb9 |
Let a pixel perfect camera draw on the game view's pixel grid
A camera that follows smoothly stands between pixels most of the time, and pixel art then wanders between them. The new camera switch draws what the camera shows on the game view's own grid, while positions in the model keep their fractions. - `Pixel perfect` is a camera row, off by default. It works at every fixed game scale and at Auto, but not at Fit and not in the Scene tab, where one works with the fractions. - The grid has one step per device pixel of a world unit. At 100% that is whole units, at 500% fifths. The view carries it (`View.grid`), so no view draws with the grid of another. - The view looks from a point that puts the world's origin on a whole device pixel (`lookFrom`). This also covers odd camera sizes and views that cut the picture to an odd size. - Sprites, texts and tilemaps snap their top left corner when they are unturned, so an odd size lands on whole pixels too. A turned object snaps its pivot (`snapPlacement` in `pixelGrid.ts`). - Under a camera shader the picture is taken the same way, and the objects in it use the picture's grid. With Camera pixels that is one step per camera pixel. - An overlay camera has its own switch and snaps only while the main picture is at a fixed scale. - Particles are drawn where they are. - New test hook `__gm.gameDrawnRect(id)` reads where a sprite's picture is drawn, which `gameRect` (the model's) cannot tell. Found: the e2e fixture at 300% drew the crate at y 205.5 device pixels without moving the camera, because the view cut to an odd height puts its centre on half a pixel. With the switch on it is at 206. Tests: - Gate: 237 files, 3587 tests (+10 in pixel-grid.test.ts) - e2e 2,3: ALL PASSED, 75 checks (+1, 3j) - Counter-checks: without the origin snap 3 unit tests fail, with the centre snapped instead of the corner 2 fail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
92395f6773 |
Find the render layer log pair in e2e 63c3 instead of the tail (#305)
The suite went red three times in a row (tasks 488, 491, 492) on one check only: 63c3 read `named: ["",""]` while the picture half passed. Alone the module was green. In the full run objects left by earlier modules are still in the scene, and their behaviors log two empty lines after the OnStart of the check. `log.slice(-2)` took those. - The check now searches the log for the pair "UI", "1". Both lines come from one exec chain, so they stand next to each other. - Without the pair it shows the last 6 lines for diagnosis. - Not found out: which earlier module writes the empty lines. Tests: - e2e 63 on melektaus-atx with a temporary behavior that logs two empty lines after the switch: ALL PASSED, 13 checks. - The same with the old `slice(-2)`: 1 FAILED, `named: ["",""]`, the picture from CI. The disturbing behavior is removed again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3f57cf731d |
Keep a pixel frame worn through a shader alive while it is drawn
Changing a palette colour of pixel art flooded the console with Pixi warnings: "[BindGroup] a 'textureSource' was destroyed while still bound to a shader", and the same for its sampler. #293 had stopped this for a drawing, but a frame of pixel art is worn as a cut of the sheet. The cut has a source of its own, so the sheet looked unbound, was freed at once and took the cut with it under the shader. - The check whether a replaced picture is still in use gets its key too. TextureLibrary then asks the picture and every cut taken from it. - A sheet with a bound cut waits like a bound picture did since #293, and both go together once the shader has taken the new frame. Found on the way: - The project the report came from has a sprite wearing a frame of an eleven-frame pixel art through a shader asset. - A nine-sliced sprite wearing the frame showed no warning, even without the change. The first try of the e2e check used it. Tests: - Gate on melektaus-atx: 236 files, 3577 tests (+1) - e2e 48: 45 checks (+1, sum 1741). 48l-iii puts a shader on the object wearing the second frame, changes a palette colour three times and counts the warnings. - Counter-checks bite: without the cuts in the check the unit test fails, and 48l-iii counts 6 warnings for 3 changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c74b6b1cab |
Show where an inspector row can be dragged and answer the pointer
After #310 rows could be dragged by their mark, but nothing said which marks take a drag, and the pointer on one changed nothing but the cursor. - A mark that drags is now a Grip: the same widget wrapped, the same size, so the value column stays where it was. Once sortBlocks takes it, it carries six faint dots at its left edge. - Marks that cannot drag stay plain: the variables of a placed object and the fields of a typed struct show no dots. - With the pointer on a grip its dots light up and the whole block that would move is washed in the accent colour, a struct with its fields. While the block is carried the wash gives way to the dimming, and the grip stays lit. - While a block is carried the other grips let the pointer pass: no lit dots, no wash. Every grip of the column shows grabbing instead of grab until the drop. - The wash first used controlHover like list rows. In the inspector it is darker than the panel and vanished next to the dark fields, so it became the accent at 15 percent, the colour of the drop line. - __gm.inspectorMark also reports the dots: none, idle or lit. Tests: - Gate on melektaus-atx: 236 files, 3576 tests (+4 in block-drag.test.ts, one of them a drag passing over another grip). - e2e 22: 40 checks (+1, sum 1740). 22f-iii reads idle dots, moves the mouse onto the mark and reads them lit. - Two counter-checks bite: no wash on hover fails 1 test, grips never made grabbable fail 3. A third: hover ignoring the drag fails 1. - Screenshots of a lit single row and a lit struct block looked at. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2c19c89878 |
Let variables, fields, ports and list items be dragged into order (#310)
Variables of a behavior, the fields of a struct, the ports of a start or end node and the items of a list could only be added and removed. Changing their order meant removing and adding them again. Now each of them is dragged by the mark in front of its row. - sortBlocks (ui/blockDrag.ts) makes the rows of one column sortable. A block is every row it covers, so a struct moves with its fields and a list item with its nested list. The block is dimmed while it is carried, an accent line shows where it lands, and a scrolled panel follows the pointer at its edge. Less than 4 px stays a click. - The arithmetic is pure and lives in ui/sortable.ts: blockGap, gapLineY, dropSlot and edgeScroll. dropIndex of the tile strip moved there too, so the reorder maths sits in one file. - MoveVariableCommand, MoveStructFieldCommand and MovePortCommand build on the existing MoveCommand. Undo moves back. - Lists need no command. InlineList replaces the list as a whole, as it does for every other edit of its items. - The grip is the kind mark, for a list item its number. Moving only happens among siblings: a field stays in its struct. - On a Struct asset the move runs through WithStructTypes. Typed variables, Pack/Expand ports and split outputs follow the new order. - Values and edges hang on names, so only the order changes. Nodes running a behavior read their ports from the start node, the new order reaches them without a copy. - Not sortable by design: the variables of a placed object, the fields of a typed struct (moved on the Struct asset) and the picked ports of Variables/Properties nodes. - Test hook __gm.inspectorMark(label) finds the mark of a row. Tests: - Gate on melektaus-atx: 235 files, 3572 tests, CHECK EXIT 0 (+9). - tests/core/reorder.test.ts: a variable, a field two levels deep, a Struct asset reaching a typed variable and Pack, and start ports reaching the calling node with edges and result kept. - e2e 22: ALL PASSED, 39 checks (+4, sum 1739). 22f-iii drags a variable past a struct of several rows, then a field inside it. - Counter-probes: dropSlot by top edge instead of middle and a field move that ignores its path fail 3 unit tests. With sortBlocks switched off, the three order checks of 22f-iii fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cf6043a066 |
Show a wheel step or Fit as a short zoom transition (#309)
The wheel and the Fit button used to jump to the new view. Now the view takes a short way there, 200 ms by default, everywhere the editor zooms. The zoom steps stay the same, only the change is shown. - viewTween: tweenView is the way between two views as a pure function. The scale moves by equal ratios, and the world point that sits at the same place on screen in both views stays there. After a wheel step that is the point under the pointer. - ViewTween drives it on the ticker. A surface hands it reading, writing and drawing its own view and wraps its wheel and Fit code in change(). A step during a transition starts from where that one was heading, so quick steps add up to exactly as many steps. - Anything else that moves the view ends the transition. A pan jumps to the target first, and saving a view saves the target. - Wired into FreeViewport (Scene, Container, animation preview), SheetCanvas (Texture, Curve, Pixel art, Vector art), GraphCanvas, panAndZoom (Tile board, plugin node board) and the timeline. - Menu > General settings... sets the time in ms, 0 jumps at once. It is kept in this browser like the panel sizes, not in the project, and it is not an undo step. - A document that opens still comes up framed at once. The animation preview frames a new subject itself instead of calling fit(). - The pixel art readout names the step the view is heading for. Measured in the scene view at 200 ms on melektaus-atx, where every frame of the transition is a full redraw: - 500 sprites: redraw 6.7 ms, 16 frames, mean 18 ms - 1000 sprites: redraw 13.0 ms, 9 frames, mean 27 ms - 2000 sprites: redraw 32.2 ms, 5 frames, mean 58 ms Tests: - The harness sets 0 ms on every load, so the existing modules read the step and not the way. - e2e 46i samples one wheel step at 1000 ms a frame later: zoom 1.033 between 1 and 2, the point under the pointer kept at (134, -40). It lands on the view a 0 ms jump reaches. - e2e 46 and 49 expect the new menu entry. - Gate (atx): 234 files, 3563 tests, CHECK EXIT 0 (+15) - e2e 46,49 (atx): ALL PASSED, 43 checks (+3, sum 1735) - Three counter-checks fail view-tween.test.ts as expected. e2e 46i fails when the scene view does not carry its centre along. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8452a99c64 |
Let a graph read and change the z of an object
Until now a behavior could not move an object in front of another while the game runs. Set position writes only x and y, and z lives on the running object rather than in its props, so Properties cannot reach it either. Drawing already reads the z every frame, so only the nodes were missing. - Get Z (data): object in, z and world z out. World z is summed up with the parents, the way drawing does it (worldTransformOf). - Set Z (action): object and z in. It sets the object's own z, local to its parent like Set position. Higher is drawn in front from the next frame on. - Two nodes of their own rather than a z port on Set position and Get position, as asked. Tests: - node-catalog: Get Z for a child (2 below 5 is world z 7) and for no object, Set Z writes only z and leaves x/y. - set-z.test.ts: in a running world a key sets the lower of two sprites to z 2, drawSlots then draws it in front, Get Z logs 2 and 2. - Two counter-checks bite: Set Z without the write and Get Z without the parent fail 3 tests. - Gate: 233 files, 3548 tests (3542 + 6), CHECK EXIT 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3e05cbbed5 |
Let pixel art and vector art be nine-sliced and sorted (#308)
Nine-slice and the sort line hung on the texture frame alone. Drawing and sorting already read both off the TextureRef every picture hands out, but the generated pictures always said "no slice, no line". So the feature moves from the frame to the picture, without a second copy of any rule. - PictureCut (border, fill, sortLine) is one type. TextureFrame extends it, PixelArtAsset and VectorArtAsset carry it as a whole. The texture file format does not change, and no migration is needed. - The rules in texture.ts take a cut and a size: cutSortLine, setCutSlice, rawCutSlice and the rest replace the frame* versions. serialize.ts reads all three through one readCut. - cutTarget.ts resolves a target to cut and size: a texture frame, a pixel frame's size or the vector canvas. It lives apart from texture.ts because vectorart.ts reaches texture.ts itself. - SetSliceCommand and SetSortLineCommand take that target instead of (assetId, frameId). They refuse an asset that draws no picture. - generatedRef fills slice and sortLine from the drawing. The whole strip of a pixel art asset with several frames gets none, it is no picture of that size. With one frame asset and frame are the same. - Nine-slice scale is one shared schema row, now also on pixel art and vector art. - cutSection.ts holds the NINE-SLICE and SORTING rows once. The frame inspector and the drawing inspector both build them. - SheetCanvas draws a drawing's lines that are set, dashed as in the Texture tab. They are not dragged there, the painting tools own the canvas (as proposed in the issue). - drawOrder falls back to the picture's height when a ref has no rect, or a sort line on vector art would have been ignored. Tests: - Gate: 232 files, 3542 tests (+8: drawing-cut.test.ts, and vector art in sort-by-y.test.ts). Three counter-proofs bite: no cut on a drawing, a cut on the whole strip, no height fallback. - e2e 48: 44 checks (+1, sum 1732). 48l-ii reads red and blue where 12-pixel borders at scale 2 put them. Without borders both spots read green. - Looked at: the Pixel art tab with the lines, and the sliced sprite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d9c6bbfa4d |
Add a node that names the render layer of an object (#305)
The issue asked for Set render layer, Set sees and Render layer of. As with #211, a probe showed that writing already works. A Properties node of the object's type writes Render layer during a run, Inherit included, and one of type Camera writes Sees. The next picture follows both. As agreed, only Render layer of is built, and writing stays with Properties. - Render layer of is a data node with the input object and the outputs name and number. It answers the layer that holds, so an object without a choice of its own names its parent's. - NodeContext.renderLayerOf answers the number with the same rule the drawing uses (renderLayerAlong). The new field renderLayers on GraphLookup brings the names to the node. - hasRenderLayer in PropertySchema says which types are drawn. - Decided here: for an object that is not drawn (a camera, a behavior, a collider) the name is empty and the number -1, as Layer of does without a collider. Changing the render layer or Sees during a run had no test so far. Three unit tests now hold the proof from the issue. Tests: - Gate: 231 files, 3534 tests (3526 + 8), exit 0 - e2e 6: 149 checks passed, e2e 63: 13 checks passed (+1) - Unit: the coin moves into the overlay with Set and back with Inherit, a HUD container takes its sprite along, the main camera sees a second layer once its Sees is set, and Render layer of names the inherited layer, and none for a behavior - Node catalog: four cases for Render layer of - 63c3 moves a sprite onto UI in a Play run: the main camera no longer draws it, and the log reads "UI" and "1" - Three counter-probes fail 1, 1 and 2 tests: without inheriting in the node, without the check for a drawn type, and without inheriting in the drawing - "layer of" in the palette still finds Layer of first (6z-x) - The e2e sum is 1731 (1730 plus 1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
70f3608733 |
Let Mouse position and Is hovered see through any camera (#304)
An overlay camera from #302 can stand elsewhere and zoom differently.
A HUD button only it shows lay somewhere else in the world than the
mouse of the main camera, so Is hovered never found it. Both nodes now
take a camera and see the mouse through that camera's picture.
- New input `camera` on Mouse position and Is hovered. Left open, both
read the main camera exactly as before.
- `pointerThrough` in core/properties/camera is the rule as a pure
function. It fits the camera's picture into the main one and centres
it, the same way overlayView draws it.
- The game view reports where the mouse lies on the main picture, as a
part of its width and height. The world turns that into a camera's
view, so a main camera on the move does not drag the mouse across
the HUD while the mouse stands still.
- `in view` is true only on the camera's own picture. A camera that is
not drawn (switched off, a second plain one, not a camera) never has
the mouse on it.
- On click and Mouse button stay with the main camera, as agreed in
the issue.
Also fixes a unit test that
|
||
|
|
1d0fa7b947 |
Let the game view zoom up to 20x
A small camera on a large screen stopped at 6x. The steps now go on in whole numbers up to 20, both in the zoom menu of the Game tab and for Auto, which takes the largest step that fits whole. - GAME_SCALE_STEPS in playSettings.ts runs 0.25, 0.5, 1 to 20. Prettier wraps the longer line. - e2e 60 keeps its own copy of the steps to work out what Auto should pick. The copy follows the new list. Tests: - view.test.ts: 23 passed (every step is used as its own factor) - e2e 60: ALL PASSED - 19 checks (Auto still picks 4 at 1280x800) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a18966bfe5 |
Show the current editor in the README screenshot
The picture from
|
||
|
|
b895ae99c0 |
Show the graph in the e2e 6 screenshot of the long name (#306)
The check for the long animation name ran after a play and stop, which leave the editor on the scene tab. Its screenshot showed the scene, not the node. The check itself read the row through the test hook and was not affected. - The step switches back to the graph tab before it renames the animation. Tests: - e2e 6: all passed, 149 checks. The screenshot shows the node, with "animation Swi…" ending before a whole "progress". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |