feat(color): expand the palette to 64 tiered slots, with a colors example - #48
Open
dsent wants to merge 3 commits into
Open
feat(color): expand the palette to 64 tiered slots, with a colors example#48dsent wants to merge 3 commits into
dsent wants to merge 3 commits into
Conversation
The Color table offered 8 hues at 2 brightness levels, so programs had no orange, brown, pink, purple, or mid grey at all. The palette is now defined by 32 vivid colors, each occupying a bright slot, with its normal slot at 0.75x that value. That is already how indices 0-15 were built, so one rule covers all 64 slots and the existing region stops being a special case. Indices 0-15 keep their values byte for byte; 16-63 were unused, rejected by Color.valid and returning recycled-bit values, so the change is additive. New hues sit in three tiers of eight, ordered essential, hue-wheel coverage, and nice-to-have, so the palette can be read or trimmed a tier at a time. Bare names resolve to the normal slot, as Color.red always has, and + Color.bright selects the vivid one. Names follow recognizable pencil-box meanings where those differ from CSS: brown is #964B00, since CSS brown #A52A2A is hue 0 and reads as brick red. The palette already differed from CSS, with red at #BF0000 and green at #00BF00. Also corrects the hex comments on the legacy names, which claimed #bf01bf for magenta and #01bf01 for green where the real values are #BF00BF and #00BF00. Verified: all 64 slots and 32 name constants match the intended values, indices 0-15 are unchanged, the suite passes at 756 tests, and an on-device render of every slot matches the intended palette pixel for pixel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xHYUVmU9C8AcqhUsBFje4
Color[64] returned nil while Color[-1] returned a color, because the guard on the palette lookup only bounded the top. Anything below 16 fell into the bit decomposition, which never had a bounds check, so negative and fractional indices came back as recycled-bit colors. Color.valid already defined what an index may be. It just was not enforced where indexing happens; it sat beside __index as advice callers had to remember to take. __index now gates on it, so the contract has one definition and every rejected index behaves the same. Color[-1], Color[1.5], Color[64] and Color["7"] are now all nil, while 0-63 and the 32 names are unchanged. This also subsumes the old type(c) ~= 'number' check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xHYUVmU9C8AcqhUsBFje4
Collaborator
Author
aldum
reviewed
Aug 4, 2026
Nothing bundled showed the palette, so the only way to learn what a number draws was to try it. The first screen lists all 64 colors, one name per line: the plain color on the left, the same name plus Color.bright on the right, each entry printed in the color it names. Space switches to a picture drawn only with named colors, where the code reads brown and springgreen rather than raw values. Both black entries print invisibly against the background, which is what black on black looks like; the README says so rather than leaving it a puzzle. The README teaches the palette as the program shows it: a name is a number, adding Color.bright is arithmetic on that number, and the four groups of sixteen are why adding 8 always moves between plain and bright. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xHYUVmU9C8AcqhUsBFje4
dsent
force-pushed
the
dsent/color-palette-64
branch
from
August 4, 2026 17:29
8c64aea to
ba587d6
Compare
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.


What
Colorfrom 16 to 64 slots, in four tiers of sixteenorange,brown,tan,yellowgreen,skyblue,purple,pink,gray,coral,gold,limegreen,springgreen,turquoise,azure,slateblue,crimson,khaki,salmon,mint,powderblue,lavender,orchid,royalblue,slategrayColor.validfromc < 16toc < 64__indexgate onColor.valid, so every index it rejects returnsnil#bf01bffor magenta and#01bf01for green where the values are#BF00BFand#00BF00colorsexample that prints the whole palette and paints a picture using only named colorsWhy
Eight hues is a thin box of pencils. Children reach for orange, brown, pink and purple immediately, and there was no way to name any of them — a drawing program either settled for the wrong color or dropped to raw RGB, which is exactly what
Colorexists to avoid.The tiers are ordered by how much they earn their place, so the palette can be cut at any 16-slot boundary without losing expressiveness:
0-1516-3132-4748-63How it works
One rule covers all 64 slots. The palette is 32 vivid colors; each sits in a bright slot, and the normal slot below it is that color at 0.75 strength. That is already how
0-15was built —brightwas1and plain was0.75— so the change generalizes the existing scheme rather than bolting a second one beside it.A bare name still resolves to the normal slot, as
Color.redalways has;+ Color.brightselects the vivid one. Lazy computation and caching in__indexare unchanged.Naming follows CSS except where the pencil-box meaning differs.
brownis#964B00: CSSbrownis#A52A2A, which is hue 0 and reads as brick red on the device.Compatibility
Indices
0-15keep their values byte for byte, and the 8 existing name constants are untouched. Indices16-63were rejected byColor.validbefore this change, so nothing correct depended on them.One behavior change worth stating plainly:
Color[n]is nownilfor any indexColor.validrejects — negative, fractional,64and up, and non-numbers. Previously those fell through to the bit decomposition and came back as a recycled-bit color;Color[-1]returned a gray.Color.validrejected them before and after, so no correct program is affected, but a program misusing an index now fails onnilatgfx.setColorinstead of quietly drawing something wrong.The example
src/examples/colorshas two screens, toggled withSpace: every slot with its number and name, and a picture drawn entirely from named colors. Its README teaches the one idea a child needs — a color name is a number, soColor.red + Color.brightis just2 + 8.Validation
dev0..1; all 32 vivid slots match their intended hex; every normal slot is its vivid slot at 0.75 strength; all 32 names map to a normal slot;0-15reproduce the legacy bit formula byte for byte; the nine rejected index forms all yieldnil— 662 assertions, 0 failures. The same assertions run against unmodifieddevfail 131 times, so they are not vacuous.16-63wrong.just zip-examples-allpackagescolors.compyalongside the existing examples