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