diff --git a/src/conf/colors.lua b/src/conf/colors.lua index b367fba1..e670f5ad 100644 --- a/src/conf/colors.lua +++ b/src/conf/colors.lua @@ -71,11 +71,11 @@ return { input = { console = { bg = Color[Color.white], - fg = Color[Color.black + Color.bright], + fg = Color[Color.black], }, user = { bg = Color[Color.white], - fg = Color[Color.black + Color.bright], + fg = Color[Color.black], }, inspect = { bg = Color[Color.white], diff --git a/src/examples/colors/README.md b/src/examples/colors/README.md new file mode 100644 index 00000000..475742f2 --- /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. Three +quarters of black is still black, so `8` is a dark grey picked by +hand, and adding `Color.bright` always takes you somewhere new. + +### 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 diff --git a/src/util/color.lua b/src/util/color.lua index b0aa90e4..df2eaec9 100644 --- a/src/util/color.lua +++ b/src/util/color.lua @@ -1,10 +1,68 @@ +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 = {}, __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 + return color + end + -- Three quarters of black is black, so the one slot the + -- scaling rule cannot fill is set by hand. + Color.bright + -- now reaches a different color from every slot. + if c == Color.bright then + Color[c] = { 64 / 255, 64 / 255, 64 / 255, 1 } + return Color[c] + end local bright = c > 7 and 1 or 0.75 local oc = c local b = c % 2 @@ -16,22 +74,49 @@ Color = { return Color[oc] 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 + black = 0, -- #000000 #404040 + 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 diff --git a/src/util/debug.lua b/src/util/debug.lua index b4b992d2..50c28ef2 100644 --- a/src/util/debug.lua +++ b/src/util/debug.lua @@ -503,7 +503,7 @@ local debug = function(...) local args = { ... } local ts = string.format("%.3f ", os.clock()) local s = annot(ts .. 'DEBUG ', - (Color.black + Color.bright), args) + (Color.black), args) printer(s) end local once = function(...) diff --git a/src/util/termcolor.lua b/src/util/termcolor.lua index 80593be4..47b445c6 100644 --- a/src/util/termcolor.lua +++ b/src/util/termcolor.lua @@ -2,12 +2,27 @@ require("util.color") local reset = '\27[0m' +--- Slots 16-63 are absolute RGB with no basic-SGR +--- equivalent, so they go out as truecolor. +---@param c number[] +---@return string +local rgb_control = function(c) + local byte = function(v) + return math.floor(v * 255 + 0.5) + end + return string.format('\27[38;2;%d;%d;%dm', + byte(c[1]), byte(c[2]), byte(c[3])) +end + ---@param ci number ---@return string local to_control = function(ci) - if type(ci) ~= 'number' or ci < 0 or ci > 15 then + if not Color.valid(ci) then return reset end + if ci > 15 then + return rgb_control(Color[ci]) + end local bright = '' if ci > Color.bright then bright = '1;' diff --git a/tests/util/color_spec.lua b/tests/util/color_spec.lua new file mode 100644 index 00000000..44b93903 --- /dev/null +++ b/tests/util/color_spec.lua @@ -0,0 +1,29 @@ +require("util.color") + +describe('Color #debug', function() + local hex = function(i) + return Color.to_hex(Color[i]) + end + + it('gives black a bright slot of its own', function() + assert.same('#000000FF', hex(Color.black)) + assert.same('#404040FF', + hex(Color.black + Color.bright)) + end) + + it('moves every slot when bright is added', function() + for d = 0, 63 do + if d % 16 < 8 then + assert.are_not.same(hex(d), hex(d + Color.bright), + 'slot ' .. d .. ' does not move') + end + end + end) + + it('leaves the rest of the classic sixteen alone', function() + assert.same('#0000BFFF', hex(Color.blue)) + assert.same('#0000FFFF', hex(Color.blue + Color.bright)) + assert.same('#BFBFBFFF', hex(Color.white)) + assert.same('#FFFFFFFF', hex(Color.white + Color.bright)) + end) +end) diff --git a/tests/util/termcolor_spec.lua b/tests/util/termcolor_spec.lua index 3103133e..49cc27a3 100644 --- a/tests/util/termcolor_spec.lua +++ b/tests/util/termcolor_spec.lua @@ -1,6 +1,33 @@ local tc = require("util.termcolor") --- @diagnostic disable param-type-mismatch +describe('to_control #debug', function() + it('names the basic slots', function() + assert.same('\27[34m', tc.to_control(Color.blue)) + assert.same('\27[1;94m', + tc.to_control(Color.blue + Color.bright)) + end) + + it('spells the extended slots out in full', function() + assert.same('\27[38;2;191;124;0m', + tc.to_control(Color.orange)) + assert.same('\27[38;2;255;165;0m', + tc.to_control(Color.orange + Color.bright)) + assert.same('\27[38;2;191;161;0m', + tc.to_control(Color.gold)) + assert.same('\27[38;2;84;96;108m', + tc.to_control(Color.slategray)) + end) + + it('resets what Color.valid rejects', function() + assert.same(tc.reset, tc.to_control(-1)) + assert.same(tc.reset, tc.to_control(1.5)) + assert.same(tc.reset, tc.to_control(64)) + assert.same(tc.reset, tc.to_control('7')) + assert.same(tc.reset, tc.to_control(nil)) + end) +end) + describe('colorize_memaddress #debug', function() local frame = function(s) return tc.reset