diff options
-rw-r--r-- | framebuf.lua | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/framebuf.lua b/framebuf.lua new file mode 100644 index 0000000..bf0dac4 --- /dev/null +++ b/framebuf.lua @@ -0,0 +1,210 @@ +local M = {} + +local hex_digits = { + "0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f" +} + +function M.new() + return setmetatable({}, M) +end + +function M:__newindex(k, v) + error("assignment to framebuffer object", 2) +end + +function M:__index(k) + if type(k) == "number" and math.floor(k) == k then + rawset(self, k, {}) + return rawget(self, k) + else + return M[k] + end +end + +function M:clear() + for k in pairs(self) do + if type(k) == "number" then + self[k] = nil + end + end +end + +local function round(x, y) + return math.floor(x + 0.5), math.floor(y + 0.5) +end + +function M:plot(x, y, r, g, b) + local x, y = round(x, y) + self[x][y] = colors.packRGB(r, g, b) +end + +function M:get(x, y) + local x, y = round(x, y) + if self[x][y] then + return colors.unpackRGB(self[x][y]) + end +end + +function M:rect(x1, y1, x2, y2, r, g, b) + for x = x1, x2 do + for y = y1, y2 do + self:plot(x, y, r, g, b) + end + end +end + +function M:ppm(data, x, y) + if type(data) ~= "string" then + local f = data + data = assert(f.readAll()) + f.close() + end + local x, y = round(x or 1, y or 1) + local w, h, depth, data = data:match "^P6\n(%d+) (%d+)\n(%d+)\n(.+)" + assert(w and h and depth, "invalid format; binary PPM (P6) requried") + for dy = 1, h do + for dx = 1, w do + local i = (((dy - 1) * w + dx) - 1) * 3 + local r, g, b = + data:byte(i + 1) / depth, + data:byte(i + 2) / depth, + data:byte(i + 3) / depth + self:plot(dx + x - 1, dy + y - 1, r, g, b) + end + end +end + +-- determine a palette for a set of colors +local function median_cut(cols, nbuckets) + local max, min = {}, {} + for _, color in ipairs(cols) do + for ch, v in ipairs(color) do + if not max[ch] or v > max[ch] then max[ch] = v end + if not min[ch] or v < min[ch] then min[ch] = v end + end + end + local max_variance, max_variant + for ch in ipairs(max) do + local variance = max[ch] - min[ch] + if not max_variance or variance > max_variance then + max_variance = variance + max_variant = ch + end + end + table.sort(cols, function(a, b) return a[max_variant] <= b[max_variant] end) + local partitions = {{}, {}} + local half = math.floor(#cols / 2) + for i = 1, half do + table.insert(partitions[1], cols[i]) + end + for i = half + 1, #cols do + table.insert(partitions[2], cols[i]) + end + local palette, map = {}, {} + if nbuckets <= 2 then + for n, part in ipairs(partitions) do + local avg = {0, 0, 0} + for _, color in ipairs(part) do + for ch, v in ipairs(color) do + avg[ch] = avg[ch] + v + end + map[colors.packRGB(unpack(color))] = n + end + for ch, v in ipairs(avg) do + avg[ch] = avg[ch] / #part + end + palette[n] = colors.packRGB(unpack(avg)) + end + else + local palette1, map1 = median_cut(partitions[1], nbuckets / 2) + local palette2, map2 = median_cut(partitions[2], nbuckets / 2) + for b, m in pairs {[palette1] = map1, [palette2] = map2} do + local offs = #palette + for i, b in ipairs(b) do + palette[i + offs] = b + end + for c, i in pairs(m) do + map[c] = i + offs + end + end + end + return palette, map +end + +local function get_char(fb, x, y) + local cols = {} + local pixels = {} + for y = y, y + 2 do + for x = x, x + 1 do + local color = {fb:get(x, y)} + if color[1] then + table.insert(cols, color) + table.insert(pixels, colors.packRGB(unpack(color))) + else + table.insert(pixels, 0) + end + end + end + if #cols == 0 then + return string.char(0x80) + end + local palette, map = median_cut(cols, 2) + local char = 0x80 + -- TODO: document this + local p6 = map[pixels[6]] or 1 + for i = 1, 5 do + if (map[pixels[i]] or 1) ~= p6 then + char = char + 2^(i - 1) + end + end + local fg, bg + if map[pixels[6]] == 2 then + fg, bg = palette[1], palette[2] + else + fg, bg = palette[2], palette[1] + end + return string.char(char), fg, bg +end + +function M.resolution() + local w, h = term.getSize() + return w * 2, h * 3 +end + +function M:present(offsx, offsy) + offsx, offsy = offsx or 0, offsy or 0 + local w, h = term.getSize() + local pw, ph = M.resolution() + local cols = {} + local chars = {} + for y = offsy + 1, offsy + ph, 3 do + for x = offsx + 1, offsx + pw, 2 do + local char, fg, bg = get_char(self, x, y) + table.insert(chars, {char, fg, bg}) + if fg and bg then + table.insert(cols, {colors.unpackRGB(fg)}) + table.insert(cols, {colors.unpackRGB(bg)}) + end + end + end + local palette, map = median_cut(cols, 16) + for i, color in ipairs(palette) do + term.setPaletteColor(2^(i - 1), color) + end + for i = 1, w * h, w do + term.setCursorPos(1, (i - 1) / w) + local blit_char, blit_fg, blit_bg = {}, {}, {} + for i = i, i + w - 1 do + local char, fg, bg = unpack(chars[i]) + table.insert(blit_char, char) + table.insert(blit_fg, fg and hex_digits[map[fg]] or "1") + table.insert(blit_bg, bg and hex_digits[map[bg]] or "1") + end + term.blit( + table.concat(blit_char), + table.concat(blit_fg), + table.concat(blit_bg)) + end +end + +return M |