Camera roll, a degenerate-fade audio fix, and public Gradient rasterisation - #1665
Merged
Merged
Conversation
`audio.fade(name, 0.5, 0.5, 0)` left the volume `NaN`, which surfaced later as a non-finite `setValueAtTime` on the next playback. Two faults, both in the interval the fade starts: - the per-tick step divides the elapsed time by the duration, so a zero duration yields Infinity (or 0/0) and `diff * tick` writes NaN - the exit test is `(to < from && ...) || (to > from && ...)`, which equal endpoints satisfy neither of — so the interval never cleared, and kept ticking even with a real duration A fade with nothing to interpolate now settles on the target volume immediately and starts no interval. Four regression tests. They drive `fade` on a loaded-but-unplayed clip deliberately: `play()` holds `_playLock` until the source starts, which never happens headlessly without a user gesture, and `fade` queues rather than applies while that lock is held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
A 3D camera could look up/down and left/right but not bank, which every flight, racing or cockpit game wants. `Camera2d.rotation` is inherited but never read by the 3D view, so `camera.rotate()` on a Camera3d was silently inert — the worst of both worlds. Camera3d composes `roll` into all six sites that build the orientation: world basis as `R(yaw) ∘ R(pitch) ∘ R(roll)`, and its inverse — roll FIRST — in the view matrix and the container transform, whose revert undoes it last. The frustum planes are extracted from `projection × view` via Gribb-Hartmann, so culling follows a banked view with no extra work. Camera2d gets the same name for the same idea, but implemented through its `currentTransform` — which is what a 2D camera's rotation already is, and what `worldToLocal` / `localToWorld` already undo, so picking stays correct under a roll for free. The setter REBUILDS that matrix rather than composing onto it: a camera banked every frame would otherwise accumulate float error and never return to an exact identity. Camera3d overrides the accessor so it writes the angle only, never that matrix. 16 tests, including that the frustum genuinely follows the bank (a tall-thin camera sees a probe upright and culls it rolled) and that the container apply/revert round-trips to identity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
20.5 made `Gradient` public so `Text.fillStyle` could take one, but left no supported way to rasterise it — a user could build a gradient and use it in exactly one place. Both rasterisation methods were `@internal` and stripped from the emitted `.d.ts`, so reaching for either failed the type gate (the AfterBurner example does exactly this). `toCanvasGradient(ctx)` is the right one to publish: it returns a native `CanvasGradient` for the caller's own 2D context and holds no shared state. `toCanvas` stays internal deliberately — it renders into a single shared bake target valid only until the next call by anyone, which is renderer implementation detail rather than something a caller should have to reason about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
…ool name
Found while reviewing the example against the 20.5 API.
`Tween.to(properties, options)` reads `options.duration`; the example
passed a bare number, which has no such property, so every enemy barrel
roll ran the 1000ms default and `ENEMY_ROLL_DURATION_MIN/MAX_MS` were
dead constants.
The backdrop now owns its bank angle instead of hanging an ad-hoc `roll`
property on the camera. `Camera3d.roll` exists now and would bank the
whole view, but this camera sits behind and below the ship rather than in
its cockpit: rolling the view spins everything about the camera's own
forward axis, which swings the player's craft off its anchored
lower-centre spot and drags the enemies around a screen-space reticle
that does not rotate with them. Rolling only the painted backdrop is the
arcade original's trick and stays.
Also: the HUD's text helper typed its settings so loosely that every
caller failed `exactOptionalPropertyTypes`, and `melonjs-performance`
documented `pool.pull("me.Tween")` — a 1.x spelling for a name registered
as plain `"Tween"`, the only `me.`-prefixed pool name left in the skills.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
Belongs with the fade fix two commits back; kept separate rather than rewriting a pushed branch. - drop a `settle()` helper that became dead when the tests moved to driving `fade` on an unplayed clip - give the promise executors block bodies: `new Promise((r) => setTimeout(r, ms))` returns the timer id out of the executor, which `no-promise-executor-return` and `arrow-body-style` both reject Caught by CI because I ran biome rather than the full `pnpm lint`, which is the gate that runs eslint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Four independent changes, one per commit, so they can be reviewed (or dropped) separately.
audio: a degenerate fade no longer poisons the volume withNaNReported:
audio.fade("sound", 0.5, 0.5, 0)leaves the volumeNaN, surfacing later as a non-finitesetValueAtTimeon the next playback. Two faults in the interval the fade starts:len = 0yieldsInfinity(or0/0) anddiff * tickwritesNaN(to < from && …) || (to > from && …), which equal endpoints satisfy neither of — so the interval never cleared, and kept ticking even with a real durationA fade with nothing to interpolate now settles on the target immediately and starts no interval. Four regression tests; removing the guard fails three of them (volume stuck at 1, live interval handles left running) while the "a genuine fade still starts an interval" control passes either way.
Camera:rollA 3D camera could pitch and yaw but not bank.
Camera2d.rotationis inherited but never read by the 3D view, socamera.rotate()on aCamera3dwas silently inert.Camera3dcomposesrollinto all six orientation sites — world basis asR(yaw) ∘ R(pitch) ∘ R(roll), its inverse (roll first) in the view matrix and container transform. The frustum planes come offprojection × viewvia Gribb–Hartmann, so culling follows a banked view for free.Camera2dgets the same name via itscurrentTransform— which is what a 2D camera's rotation already is, and whatworldToLocal/localToWorldalready undo, so picking stays correct under roll. The setter rebuilds that matrix rather than composing onto it, so a camera banked every frame cannot drift; there's a test that assigns 2000 times and asserts an exact identity at 0.16 tests, including that the frustum genuinely follows the bank (a tall-thin camera sees a probe upright, culls it rolled).
Gradient#toCanvasGradientis public20.5 made
Gradientpublic forText.fillStylebut left no supported way to rasterise one — both rasterisation methods were@internaland stripped from the.d.ts.toCanvasstays internal deliberately: it renders into a shared bake target valid only until the next call by anyone.AfterBurner: a silently-ignored tween duration
Tween.to(properties, options)readsoptions.duration; the example passed a bare number, so every enemy barrel roll ran the 1000 ms default and both tuning constants were dead. Also fixes the HUD's over-loose settings type and apool.pull("me.Tween")inmelonjs-performance— a 1.x spelling for a name registered as plain"Tween".The example deliberately does not adopt
Camera3d.roll: its camera sits behind and below the ship rather than in its cockpit, so rolling the view swings the player's craft off its anchor and drags enemies around a screen-space reticle. Rolling only the painted backdrop stays.Verification
pnpm -F melonjs doc: 0 errors;check-doc-readmepasses🤖 Generated with Claude Code
https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t