mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
267f53fccd
Add a Colour setting to Settings > Home: Accents, the default, colours each widget's marks and draws Continue reading in its cover's deep shade; Tinted also washes every tile in its widget's colour; Off keeps the greys. Black-and- white screens draw as before, colour e-readers get stronger shades, and night mode keeps each colour's hue.
209 lines
11 KiB
Lua
209 lines
11 KiB
Lua
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
|
|
|
|
local BLACK = { grey = 0 }
|
|
package.loaded["ffi/blitbuffer"] = {
|
|
COLOR_BLACK = BLACK,
|
|
ColorRGB32 = function(r, g, b, a) return { r = r, g = g, b = b, a = a } end,
|
|
isColor8 = function(c) return c.grey ~= nil end,
|
|
}
|
|
package.loaded["gettext"] = function(value) return value end
|
|
local pixels_of = {}
|
|
package.loaded["ui/renderimage"] = {
|
|
renderImageFile = function(_self, path)
|
|
local list = pixels_of[path]
|
|
if not list then return nil end
|
|
return {
|
|
getWidth = function() return #list end,
|
|
getHeight = function() return 1 end,
|
|
getPixel = function(_bb, x)
|
|
local p = list[x + 1]
|
|
return { getColorRGB32 = function() return { r = p[1], g = p[2], b = p[3] } end }
|
|
end,
|
|
free = function() end,
|
|
}
|
|
end,
|
|
}
|
|
|
|
local Colour = require("bookorbit_board_colour")
|
|
local Layout = require("bookorbit_board_layout")
|
|
local Store = require("bookorbit_board_store")
|
|
|
|
local function hex(c) return string.format("#%02x%02x%02x", c.r, c.g, c.b) end
|
|
local function rgb(s) return tonumber(s:sub(2, 3), 16), tonumber(s:sub(4, 5), 16), tonumber(s:sub(6, 7), 16) end
|
|
local function chroma(s)
|
|
local r, g, b = rgb(s)
|
|
return math.max(r, g, b) - math.min(r, g, b)
|
|
end
|
|
local function luma(s)
|
|
local r, g, b = rgb(s)
|
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
end
|
|
|
|
-- Colour math.
|
|
assert(Colour.mix("#000000", "#ffffff", 0.5) == "#808080", "mix halves")
|
|
assert(Colour.boost("#777777", 1.4) == "#777777", "boost leaves a gray alone")
|
|
assert(chroma(Colour.boost("#2b61d6", 1.4)) > chroma("#2b61d6"), "boost pushes chroma out")
|
|
assert(math.abs(luma(Colour.boost("#2b61d6", 1.4)) - luma("#2b61d6")) < 3, "boost keeps luma")
|
|
assert(Colour.forNight("#808080") == "#808080", "night leaves a gray alone")
|
|
do
|
|
-- Inverting what is painted for night shows the colour's own hue.
|
|
local painted = Colour.forNight("#de4127")
|
|
local r, g, b = rgb(painted)
|
|
local hn = Colour.rgbToHls((255 - r) / 255, (255 - g) / 255, (255 - b) / 255)
|
|
local r0, g0, b0 = rgb("#de4127")
|
|
local h0, l0 = Colour.rgbToHls(r0 / 255, g0 / 255, b0 / 255)
|
|
local _, lp = Colour.rgbToHls(r / 255, g / 255, b / 255)
|
|
assert(math.abs(hn - h0) < 0.01, "night shows the same hue")
|
|
assert(math.abs(lp - l0) < 0.01, "night paints at the same lightness")
|
|
end
|
|
|
|
-- Cover families.
|
|
local function fill(n, p) local t = {} for i = 1, n do t[i] = p end return t end
|
|
assert(Colour.familyFromPixels(fill(100, { 128, 128, 128 })) == nil, "a gray jacket has no colour")
|
|
assert(Colour.familyFromPixels(fill(100, { 250, 250, 250 })) == nil, "a white jacket has no colour")
|
|
do
|
|
local mostly_gray = fill(97, { 30, 30, 30 })
|
|
for _i = 1, 3 do table.insert(mostly_gray, { 40, 90, 200 }) end
|
|
assert(Colour.familyFromPixels(mostly_gray) == nil, "a few coloured pixels are not enough")
|
|
local blue = fill(60, { 40, 90, 200 })
|
|
for _i = 1, 40 do table.insert(blue, { 20, 20, 20 }) end
|
|
local fam = Colour.familyFromPixels(blue)
|
|
assert(fam, "a blue jacket has a family")
|
|
assert(luma(fam.deep) < luma(fam.ink) and luma(fam.ink) < luma(fam.tint) and luma(fam.tint) < luma(fam.wash), "deep, ink, tint, wash get lighter")
|
|
local r, g, b = rgb(fam.ink)
|
|
assert(b > r and b > g, "the family keeps the jacket's hue")
|
|
-- Deep carries white text.
|
|
assert(luma(fam.deep) < 80, "deep is dark enough for white text")
|
|
end
|
|
pixels_of["/covers/ocean.jpg"] = fill(50, { 30, 120, 160 })
|
|
pixels_of["/covers/plain.jpg"] = fill(50, { 200, 200, 200 })
|
|
assert(Colour.coverFamily("/covers/ocean.jpg"), "a cover file gives its family")
|
|
pixels_of["/covers/ocean.jpg"] = nil
|
|
assert(Colour.coverFamily("/covers/ocean.jpg"), "a cover's family is kept")
|
|
assert(Colour.coverFamily("/covers/plain.jpg") == nil and Colour.coverFamily("/covers/plain.jpg") == nil, "no family, asked twice")
|
|
assert(Colour.coverFamily(nil) == nil, "no path, no family")
|
|
|
|
-- What reaches the screen.
|
|
local GREY = { grey = 136 }
|
|
local function ctx(scheme, extra)
|
|
local c = { colour = scheme, tiles = "outline", d = { continue_reading = { { cover_path = "/covers/ocean.jpg" } } } }
|
|
for k, v in pairs(extra or {}) do c[k] = v end
|
|
return c
|
|
end
|
|
local LCD, EINK = { colour = true, eink = false, night = false }, { colour = true, eink = true, night = false }
|
|
local MONO = { colour = false, eink = true, night = false }
|
|
|
|
Colour._env = MONO
|
|
for _, scheme in ipairs(Colour.SCHEMES) do
|
|
local c = ctx(scheme, { tile_key = "streak" })
|
|
assert(Colour.ink(c, GREY) == GREY and Colour.track(c, GREY) == GREY and Colour.mid(c, GREY) == GREY, scheme .. ": a black-and-white screen keeps its grays")
|
|
assert(Colour.inkFill(c, "hero") == BLACK, scheme .. ": Continue reading stays black there")
|
|
local base = { bg = GREY, border = 1, border_col = GREY }
|
|
assert(Colour.chrome(c, "line", "highlight", base, 1, 2) == base, scheme .. ": tiles unchanged there")
|
|
assert(Colour.icon(c, 2.25).col == nil and Colour.icon(c, 2.25).sw == 2.25, scheme .. ": icons unchanged there")
|
|
end
|
|
|
|
Colour._env = LCD
|
|
do
|
|
local off = ctx("off", { tile_key = "streak" })
|
|
assert(Colour.ink(off, GREY) == GREY, "Off keeps the grays on a colour screen")
|
|
local acc = ctx("accents", { tile_key = "streak" })
|
|
assert(hex(Colour.ink(acc, GREY)) == Colour.FAM.orange.ink, "Streak is orange")
|
|
assert(hex(Colour.track(acc, GREY)) == Colour.FAM.orange.tint, "a bar runs on its own tint")
|
|
assert(Colour.paint("#123456") == Colour.paint("#123456"), "one object per colour")
|
|
assert(Colour.icon(acc, 2.25).sw == 2.25, "a phone keeps icon weights")
|
|
-- Accents: tiles as they are, the highlight yellow, Continue reading in its cover.
|
|
local base = { bg = GREY, border = 1, border_col = GREY, radius = 4 }
|
|
assert(Colour.chrome(acc, "line", "streak", base, 1, 2) == base, "Accents keeps a tile's own chrome")
|
|
local hl = Colour.chrome(acc, "fill", "highlight", base, 1, 2)
|
|
assert(hex(hl.bg) == Colour.FAM.yellow.wash and hl.border == 0 and hl.radius == 4, "Highlight of the day is a yellow card")
|
|
local lead = Colour.coverFamily("/covers/ocean.jpg")
|
|
assert(hex(Colour.chrome(acc, "ink", "hero", base, 1, 2).bg) == lead.deep, "Continue reading takes its cover's deep shade")
|
|
assert(Colour.chrome(acc, "ink", "clock", base, 1, 2).bg == BLACK, "the clock stays black in Accents")
|
|
assert(Colour.chrome(acc, "ink", "moon", base, 1, 2).bg == BLACK, "the moon stays black in Accents")
|
|
assert(Colour.onInk(acc, GREY, "clock") == GREY, "quiet text on black stays gray")
|
|
assert(Colour.onInk(acc, GREY, "hero") ~= GREY, "quiet text on the cover's shade takes a pale shade")
|
|
local plain = ctx("accents", { d = { continue_reading = { { cover_path = "/covers/plain.jpg" } } } })
|
|
assert(Colour.inkFill(plain, "hero") == BLACK, "a jacket without colour keeps the black tile")
|
|
-- Tinted: the wash, with the tile style's edge in the widget's colour.
|
|
local outline = ctx("tinted", { tile_key = "week" })
|
|
local ch = Colour.chrome(outline, "line", "week", base, 1, 2)
|
|
assert(hex(ch.bg) == Colour.FAM.green.wash and ch.border == 1 and hex(ch.border_col) == Colour.mix(Colour.FAM.green.wash, Colour.FAM.green.ink, 0.4), "Outline: wash and a hairline")
|
|
local soft = Colour.chrome(ctx("tinted", { tiles = "soft" }), "line", "week", base, 1, 2)
|
|
assert(hex(soft.bg) == Colour.FAM.green.wash and soft.border == 0, "Soft: the wash alone")
|
|
local ink = Colour.chrome(ctx("tinted", { tiles = "ink" }), "line", "week", base, 1, 2)
|
|
assert(ink.border == 2 and hex(ink.border_col) == Colour.FAM.green.deep, "Ink: a firm deep edge")
|
|
assert(hex(Colour.chrome(outline, "ink", "clock", base, 1, 2).bg) == Colour.FAM.blue.deep, "the clock goes deep blue when tinted")
|
|
assert(hex(Colour.track(outline, GREY)) == "#ffffff", "bars run on white in a washed tile")
|
|
assert(hex(Colour.halo(outline, GREY)) == Colour.FAM.green.wash, "fanned covers sit in the wash")
|
|
assert(Colour.halo(acc, GREY) == GREY, "and keep their white halo on white tiles")
|
|
assert(Colour.chrome(outline, "open", "week", base, 1, 2) == base, "open tiles stay open")
|
|
end
|
|
|
|
Colour._env = EINK
|
|
do
|
|
local acc = ctx("accents", { tile_key = "streak" })
|
|
local ink = hex(Colour.ink(acc, GREY))
|
|
assert(ink == Colour.boost(Colour.FAM.orange.ink, 1.4), "a colour e-reader gets the boosted shade")
|
|
assert(Colour.icon(acc, 2.25).sw > 2.25, "and heavier icons")
|
|
end
|
|
|
|
Colour._env = { colour = true, eink = false, night = true }
|
|
do
|
|
local acc = ctx("accents", { tile_key = "streak" })
|
|
assert(hex(Colour.ink(acc, GREY)) == Colour.forNight(Colour.FAM.orange.ink), "night mode paints the turned hue")
|
|
assert(Colour.icon(acc, 2.25).col == Colour.forNight(Colour.FAM.orange.ink), "icons too")
|
|
end
|
|
Colour._env = nil
|
|
|
|
-- The setting.
|
|
assert(Store.read({}).colour == "accents", "Accents by default")
|
|
assert(Store.read({ catalog_board = { version = 1, colour = "loud" } }).colour == "accents", "an unknown value reads as Accents")
|
|
assert(Store.withColour({}, "tinted").colour == "tinted", "Tinted is kept")
|
|
assert(Store.withColour({ catalog_board = { version = 1, tiles = "ink", colour = "tinted" } }, "off").tiles == "ink", "changing colour keeps the tile style")
|
|
assert(Colour.normalize(nil) == "accents" and Colour.normalize("off") == "off", "normalize")
|
|
|
|
-- No two touching tiles on a starter page share a colour or a look-alike
|
|
-- wash, on any screen; a dark ink tile may touch a pale one of its family.
|
|
local FIXED = { highlight = true, hero = true, chapters = true, clock = true, analog = true, moon = true }
|
|
local ALIKE = { gold = "yellow", yellow = "gold", rose = "red", red = "rose", teal = "green", green = "teal" }
|
|
local function clash(a, b) return a == b or ALIKE[a] == b end
|
|
for _, cols in ipairs({ 2, 4, 6 }) do
|
|
for _, landscape in ipairs({ false, true }) do
|
|
for _, has_light in ipairs({ true, false }) do
|
|
for _, page in ipairs(Store.starterPages(cols, "reader", has_light, landscape)) do
|
|
local packed = Layout.pack(page.base, cols)
|
|
local at = {}
|
|
for _, it in ipairs(packed.placed) do
|
|
for y = it.y, it.y + it.rh - 1 do
|
|
for x = it.x, it.x + it.cw - 1 do
|
|
at[y .. ":" .. x] = it
|
|
end
|
|
end
|
|
end
|
|
local function fam(it)
|
|
local key = Colour.keyFor(it.type, it.props)
|
|
return Colour.HUE[key]
|
|
end
|
|
for _, it in ipairs(packed.placed) do
|
|
for y = it.y, it.y + it.rh - 1 do
|
|
for _, other in ipairs({ at[y .. ":" .. (it.x + it.cw)] }) do
|
|
if other and other ~= it and not FIXED[it.type] and not FIXED[other.type] then
|
|
assert(not clash(fam(it), fam(other)), string.format("%d cols%s %s: %s beside %s", cols, landscape and " landscape" or "", page.id, it.type, other.type))
|
|
end
|
|
end
|
|
end
|
|
for x = it.x, it.x + it.cw - 1 do
|
|
local other = at[(it.y + it.rh) .. ":" .. x]
|
|
if other and other ~= it and not FIXED[it.type] and not FIXED[other.type] then
|
|
assert(not clash(fam(it), fam(other)), string.format("%d cols%s %s: %s above %s", cols, landscape and " landscape" or "", page.id, it.type, other.type))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
print("bookorbit_board_colour_test: ok")
|