Skip to content

Handle a palette with fewer than two colors in Palette.At - #7

Merged
peterhellberg merged 1 commit into
peterhellberg:masterfrom
youdie006:palette-at-degenerate
Sep 7, 2026
Merged

peterhellberg merged 1 commit into
peterhellberg:masterfrom
youdie006:palette-at-degenerate

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

Palette.At is documented as taking a value in range 0-1, and it guards t <= 0, t >= 1 and NaN, so it looks total over that range. It is not, for a palette with fewer than two colors.

p := gfx.Palette{color.NRGBA{R: 255, A: 255}}
p.At(0.5)
At(0) = {255 0 0 255}
At(1) = {255 0 0 255}
panic: runtime error: index out of range [1] with length 1
gfx.Palette.At(...)
	palette.go:102

Both endpoints return correctly and only the interior of the documented range panics, which is why it survives a smoke test. The cause is at palette.go:99-102: n-1 is the divisor, so with n == 1 it is 0, s becomes +Inf, i stays 0, and the last line reads p[i+1]. An empty palette is worse -- it panics at p[0] for every t.

Why I picked these return values

Rather than invent a convention I followed the two sibling methods on the same type:

  • Color(n) (palette.go:15) bounds-checks and returns color.NRGBA{}
  • Convert (palette.go:48) guards len(p) == 0 and returns color.RGBA{}

So the empty case returns the same transparent zero value the rest of the type already returns for "nothing to give", and the single-color case returns that color, which is what both existing endpoint guards already do for t <= 0 and t >= 1.

Convert being guarded is not hypothetical -- palette_test.go:65 constructs Palette{} and passes it to Convert, so an empty palette is already an expected input to this type. CmplxPhaseAt (cmplx.go:56) forwards a [0,1] value straight into At.

Behaviour change

Only on inputs that previously panicked. No existing test row changes.

Verification

go test -count=1 ./... passes both packages; gofmt -l . empty; go vet ./... clean. The workflow runs go test -v ./... on Go 1.25.x/1.26.x; I ran go1.26.3.

I checked the boundary from both sides rather than only confirming the new rows go green:

  • revert the guard -- TestPaletteAt fails with the panic above, so the test reaches the bug
  • over-correct n == 1 to n <= 2 -- the new {Palette{ColorBlack, ColorWhite}, 0.5, ...} row fails with r = 0, want 32767

The two-color row is there specifically to pin that side, so the boundary is exactly 1 rather than merely "not the old behaviour".


Disclosure: AI-assisted. I found and prepared this with an AI assistant, and I ran and verified everything above myself.

At interpolates between p[i] and p[i+1] using n-1 as the divisor. With a
single color that divisor is zero, so any t strictly between 0 and 1
indexes p[1] on a length-1 slice and panics. An empty palette panics at
p[0] for every t.

Color(n) and Convert already guard the degenerate cases on the same
type; At was the one that did not.
@peterhellberg
peterhellberg merged commit 3315c42 into peterhellberg:master Sep 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants