summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--framebuf.lua210
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