You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(examples): playground quality and experience sweep - #860
A quality and experience pass over the playground. The implementation audit covered all 109 original catalog entries using captures requested at settle, 2 s, 5 s and 10 s and, where an example plays audio, level measurements. Only entries with a demonstrated problem changed. Sixteen commits on top of next (the audio core fix of #859 is already in next and is no longer part of this branch), grouped by theme below.
How the evidence was taken
Visual: the catalog smoke route in headless Chromium 153, with wall-clock capture targets after the canvas mounted, using the --shot-times option this PR adds. These are best-effort requested times, not deterministic simulation frames. The settle checks run before the timed series, so an overdue capture can be taken later than its filename suggests; every series therefore writes a <example>.captures.json sidecar with the requested and observed time, the lateness and any capture that failed, and the file names are not evidence of the exact time. Contact sheets were compared before and after; they are available on request.
Audio:
A test-local AudioWorklet tap records every render quantum that reaches AudioContext.destination while Playwright drives the real playground route. No package API is involved.
Each scenario has its own steps (buttons, presets, trigger spam, bypass, hidden tab).
Reported values are sample peak in dBFS and RMS over 400 ms and 3 s windows. The RMS is unweighted and is not LUFS.
The digital level says nothing about loudness at the ear.
Mix limits, fixed before any tuning:
Sample peak ≤ −6 dBFS, with zero clipped samples.
RMS(400 ms) ≤ −16 dBFS, unless dynamics are the lesson.
A/B and bypass steps ≤ 6 dB.
Music beds around the existing loops (≈ −28 dBFS RMS).
The implementation report records compliance for its tested scenarios across all 24 examples with audio. This is not a claim that every possible parameter combination has been exhaustively tested. The detailed scenario matrix, measurement harness and raw measurements have not been attached to this PR, so the aggregate claim is not yet independently reproduced here.
Audio
fix(examples): level-match the effect chains and leave headroom for one-shots
each chain trimmed on the master bus to within ~2 dB of dry
effect-chains, impact
−0.7 dBFS peak
−8.9 dBFS
ducking, voice cue
−0.7 dBFS peak, +15 dB over the music
−7.8 dBFS
audio-buses, SFX
−4.3 dBFS
−10.0 dBFS
reverb-and-delay, dry impact
−1.3 dBFS
−9.2 dBFS
The chain trim is reset on bypass and on destroy.
fix(examples): keep the dry voice out of the vocoder mix
Before: +3.8 dBFS peak and 257 clipped samples. The modulator bus was parented to the master, so the dry voice played under the robot voice.
After: −7.0 dBFS and no clipping. The modulator sits under a muted bus. The vocoder taps the bus output, so it still gets the signal. The sound bus leaves headroom for voice samples mastered near full scale. Processed and dry are within 4 dB of each other.
feat(examples): place the spatial audio source with a mono pulse (listener-and-source)
Before: a stereo music loop as the spatial source, at −15 dBFS peak. The readout sat under the HUD panel, and the mode and falloff buttons had no active state.
After: a synthesised, seeded mono noise-burst loop (accented, high-passed, broadband, so it can be localised) at −7.2 dBFS peak and −28.5 dBFS RMS400.
Movement and Falloff are choices that show the active value, and a Sound toggle can silence the source.
The readout is at the top centre.
The listener has ears that light up with the pan, and the hint recommends headphones.
Pointer handlers are removed on destroy.
feat(examples): choose convolution rooms from a panel and fix(examples): strike convolution pads on quick clicks
Before:
A quick click or tap never struck a pad. The handler tested the live pointer.buttons, which has already been released when the queued press is delivered at the next frame.
The room changed only on right-click, which is not discoverable and impossible on touch.
There was no destroy(), so the effect stayed on the sound bus.
After:
Hits use the phase coordinates, and only a held right button is skipped. Pads sound with a −11.8 dBFS peak. The press button is recorded in the document capture phase when the press happens (the engine stops the event on the canvas), so a right click released before the next frame no longer strikes a pad; the helper trackPressButtons is unit-tested and the behaviour was checked in the browser with a synthesized quick right click and left click.
An impulse picker sits in a panel and stays in sync with right-click.
destroy() removes the effect, the input handlers and the panels.
fix(examples): start the spaceship engine after the audio unlock (gamepad-spaceship)
Before: the engine AudioGenerator was started in init() while audio was still locked, so it was dropped and never sounded. When audio happened to be unlocked, it was a raw sawtooth at up to 0.38. Visually: grey circles and a tiny ship.
After:
The engine starts from onUnlock on its own bus, behind a low-pass filter. The cutoff and level follow the smoothed thrust (−8.6 dBFS peak).
The scene has a starfield, polygon asteroids, a thruster flame and glowing bolts.
Impact sparks are layered, with absolute scale curves.
Everything is cleaned up on destroy.
feat(examples): add an audio signal graph showcase (new: audio-fx/signal-graph, in the "audio" catalog, linked from the Audio effects guide)
The graph shows the actual routing: music → EQ → compressor, and impact → reverb, both into the master bus with a limiter.
Selecting a node edits its real parameters. "Active" removes the effect from its bus and re-adds it in order, so the drawing and the audio graph cannot drift apart.
Meters read each bus output, the waveform is the master analyser window, and the Bézier wires are weighted by bus level.
Levels: −7.2 dBFS peak, −17.9 dBFS RMS400. The bypass steps measured ≤ 4 dB; the Web Audio compressor's automatic make-up gain prevents them from being exactly level-neutral.
The limiter is presented as a safety stage, not a brickwall.
First impression
feat(examples): replace the placeholder ship and announce the hello world interaction
Before: ship-a.png was a 64×64 pale chevron about 30 px wide. It is the placeholder in 23 catalog entries, among them hello-world, sprite-basics, blendmodes, blur-filter, water-mirror, render-to-texture, easing-curves, tween-* and boss-intro, and read as a tiny white mark on dark backgrounds.
After: an original 64×64 ship, nose up, with a light base so tints read and a dark outline that holds its silhouette on light backgrounds. The texture size is unchanged, so no example code changed.
hello-world: the ship is shown at 1.5×, an on-canvas hint tells the player the ship is clickable and reflects the paused state, and the background is dark blue. The guide region is unchanged.
Particles
fix(examples): size particle effects in texture scale
Every particle texture is 512×512 px, and ScaleOverLifetime sets the absolute scale.
Before: emitter-basics peaked at scale 0.55 and moved at 70–180 px/s, so it showed a ~100 px blob instead of a fountain. cursor-attractor-particles and audio-reactive-particles drew 90–110 px particles.
After: a readable fountain at 280–460 px/s, and particles sized in texture scale.
feat(examples): layer the fireworks and campfire effects
fireworks
Before: 375 stars at 0.95 × 512 px per burst formed one white blob, and a burst fired once at (0,0) on start.
After: three roles. A short flash, sparks on an expanding shell that brake and fall, and slow embers with turbulence. Rockets leave streaks. The burst point is parked off-screen until the first detonation. Each burst now draws 180 sparks of about 50 px instead of 375 of about 490 px.
bonfire
Before: flames at scale 1 (512 px) and a hard orange ground disc.
After: four layers (smoke, flames, a hot core, embers) with absolute scale curves and turbulence, a stone ring and logs, a soft additive ground glow and wind weighted per layer.
Comparison lab
feat(examples): compare texture sampling modes side by side
Before: one fixed reference next to one switchable sample. At the default 8× zoom, Linear and Linear+mipmaps were identical, so mipmaps never showed.
After: Nearest, Linear and Linear+mipmaps in three columns with the same transform and time, in two rows:
Magnified pixel art: Nearest stays blocky, Linear blurs.
A minified zone plate that is alias-free in the source: Nearest and Linear produce false rings, mipmaps average to grey.
Panel sliders and toggles are added, the old keys still work, and the guide and catalog text are updated. The Motion toggle and the Space shortcut share one setter, so the toggle text and aria-pressed follow the shortcut (checked in the browser).
Controls and QA
feat(examples): show focus, hover and pressed states on example controls
Shared panels get a visible keyboard focus ring and hover and pressed states, and respect prefers-reduced-motion.
Panels have role="group" and a label, toggles carry aria-pressed, and sliders and cycle arrows carry aria-label. Buttons are type="button".
test(site): capture examples at fixed times in the smoke run
--shot-times 2,5,10 requests additional wall-clock captures after the canvas mounted. They are review evidence, not a deterministic timing guarantee. Each series writes a sidecar with requested and observed times, lateness and failures; the smoke verdict stays separate.
Validation
Reported local smoke: Chromium headless with a WebGPU adapter: before 109 passed; after 110 passed, 0 warned, 0 skipped, 0 failed.
pnpm gates typecheck / sync / site, ESLint and Prettier on the changed files, and examples:sync:check all pass in the implementation report.
Local pre-push lanes on the final head (typecheck, lint, sync, site, unit, webgl, webgpu, smoke): all passed.
CI runs on the exact head after the rebase onto next; see the checks of this PR.
The earlier dispatch on 75e5d464e and its smoke artifact (107 passed, 3 host skips) belong to the previous head and do not qualify this one.
Not verified
No perceptual listening has been done; all audio statements are digital measurements. Please listen with headphones to listener-and-source, the spaceship engine and the vocoder.
The original Playwright measurement environment allowed audio without a gesture, so actual autoplay enforcement was not tested there. Start-on-gesture was inspected in code only. An enforcing, clean browser session still needs qualification; this limitation of the measurement setup does not establish that autoplay is untestable with browser automation.
No headed real-GPU WebGPU run and no video captures.
The private audit matrix, contact sheets and continuous-PCM measurement harness have not been attached for independent reproduction.
Blend-mode gallery and wrap-mode lab were assessed but not built.
Independent review of 75e5d464e940c376ab3bc14add73c4e30fbc2933
The CI run and its exact head were checked, and the shipped site artifact was inspected (SHA-256 a10db3d23cbec9e53301dab93be3ec7f204631c47797e110b9b1142e2667299e). The PR description now distinguishes reported local validation from independently checked CI evidence. No source commits, merge, retarget, or auto-merge were performed.
P2: Motion control becomes stale after its keyboard shortcut
examples/sprites-textures/texture-sampling.ts discards the handle returned by the Motion toggle and changes moving directly from the Space callback. The real shared helper therefore keeps its old internal toggle state, visible text and aria-pressed.
Isolated reproduction using the authored TypeScript and the actual shared helper (renderer, input dispatch and DOM supplied as test doubles): initial state is moving=true, On, aria-pressed=true; Space produces moving=false while the control still reads On/true; the next control click changes only the stale control state and does not resume movement.
Use the same pattern already used for Pixel snap: retain motionControl: ControlBinding<boolean>, route both callbacks through setMoving(value), and update the binding there. A focused regression assertion failed on the original source and passed on that candidate, including repeated keyboard/control alternation. This is a logic test, not a full browser or repository-gate run. Regenerate the committed JS through the normal sync command rather than hand-editing it.
P2: The quick-click fix still lets a released right press strike a pad
examples/audio-fx/convolution-rooms.ts excludes right presses using the live pointer.buttons & 2. A right button released before the queued down phase is delivered is already 0, so it follows the same path as a released left press. The new phase coordinates preserve position, not button identity. An isolated call at a pad with buttons=2 was ignored; the same queued press represented after release with buttons=0 called strike.
Preserve the initiating button through an appropriate public event/interaction path, or capture the native gesture identity in a narrowly scoped, lifecycle-managed adapter. Do not restore the old buttons & 1 check: that would reintroduce the quick-left-click/tap defect. Cover fast left click, held and fast right click, auxiliary button, touch, cancellation and teardown. Do not expand this into an unrelated engine-input redesign in this PR.
P2 (evidence): Timed captures need actual timing and completeness metadata
site/scripts/smoke-examples.ts reaches the timed-capture loop only after settling, validation and the first screenshot. If those steps have already taken 6.5 seconds, the current loop immediately writes @2s.png and @5s.png at approximately 6.5 seconds. The catch also silently abandons the remaining series. The names currently describe requested times, not verified observation times.
Record requested and actual timestamps/lateness plus incomplete/error status in a sidecar or capture manifest. Prefer a monotonic clock. Missed targets must not be presented as exact-time evidence. Keep evidence status separate from the existing smoke verdict; do not loosen gates or turn capture failures into hidden successes.
Evidence boundaries
CI run 39549807196 is successful on the exact reviewed head and its metadata explicitly associates it with #860. Automatic pull_request execution is absent because .github/workflows/ci.yml filters base branches to main/next; dispatches are not generally incapable of PR association. The CI smoke artifact reports 107 PASS / 3 host SKIP / 0 FAIL, not the locally reported 110/110. Both results can be valid on their different hosts.
The complete private audit matrix, contact sheets and continuous-PCM scenarios/raw outputs were not available for this review. No perceptual listening, enforcing-autoplay browser run or real-GPU visual qualification is claimed. The local browser integration attempt in this review environment was blocked by host policy; no gate has been bypassed. Please attach reproducible evidence for the important audio scenarios and retain the outstanding listening/startup checks.
The changes remain directionally useful, especially the core-dependent audio corrections, controlled sampling comparison and effect layering. Address the bounded findings above, run normal targeted regressions/sync/pre-push validation, then validate the resulting head. Package consolidation, a new demo-controls library and gallery restructuring belong to a separate design decision, not this corrective pass.
Base automatically changed from
polish/audio-initial-volume to
nextOctober 21, 2026 11:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A quality and experience pass over the playground. The implementation audit covered all 109 original catalog entries using captures requested at settle, 2 s, 5 s and 10 s and, where an example plays audio, level measurements. Only entries with a demonstrated problem changed. Sixteen commits on top of
next(the audio core fix of #859 is already innextand is no longer part of this branch), grouped by theme below.How the evidence was taken
Visual: the catalog smoke route in headless Chromium 153, with wall-clock capture targets after the canvas mounted, using the
--shot-timesoption this PR adds. These are best-effort requested times, not deterministic simulation frames. The settle checks run before the timed series, so an overdue capture can be taken later than its filename suggests; every series therefore writes a<example>.captures.jsonsidecar with the requested and observed time, the lateness and any capture that failed, and the file names are not evidence of the exact time. Contact sheets were compared before and after; they are available on request.Audio:
AudioContext.destinationwhile Playwright drives the real playground route. No package API is involved.Mix limits, fixed before any tuning:
The implementation report records compliance for its tested scenarios across all 24 examples with audio. This is not a claim that every possible parameter combination has been exhaustively tested. The detailed scenario matrix, measurement harness and raw measurements have not been attached to this PR, so the aggregate claim is not yet independently reproduced here.
Audio
fix(examples): level-match the effect chains and leave headroom for one-shotsThe chain trim is reset on bypass and on destroy.
fix(examples): keep the dry voice out of the vocoder mixfeat(examples): place the spatial audio source with a mono pulse(listener-and-source)feat(examples): choose convolution rooms from a panelandfix(examples): strike convolution pads on quick clickspointer.buttons, which has already been released when the queued press is delivered at the next frame.destroy(), so the effect stayed on the sound bus.trackPressButtonsis unit-tested and the behaviour was checked in the browser with a synthesized quick right click and left click.destroy()removes the effect, the input handlers and the panels.fix(examples): start the spaceship engine after the audio unlock(gamepad-spaceship)AudioGeneratorwas started ininit()while audio was still locked, so it was dropped and never sounded. When audio happened to be unlocked, it was a raw sawtooth at up to 0.38. Visually: grey circles and a tiny ship.onUnlockon its own bus, behind a low-pass filter. The cutoff and level follow the smoothed thrust (−8.6 dBFS peak).feat(examples): add an audio signal graph showcase(new:audio-fx/signal-graph, in the "audio" catalog, linked from the Audio effects guide)First impression
feat(examples): replace the placeholder ship and announce the hello world interactionship-a.pngwas a 64×64 pale chevron about 30 px wide. It is the placeholder in 23 catalog entries, among them hello-world, sprite-basics, blendmodes, blur-filter, water-mirror, render-to-texture, easing-curves, tween-* and boss-intro, and read as a tiny white mark on dark backgrounds.hello-world: the ship is shown at 1.5×, an on-canvas hint tells the player the ship is clickable and reflects the paused state, and the background is dark blue. The guide region is unchanged.Particles
fix(examples): size particle effects in texture scaleEvery particle texture is 512×512 px, and
ScaleOverLifetimesets the absolute scale.emitter-basicspeaked at scale 0.55 and moved at 70–180 px/s, so it showed a ~100 px blob instead of a fountain.cursor-attractor-particlesandaudio-reactive-particlesdrew 90–110 px particles.feat(examples): layer the fireworks and campfire effectsComparison lab
feat(examples): compare texture sampling modes side by sidearia-pressedfollow the shortcut (checked in the browser).Controls and QA
feat(examples): show focus, hover and pressed states on example controlsprefers-reduced-motion.role="group"and a label, toggles carryaria-pressed, and sliders and cycle arrows carryaria-label. Buttons aretype="button".test(site): capture examples at fixed times in the smoke run--shot-times 2,5,10requests additional wall-clock captures after the canvas mounted. They are review evidence, not a deterministic timing guarantee. Each series writes a sidecar with requested and observed times, lateness and failures; the smoke verdict stays separate.Validation
pnpm gates typecheck/sync/site, ESLint and Prettier on the changed files, andexamples:sync:checkall pass in the implementation report.next; see the checks of this PR.75e5d464eand its smoke artifact (107 passed, 3 host skips) belong to the previous head and do not qualify this one.Not verified
listener-and-source, the spaceship engine and the vocoder.