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 diff --git a/src/util/color.lua b/src/util/color.lua index b0aa90e4..f9a34a85 100644 --- a/src/util/color.lua +++ b/src/util/color.lua @@ -1,10 +1,61 @@ +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 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