From 2f5291bb747b7ec827d7ce57908c31c3da21ed81 Mon Sep 17 00:00:00 2001 From: dsent <8774536+dsent@users.noreply.github.com> Date: Sun, 2 Aug 2026 04:05:52 +0200 Subject: [PATCH 1/3] feat(color): expand the palette to 64 tiered slots 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) Claude-Session: https://claude.ai/code/session_019xHYUVmU9C8AcqhUsBFje4 --- src/util/color.lua | 94 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/src/util/color.lua b/src/util/color.lua index b0aa90e4..63267925 100644 --- a/src/util/color.lua +++ b/src/util/color.lua @@ -1,3 +1,49 @@ +local vivid = { + [16] = { 0xFF, 0xA5, 0x00 }, + [17] = { 0x96, 0x4B, 0x00 }, + [18] = { 0xFF, 0xCC, 0x99 }, + [19] = { 0x9A, 0xCD, 0x32 }, + [20] = { 0x00, 0xBF, 0xFF }, + [21] = { 0x8A, 0x2B, 0xE2 }, + [22] = { 0xFF, 0x69, 0xB4 }, + [23] = { 0x99, 0x99, 0x99 }, + [32] = { 0xFF, 0x7F, 0x50 }, + [33] = { 0xFF, 0xD7, 0x00 }, + [34] = { 0x32, 0xCD, 0x32 }, + [35] = { 0x00, 0xFF, 0x80 }, + [36] = { 0x40, 0xE0, 0xD0 }, + [37] = { 0x00, 0x80, 0xFF }, + [38] = { 0x6A, 0x5A, 0xCD }, + [39] = { 0xDC, 0x14, 0x3C }, + [48] = { 0xF0, 0xE6, 0x8C }, + [49] = { 0xFF, 0x80, 0x80 }, + [50] = { 0x99, 0xFF, 0xCC }, + [51] = { 0x99, 0xDD, 0xFF }, + [52] = { 0xCC, 0x99, 0xFF }, + [53] = { 0xDA, 0x70, 0xD6 }, + [54] = { 0x41, 0x69, 0xE1 }, + [55] = { 0x70, 0x80, 0x90 }, +} + +local function palette_color(c) + local tier = math.floor(c / 16) + local position = tier * 16 + c % 8 + local color = vivid[position] + if not color then return end + + local scale = c % 16 >= 8 and 1 or 0.75 + local function channel(value) + return math.floor(scale * value + 0.5) / 255 + end + + return { + channel(color[1]), + channel(color[2]), + channel(color[3]), + 1, + } +end + --- @class Color Color = { color = {}, @@ -5,6 +51,11 @@ Color = { local rc = rawget(Color, c) if rc then return rc end if type(c) ~= 'number' then return end + if c >= 16 then + local color = palette_color(c) + Color[c] = color + return color + end local bright = c > 7 and 1 or 0.75 local oc = c local b = c % 2 @@ -17,21 +68,48 @@ Color = { end, black = 0, -- #000000 - blue = 1, -- #0000bf #0000ff - red = 2, -- #bf0000 #ff0000 - magenta = 3, -- #bf01bf #ff01ff - green = 4, -- #01bf01 #01ff01 - cyan = 5, -- #00bfbf #00ffff - yellow = 6, -- #bfbf00 #ffff00 - white = 7, -- #bfbfbf #ffffff + blue = 1, -- #0000BF #0000FF + red = 2, -- #BF0000 #FF0000 + magenta = 3, -- #BF00BF #FF00FF + green = 4, -- #00BF00 #00FF00 + cyan = 5, -- #00BFBF #00FFFF + yellow = 6, -- #BFBF00 #FFFF00 + white = 7, -- #BFBFBF #FFFFFF bright = 8, + orange = 16, + brown = 17, + tan = 18, + yellowgreen = 19, + skyblue = 20, + purple = 21, + pink = 22, + gray = 23, + + coral = 32, + gold = 33, + limegreen = 34, + springgreen = 35, + turquoise = 36, + azure = 37, + slateblue = 38, + crimson = 39, + + khaki = 48, + salmon = 49, + mint = 50, + powderblue = 51, + lavender = 52, + orchid = 53, + royalblue = 54, + slategray = 55, + valid = function(c) return c and type(c) == 'number' and math.floor(c) == c -- weird way to isInt and c >= 0 - and c < 16 + and c < 64 end, --- @param color table From 5248c74ad3f908d3076ad578de544020766b3f99 Mon Sep 17 00:00:00 2001 From: dsent <8774536+dsent@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:16:52 +0200 Subject: [PATCH 2/3] fix(color): reject every invalid index, not just the too-large ones 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) Claude-Session: https://claude.ai/code/session_019xHYUVmU9C8AcqhUsBFje4 --- src/util/color.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/color.lua b/src/util/color.lua index 63267925..f9a34a85 100644 --- a/src/util/color.lua +++ b/src/util/color.lua @@ -50,7 +50,7 @@ Color = { __index = function(t, c) local rc = rawget(Color, c) if rc then return rc end - if type(c) ~= 'number' then return end + if not Color.valid(c) then return end if c >= 16 then local color = palette_color(c) Color[c] = color From ba587d69aa1177606f8b6cc379c7598b6fcfa59d Mon Sep 17 00:00:00 2001 From: dsent <8774536+dsent@users.noreply.github.com> Date: Sun, 2 Aug 2026 05:53:48 +0200 Subject: [PATCH 3/3] feat(examples): add a colors example for the 64-slot palette 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) Claude-Session: https://claude.ai/code/session_019xHYUVmU9C8AcqhUsBFje4 --- src/examples/colors/README.md | 84 ++++++++++++ src/examples/colors/main.lua | 245 ++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 src/examples/colors/README.md create mode 100644 src/examples/colors/main.lua diff --git a/src/examples/colors/README.md b/src/examples/colors/README.md new file mode 100644 index 00000000..79314373 --- /dev/null +++ b/src/examples/colors/README.md @@ -0,0 +1,84 @@ +## colors + +Prints every color Compy can draw, then paints a picture using only +colors you can call by name. + +Press `Space` to swap between the two screens. + +### The palette + +Compy knows 64 colors, numbered `0` to `63`. You can always ask for one +by number: + +```lua +gfx.setColor(Color[2]) +``` + +...but numbers are hard to read, so every color also has a name. The +name *is* the number — `Color.red` is just another way of writing `2`: + +```lua +gfx.setColor(Color[Color.red]) +``` + +Both lines draw exactly the same red. Use the name; a reader of your +program should not have to remember what `2` looks like. + +### Plain and bright + +Every color comes in two strengths. The plain one is what you get from +the name on its own. Add `Color.bright` for the strong one: + +```lua +gfx.setColor(Color[Color.red]) -- a deep red +gfx.setColor(Color[Color.red + Color.bright]) -- a vivid red +``` + +This works because a name is a number, so you really are doing +arithmetic: `Color.bright` is `8`, and `Color.red` is `2`, so +`Color.red + Color.bright` is `10`. That is why every entry on the +list screen carries a small square to its right: the wide block is +`2 red`, and the square beside it is `10`, the same red gone bright. + +The plain version is the bright one at three quarters strength. Black +is the one color where this changes nothing, so `0` and `8` are both +black. + +### Groups of sixteen + +The 64 colors come in four groups of sixteen. Within any group, the +first eight are the plain colors and the next eight are the same eight +gone bright, which is why adding `8` always moves you from one to the +other. + +| Numbers | What lives there | +| --- | --- | +| `0`-`15` | the classic eight: black, blue, red, magenta, green, cyan, yellow, white | +| `16`-`31` | the everyday extras: orange, brown, tan, pink, purple, grey and friends | +| `32`-`47` | colors that fill in the color wheel: coral, gold, turquoise, azure | +| `48`-`63` | soft, pale colors: khaki, mint, lavender, powderblue | + +### Reading the picture + +The second screen is drawn with nothing but named colors — no raw +numbers anywhere. Look at `drawTree` for the shortest example: + +```lua +function drawTree(x, y) + gfx.setColor(Color[Color.brown]) + gfx.rectangle("fill", x - 12, y - 78, 24, 84) + gfx.setColor(Color[Color.springgreen]) + gfx.circle("fill", x, y - 116, 52) + ... +end +``` + +A trunk in plain `brown`, then leaves in `springgreen` with brighter +greens layered on top. Nowhere does the code say `#964B00`; it says +`brown`, and the reader can picture it. + +### Try it + +* Change a color name in `drawHouse` and run it again. +* Take `Color.bright` off the sun and watch it dull. +* Add a flower to the `FLOWERS` list. Each entry is `{ x, y, color }`. diff --git a/src/examples/colors/main.lua b/src/examples/colors/main.lua new file mode 100644 index 00000000..3f8fc24e --- /dev/null +++ b/src/examples/colors/main.lua @@ -0,0 +1,245 @@ +-- Every color Compy can draw. Each cell is filled with the +-- color it names and labelled with its number; the cell to +-- its right is the same color plus Color.bright. +-- Press Space for a picture painted only with named colors. +WIDTH, HEIGHT = gfx.getDimensions() +SCREEN = 0 +SCREENS = 2 + +NAMES = { + "black", + "blue", + "red", + "magenta", + "green", + "cyan", + "yellow", + "white", + "orange", + "brown", + "tan", + "yellowgreen", + "skyblue", + "purple", + "pink", + "gray", + "coral", + "gold", + "limegreen", + "springgreen", + "turquoise", + "azure", + "slateblue", + "crimson", + "khaki", + "salmon", + "mint", + "powderblue", + "lavender", + "orchid", + "royalblue", + "slategray", +} + +-- list geometry: 11 rows of 3 names. Cell height follows the +-- font rather than a guess, so rows cannot overlap. +CELL_H = gfx.getFont():getHeight() + 6 +TEXT_DY = 3 +TOP = CELL_H + 2 +ROWS = 11 +ROW_STEP = (HEIGHT - TOP - 4) / ROWS +NAME_W = 244 +CHIP_W = 48 +BLOCK_STEP = 330 +BLOCK_X0 = 16 + +-- picture geometry +HORIZON = 350 +FLOWERS = { + { 150, 470, Color.pink + Color.bright }, + { 205, 505, Color.orchid + Color.bright }, + { 262, 462, Color.lavender + Color.bright }, + { 330, 520, Color.salmon + Color.bright }, + { 700, 470, Color.crimson + Color.bright }, + { 762, 512, Color.lavender + Color.bright }, + { 830, 466, Color.pink + Color.bright }, + { 900, 528, Color.orchid + Color.bright }, +} + +gfx.setBackgroundColor(Color[Color.black]) + +function indexOf(row) + local tier = math.floor(row / 8) + return tier * 16 + row % 8 +end + +function labelFor(i) + local c = Color[i] + local lum = 0.299 * c[1] + 0.587 * c[2] + 0.114 * c[3] + if lum > 0.55 then + return Color[Color.black] + end + return Color[Color.white + Color.bright] +end + +function drawNameCell(i, text, x, y) + gfx.setColor(Color[i]) + gfx.rectangle("fill", x, y, NAME_W, CELL_H) + gfx.setColor(labelFor(i)) + gfx.print(text, x + 7, y + TEXT_DY) +end + +function drawChip(i, x, y) + gfx.setColor(Color[i]) + gfx.rectangle("fill", x, y, CHIP_W, CELL_H) + gfx.setColor(labelFor(i)) + gfx.print(tostring(i), x + 7, y + TEXT_DY) +end + +function drawBlock(hue, col, row) + local i = indexOf(hue) + local x = BLOCK_X0 + col * BLOCK_STEP + local y = TOP + row * ROW_STEP + drawNameCell(i, i .. " " .. NAMES[hue + 1], x, y) + drawChip(i + 8, x + NAME_W + 6, y) +end + +function drawList() + local hint = "space: picture" + local hint_w = gfx.getFont():getWidth(hint) + gfx.setColor(Color[Color.white + Color.bright]) + gfx.print(hint, WIDTH - hint_w - BLOCK_X0, 2) + for hue = 0, 31 do + drawBlock(hue, hue % 3, math.floor(hue / 3)) + end +end + +function drawSky() + gfx.setColor(Color[Color.skyblue + Color.bright]) + gfx.rectangle("fill", 0, 0, WIDTH, HORIZON) + gfx.setColor(Color[Color.powderblue + Color.bright]) + gfx.rectangle("fill", 0, HORIZON - 60, WIDTH, 60) +end + +function drawSun() + gfx.setColor(Color[Color.gold]) + gfx.circle("fill", 858, 92, 62) + gfx.setColor(Color[Color.gold + Color.bright]) + gfx.circle("fill", 858, 92, 46) + gfx.setColor(Color[Color.khaki + Color.bright]) + gfx.circle("fill", 858, 92, 30) +end + +function drawCloud(x, y, w) + gfx.setColor(Color[Color.white + Color.bright]) + gfx.ellipse("fill", x, y, w, w * 0.44) + gfx.ellipse("fill", x - w * 0.6, y + 6, w * 0.6, w * 0.3) + gfx.ellipse("fill", x + w * 0.62, y + 5, w * 0.55, w * 0.3) +end + +function drawMountains() + gfx.setColor(Color[Color.slategray]) + gfx.polygon("fill", 60, HORIZON, 250, 120, 440, HORIZON) + gfx.setColor(Color[Color.slategray + Color.bright]) + gfx.polygon("fill", 330, HORIZON, 520, 170, 710, HORIZON) + gfx.setColor(Color[Color.white + Color.bright]) + gfx.polygon("fill", 208, 178, 250, 120, 292, 178) + gfx.polygon("fill", 486, 222, 520, 170, 554, 222) +end + +function drawGround() + gfx.setColor(Color[Color.limegreen]) + gfx.rectangle("fill", 0, HORIZON, WIDTH, HEIGHT - HORIZON) + gfx.setColor(Color[Color.yellowgreen]) + gfx.ellipse("fill", 200, HORIZON + 30, 340, 70) + gfx.ellipse("fill", 820, HORIZON + 24, 300, 60) + gfx.setColor(Color[Color.limegreen + Color.bright]) + gfx.rectangle("fill", 0, 500, WIDTH, HEIGHT - 500) +end + +function drawPond() + gfx.setColor(Color[Color.azure]) + gfx.ellipse("fill", 512, 452, 132, 40) + gfx.setColor(Color[Color.turquoise + Color.bright]) + gfx.ellipse("fill", 500, 445, 92, 22) + gfx.setColor(Color[Color.powderblue + Color.bright]) + gfx.ellipse("fill", 470, 440, 34, 8) +end + +function drawTree(x, y) + gfx.setColor(Color[Color.brown]) + gfx.rectangle("fill", x - 12, y - 78, 24, 84) + gfx.setColor(Color[Color.springgreen]) + gfx.circle("fill", x, y - 116, 52) + gfx.setColor(Color[Color.limegreen + Color.bright]) + gfx.circle("fill", x - 34, y - 96, 38) + gfx.circle("fill", x + 34, y - 98, 36) + gfx.setColor(Color[Color.springgreen + Color.bright]) + gfx.circle("fill", x + 6, y - 140, 28) +end + +function drawHouse() + gfx.setColor(Color[Color.tan + Color.bright]) + gfx.rectangle("fill", 640, 300, 190, 120) + gfx.setColor(Color[Color.crimson]) + gfx.polygon("fill", 620, 302, 735, 226, 850, 302) + gfx.setColor(Color[Color.brown + Color.bright]) + gfx.rectangle("fill", 706, 350, 46, 70) + gfx.setColor(Color[Color.gold + Color.bright]) + gfx.rectangle("fill", 660, 328, 38, 34) + gfx.rectangle("fill", 772, 328, 38, 34) + gfx.setColor(Color[Color.coral + Color.bright]) + gfx.rectangle("fill", 620, 296, 230, 8) +end + +function drawFlowers() + for n = 1, #FLOWERS do + local f = FLOWERS[n] + gfx.setColor(Color[f[3]]) + gfx.circle("fill", f[1], f[2], 9) + gfx.setColor(Color[Color.gold + Color.bright]) + gfx.circle("fill", f[1], f[2], 4) + end +end + +function drawButterfly(x, y) + gfx.setColor(Color[Color.purple + Color.bright]) + gfx.ellipse("fill", x - 11, y - 6, 11, 15) + gfx.ellipse("fill", x + 11, y - 6, 11, 15) + gfx.setColor(Color[Color.orchid + Color.bright]) + gfx.ellipse("fill", x - 9, y + 9, 8, 10) + gfx.ellipse("fill", x + 9, y + 9, 8, 10) + gfx.setColor(Color[Color.brown]) + gfx.ellipse("fill", x, y, 3, 17) +end + +function drawPicture() + drawSky() + drawSun() + drawCloud(180, 96, 62) + drawCloud(430, 66, 46) + drawMountains() + drawGround() + drawHouse() + drawPond() + drawTree(228, HORIZON + 96) + drawTree(946, HORIZON + 74) + drawFlowers() + drawButterfly(566, 300) + gfx.setColor(Color[Color.white + Color.bright]) + gfx.print("space: color list", 830, 2) +end + +function love.draw() + if SCREEN == 0 then + drawList() + return + end + drawPicture() +end + +function love.keypressed(k) + if k == "space" then + SCREEN = (SCREEN + 1) % SCREENS + end +end