Repository navigation
Particles: pin the randomFrame contract, drop an unreachable clamp - #1730
Merged
Merged
Conversation
Follow-up to #1728, plus its changelog entry. Two test findings from reviewing it. The suite caught ten of eleven mutations, and the one that survived was `randomFrame` particles ANIMATING instead of holding their frame, which is half of what that setting promises. The existing test looks like it covers this: it mocks `Math.random` and re-checks the frame after an update. It cannot, because `0.75` picks the LAST of two frames and the age-based index at 99.9% of the lifetime is also the last frame, so the two answers coincide and the assertion passes either way. Mocking `0.25` picks the first frame, which an animating particle leaves almost at once, and the mutation then fails on both backends. The second is a clamp around the frame index that nothing can reach. `timer.update` already pins a negative delta to zero at the source, so `life` only shrinks and a particle whose life hits zero returns before that line; `ageRatio` is therefore in `(0, 1]` and the floored progress cannot leave the sequence. Removed, with the two facts that keep it in range written down, and a boundary test that walks a whole lifetime and asserts every frame drawn is one of the sequence's own cells. That test catches `round`, `ceil` and an off-by-one on the length. Also documents that `randomFrame` on its own does nothing, since the grid check keys off `framewidth`/`frameheight`/`frames` and a random choice needs a grid to choose from, and that the variety is in the frame rather than the colour, `tint` being one value per emitter. 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.
Follow-up to #1728, which merged without a CHANGELOG entry. Adds that entry, crediting @snowyukitty, plus two findings from reviewing the change.
The surviving mutation
#1728's suite is strong: of eleven mutations I ran against it, ten were caught. The one that survived was
randomFrameparticles animating instead of holding their frame, which is half of what that setting promises.The existing test looks like it covers this. It mocks
Math.random, reads the frame, updates, and reads again:It cannot catch it.
0.75picks the last of two frames, and the age-based index at 99.9% of a 1000ms lifetime is also the last frame. The two answers coincide, so the assertion passes whether the particle held its frame or animated to the end of the sequence.Mocking
0.25picks the first frame, which an animating particle leaves almost immediately. The mutation then fails on both backends, which is the point of the test.The unreachable clamp
The frame index was wrapped in
Math.max(0, Math.min(length - 1, ...)). Nothing can reach either bound:timer.updatealready pins a negative delta to zero at the source (timer.ts:238, the long-standing fix for the startup timestamp some browsers report), sodtis never negative andlifeonly shrinks.lifereaches 0 returns before that line.So
ageRatiois in(0, 1], the floored progress is in[0, length - 1], and the clamp is dead code. Removed, with both of those facts written down next to it, because an unreachable guard is worse than an absent one: it reads as the thing keeping the index in range, and the two facts that actually do would be free to change out from under it.A new boundary test guards the removal by walking a whole lifetime in 10ms steps and asserting every frame drawn is one of the sequence's own cells, with the first and last both reached. It catches
round,ceiland an off-by-one on the length.One documentation gap
randomFrame: trueon its own does nothing, because the grid check keys offframewidth/frameheight/framesand a random choice needs a grid to choose from. Now said in both the skill and the setting's doc comment, along with the fact that the variety is in the frame rather than the colour,tintbeing one value per emitter.Verification
pnpm lint: 0 errors (175 warnings, the unchanged baseline)🤖 Generated with Claude Code
https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t