Created
November 8, 2019 20:03
-
-
Save polyzium/fd025f59eb8714473a4a87d2c19872d0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- Double Buffered Magic -- | |
local component = require("component") | |
local unicode = require("unicode") | |
local color = require("color") | |
local format = require("format") | |
local gpuProxy = component.gpu | |
if not gpuProxy then | |
error("A graphics card is required for screen.lua") | |
end | |
-- Buffer which stores all the changes | |
-- that are to be displayed on the screen | |
local bufferWidth, bufferHeight | |
local bufferBackground, bufferForeground, bufferSymbol | |
local changeBackgrounds, changeForegrounds, changeSymbols | |
-- Limits for drawing on the buffer, to avoid | |
-- checking for excessive changes (Bounds rectangle where changes occured) | |
local updateBoundX1, updateBoundY1, updateBoundX2, updateBoundY2 = 1, 1, bufferWidth, bufferHeight | |
-- True "bounding box", anything outside won't be rendered | |
local drawX1, drawY1, drawX2, drawY2 | |
-- Current fg / bg colors for set and fill | |
local currentBackground, currentForeground = 0, 0xFFFFFF | |
-- Optimization for lua / gpu proxying | |
local GPUfill, GPUset, GPUsetBackground, GPUsetForeground | |
local GPUgetResolution, GPUsetResolution, GPUgetPaletteColor, GPUcopy, GPUsetResolution, GPUsetPaletteColor | |
local GPUgetBackground, GPUgetForeground | |
local rep, sub, len = string.rep, unicode.sub, unicode.len | |
local floor, ceil, min, max, abs, sqrt, sin, cos = math.floor, math.ceil, | |
math.min, math.max, math.abs, math.sqrt, math.sin, math.cos | |
local concat = table.concat | |
local unpack = unpack or table.unpack | |
-- Constants | |
local fillIfAreaIsGreaterThan = 40 | |
local function utfByte(s) | |
one, two, three = s:byte(1,-1) | |
return (one % 16 * 4096 + two % 64 * 64 + three % 64) | |
end | |
-- Convert x y coords to a buffer index | |
local function getIndex(x, y) | |
return bufferWidth * (y - 1) + x | |
end | |
-- Convert index to x y coords | |
local function getCoords(index) | |
local y = floor(index / buffWidth) + 1 | |
local x = index - (y - 1) * buffWidth | |
return x, y | |
end | |
function checkMultiArg(ctype, ...) | |
for index, value in ipairs({...}) do | |
if type(value) ~= ctype then | |
error("Bad argument #" .. index .. " (" .. ctype .. " expected, got " .. type(value) .. ")") | |
end | |
end | |
end | |
-- Check if two characters at the buffers are | |
-- equal. Equality is checked by symbol, fg and bg | |
-- unless the character is whitespace, in which case | |
-- fg is ignored | |
local function areEqual(symbol, changeSymbol, background, changeBackground, foreground, changeForeground) | |
-- If symbols don't match or backgrounds match always unequal | |
if symbol ~= changeSymbol or background ~= changeBackground then return false end | |
if symbol == " " then return true end | |
return foreground == changeForeground | |
end | |
-- Palette index = (-index - 1), this takes the "absolute" | |
-- value to convert to the proper index if negative | |
local function absColor(val) | |
if val < 0 then return -val - 1 end | |
return val | |
end | |
-- Normalize a color to a 24 bit int (Including) | |
-- palette indexes | |
local function normalizeColor(val) | |
if val < 0 then return GPUgetPaletteColor(-val - 1) end | |
return val | |
end | |
-------------------------------------------------------- | |
-- Flush the buffer and re-create each array -- | |
local function flush(w, h) | |
if w == nil or h == nil then | |
w, h = GPUgetResolution() | |
end | |
checkMultiArg("number", w, h) | |
bufferWidth, bufferHeight = w, h | |
updateBoundX1, updateBoundX2, updateBoundY1, updateBoundY2 = w, 1, h, 1 | |
drawX1, drawY1, drawX2, drawY2 = 1, 1, bufferWidth, bufferHeight | |
bufferBackground, bufferForeground, bufferSymbol = {}, {}, {} | |
changeBackgrounds, changeForegrounds, changeSymbols = {}, {}, {} | |
-- Prefill the buffers to avoid rehashing -- | |
for i = 1, w * h do | |
bufferBackground[i], bufferForeground[i], bufferSymbol[i] = 0, 0, " " | |
changeBackgrounds[i], changeForegrounds[i], changeSymbols[i] = 0, 0, " " | |
end | |
end | |
-- Set a specific character (internal method) -- | |
local function setChar(x, y, fgColor, bgColor, symbol) | |
if x < drawX1 or x > drawX2 or y < drawY1 or y > drawY2 then return false end | |
-- Don't check arg types in this function as this function is used A LOT | |
-- internally and checking args actually slows down a full screen render | |
-- by up to 100 ms | |
local i = getIndex(x, y) | |
-- Replace empty braille with regular space | |
if symbol == "⠀" then symbol = " " end | |
-- Update draw bounds if needed | |
updateBoundX1, updateBoundX2 = min(x, updateBoundX1), max(x, updateBoundX2) | |
updateBoundY1, updateBoundY2 = min(x, updateBoundY1), max(x, updateBoundY2) | |
changeBackgrounds[i], changeForegrounds[i], changeSymbols[i] = bgColor, fgColor, symbol | |
end | |
-- Write changes to the screen | |
local function update(force) | |
-- Force update all bounds | |
if force then | |
updateBoundX1, updateBoundY1, updateBoundX2, updateBoundY2 = 1, 1, bufferWidth, bufferHeight | |
end | |
-- If there have been no changes then ignore | |
if updateBoundX1 > updateBoundX2 or updateBoundY1 > updateBoundY2 then return end | |
-- i = current index | |
-- lineChange = index increment when changing to next y value | |
-- subgroup = the foreground subgroup of the background dict | |
-- searchX = x value of end of repeated length | |
-- searchIndex = index of end of repeated length | |
-- tempLine = temp table to store line of chars | |
-- colorChanges = dict of bg / fg color pixels grouped togther | |
-- tempLineSize, subGroupSize = temp varable to keep track of sizes of these tables | |
local i = getIndex(updateBoundX1, updateBoundY1) | |
local lineChange = bufferWidth - updateBoundX2 + updateBoundX1 - 1 | |
local subgroup, searchX, searchIndex, tempLineSize, subgroupSize | |
local colorChanges, tempLine = {}, {} | |
for y = updateBoundY1, updateBoundY2 do | |
x = updateBoundX1 | |
while x <= updateBoundX2 do | |
if changeBackgrounds[i] == nil or changeBackgrounds[i] == -17 or changeForegrounds[i] == -17 then | |
-- Ignore transparent characters | |
-- If char is same as buffer below don't update it | |
-- unless the force parameter is true. | |
elseif force or not areEqual(bufferSymbol[i], changeSymbols[i], bufferBackground[i], changeBackgrounds[i], bufferForeground[i], changeForegrounds[i]) then | |
::doChange:: | |
bufferSymbol[i], bufferBackground[i], bufferForeground[i] = changeSymbols[i], changeBackgrounds[i], changeForegrounds[i] | |
tempLine = { changeSymbols[i] } | |
tempLineSize, searchX, searchIndex = 1, x + 1, i + 1 | |
while searchX <= updateBoundX2 do | |
if changeBackgrounds[i] == changeBackgrounds[searchIndex] and | |
(changeSymbols[searchIndex] == " " or | |
changeForegrounds[i] == changeForegrounds[searchIndex]) | |
then | |
-- Update current "image" buffer | |
bufferSymbol[searchIndex], bufferBackground[searchIndex], bufferForeground[searchIndex] = | |
changeSymbols[searchIndex], changeBackgrounds[searchIndex], changeForegrounds[searchIndex] | |
tempLine[tempLineSize + 1] = changeSymbols[searchIndex] | |
searchX, searchIndex, tempLineSize = searchX + 1, searchIndex + 1, tempLineSize + 1 | |
else break end | |
end | |
if colorChanges[changeBackgrounds[i]] == nil then colorChanges[changeBackgrounds[i]] = {} end | |
if colorChanges[changeBackgrounds[i]][changeForegrounds[i]] == nil then colorChanges[changeBackgrounds[i]][changeForegrounds[i]] = {} end | |
subgroup = colorChanges[changeBackgrounds[i]][changeForegrounds[i]] | |
subgroupSize = #subgroup | |
subgroup[subgroupSize + 1], subgroup[subgroupSize + 2] = x, y | |
subgroup[subgroupSize + 3] = concat(tempLine) | |
i = i + searchX - x - 1 | |
x = searchX - 1 | |
end | |
-- This is required to avoid infinite loops | |
-- when buffers are the same | |
x = x + 1 | |
i = i + 1 | |
end | |
i = i + lineChange | |
end | |
-- Draw color groups | |
local currentForeground | |
for backgroundColor, foregrounds in pairs(colorChanges) do | |
GPUsetBackground(absColor(backgroundColor), backgroundColor < 0) | |
for foregroundColor, group2 in pairs(foregrounds) do | |
if currentForeground ~= foregroundColor then | |
GPUsetForeground(absColor(foregroundColor), foregroundColor < 0) | |
currentForeground = foregroundColor | |
end | |
for i = 1, #group2, 3 do | |
GPUset(group2[i], group2[i + 1], group2[i + 2]) | |
end | |
end | |
end | |
-- Reset the drawX drawY bounds to largest / smallest possible | |
updateBoundX1, updateBoundX2 = bufferWidth, 1 | |
updateBoundY1, updateBoundY2 = bufferHeight, 1 | |
colorChanges = nil | |
end | |
-- Set the drawing bounds. If useCurrent is True it will take the min | |
-- of the current bound and the updated bound | |
local function setDrawingBound(x1, y1, x2, y2, useCurrent) | |
-- If no arguments are passed reset to full screen | |
if x1 == nil then | |
drawX1, drawY1, drawX2, drawY2 = 1, 1, bufferWidth, bufferHeight | |
return | |
end | |
checkMultiArg("number", x1, y1, x2, y2) | |
x1, y1, x2, y2 = floor(x1), floor(y1), floor(x2), floor(y2) | |
if useCurrent then | |
-- Take the intersection of rectangles defined by the corners | |
-- (drawX1, drawY1), (drawX2, drawY2) and (x1, y1), (x2, y2) | |
local x3, y3, x4, y4 = max(drawX1, x1), max(drawY1, y1), min(drawX2, x2), min(drawY2, y2) | |
if x3 < x4 and y3 < y4 then | |
drawX1, drawY1, drawX2, drawY2 = x3, y3, x4, y4 | |
end | |
else | |
-- Overwrite any changes | |
drawX1, drawY1, drawX2, drawY2 = max(1, x1), max(1, y1), min(bufferWidth, x2), min(bufferHeight, y2) | |
end | |
-- Bound checks | |
drawX1, drawX2 = max(1, drawX1), min(bufferWidth, drawX2) | |
drawY1, drawY2 = max(1, drawY1), min(bufferHeight, drawY2) | |
-- Invalid corners (x1, y1) must be top left corner | |
if drawX1 >= drawX2 or drawY1 >= drawY2 then | |
error("Rectangle defined by corners (" .. drawX1 .. ", " .. drawY1 .. | |
"), (" .. drawX2 .. ", " .. drawY2 .. ") is not valid (First corner must be top left)") | |
end | |
end | |
local function resetDrawingBound() | |
drawX1, drawY1, drawX2, drawY2 = 1, 1, bufferWidth, bufferHeight | |
end | |
-- Getter for drawing bounds | |
local function getDrawingBound() | |
return drawX1, drawY1, drawX2, drawY2 | |
end | |
-- Raw get for buffer values | |
local function getRaw(x, y) | |
local index = getIndex(x, y) | |
if changeBackgrounds[index] ~= nil and changeBackgrounds[index] ~= -17 and changeForegrounds[index] ~= -17 then | |
return normalizeColor(changeBackgrounds[index]), normalizeColor(changeForegrounds[index]), changeSymbols[index] | |
end | |
return normalizeColor(bufferBackground[index]), normalizeColor(bufferForeground[index]), bufferSymbol[index] | |
end | |
-- Additional functions for the screen | |
-- Override GPU Functions | |
-- All files will now utilize the buffered drawing code | |
-- instead of native gpu codem which should be abstracted | |
local function setBackground(color, isPalette) | |
checkArg(1, color, "number") | |
checkArg(2, isPalette, "boolean", "nil") | |
local prev = currentBackground | |
if isPalette then currentBackground = -color - 1 else currentBackground = color end | |
if prev < 0 then return GPUgetPaletteColor(-prev - 1), -prev - 1 end | |
return prev | |
end | |
local function setForeground(color, isPalette) | |
checkArg(1, color, "number") | |
checkArg(2, isPalette, "boolean", "nil") | |
local prev = currentForeground | |
if isPalette then currentForeground = -color - 1 else currentForeground = color end | |
if prev < 0 then return GPUgetPaletteColor(-prev - 1), -prev - 1 end | |
return prev | |
end | |
local function getBackground() | |
return absColor(currentBackground), currentBackground < 0 | |
end | |
local function getForeground() | |
return absColor(currentForeground), currentForeground < 0 | |
end | |
local function set(x, y, string, vertical) | |
checkMultiArg("number", x, y) | |
checkArg(3, string, "string") | |
x, y = floor(x), floor(y) | |
if (not vertical and (y < 1 or y > bufferHeight or x > bufferWidth or x + len(string) < 1)) or | |
(vertical and (x < 1 or x > bufferWidth or y > bufferHeight or y + len(string) < 1)) then | |
return false | |
end | |
for delta = 0, len(string) - 1 do | |
if vertical then | |
setChar(x, y + delta, currentForeground, currentBackground, sub(string, delta + 1, delta + 1)) | |
else | |
setChar(x + delta, y, currentForeground, currentBackground, sub(string, delta + 1, delta + 1)) | |
end | |
end | |
return true | |
end | |
-- Copy a region by a displacement tx and ty | |
local function copy(x, y, w, h, tx, ty) | |
checkMultiArg("number", x, y, w, h, tx, ty) | |
x, y, w, h = floor(x), floor(y), floor(w), floor(h) | |
if x > bufferWidth or y > bufferHeight or w < 1 or h < 1 or | |
x + tx > bufferWidth or y + ty > bufferHeight then return false end | |
-- If ty > 0 then scan bottom up, else top down | |
-- If tx > 0 then scan right-left, else left-right | |
local x1, x2, y1, y2 = x, x + w - 1, y, y + h - 1 | |
local incX, incY = 1, 1 | |
local background, foreground, symbol | |
if tx > 0 then x1, x2, incX = x2, x1, -1 end | |
if ty > 0 then y1, y2, incY = y2, y1, -1 end | |
for x = x1, x2, incX do | |
for y = y1, y2, incY do | |
background, foreground, symbol = getRaw(x, y) | |
setChar(x + tx, y + ty, foreground, background, symbol) | |
end | |
end | |
return true | |
end | |
local function fill(x, y, w, h, symbol) | |
checkMultiArg("number", x, y, w, h) | |
checkArg(5, symbol, "string") | |
x, y, w, h = floor(x), floor(y), floor(w), floor(h) | |
if len(symbol) ~= 1 or x > bufferWidth or y > bufferHeight or w < 1 or h < 1 then return false end | |
for x1 = x, x + w - 1 do | |
for y1 = y, y + h - 1 do | |
set(x1, y1, symbol) | |
end | |
end | |
return true | |
end | |
local function setResolution(w, h) | |
checkMultiArg("number", w, h) | |
local success = GPUsetResolution(w, h) | |
if sucess then | |
flush() | |
GPUsetBackground(0) | |
GPUfill(0, 0, w, h, " ") | |
end | |
return success | |
end | |
local function getResolution() | |
return bufferWidth, bufferHeight | |
end | |
local function getWidth() | |
return bufferWidth | |
end | |
local function getHeight() | |
return bufferHeight | |
end | |
-- Raw method to set current background | |
-- to adapt to the background of x, y and | |
-- current foreground to blend | |
-- Optionally, blendBg can be set to False to | |
-- not use adapative background | |
-- Returns the character to use for the fill | |
local function setAdaptive(x, y, cfg, cbg, alpha, blendBg, blendBgalpha, symbol) | |
-- Ignore if x, y not in screen buffer bounds | |
if x < 1 or y < 1 or x > bufferWidth or y > bufferHeight then return "" end | |
if alpha == 1 and blendBgalpha then | |
if cbg ~= nil and cbg ~= false then setBackground(cbg) end | |
if cfg ~= nil and cfg ~= false then setForeground(cfg) end | |
return symbol | |
end | |
local bg, fg, sym = getRaw(x, y) | |
if blendBg then | |
if blendBgalpha then setBackground(color.blend(bg, cbg, 1 - alpha)) | |
else setBackground(color.getProminentColor(fg, bg, sym)) end | |
end | |
setForeground(color.blend(cfg, fg, alpha)) | |
-- If filling with a space it's better if one fills with the background symbol | |
-- instead of losing accuracy by overwriting it with a space | |
::symbolcheck:: | |
if sym == nil then bg, fg, sym = getRaw(x, y) end -- Since the goto skips this line | |
--[[ | |
if symbol == " " then | |
setForeground(color.blend(fg, cbg, 1 - alpha)) | |
return sym | |
elseif symbol == "█" or symbol == "⣿" then -- Full block doesn't blend with current background | |
return symbol | |
end | |
return symbol -- Otherwise fill with the symbol | |
]] | |
if ((bg == cbg or blendBg) and fg == cfg) then | |
if (utfByte(sym) > 0x2800 and utfByte(sym) < 0x28FF) then | |
if (utfByte(symbol) > 0x2800 and utfByte(symbol) < 0x28FF) then | |
symbol = require("bit32").bor(utfByte(sym), utfByte(symbol)) | |
end | |
end | |
end | |
return unicode.char(symbol) | |
end | |
-- Screen drawing methods -- | |
-- Important note: alpha is equal to alpha value, meaning 1 = visible, 0 = invisible -- | |
-- Since all functions below basically do the same variable checking this | |
-- function takes care of it. Returns the following: | |
-- (should func return false), x, y, w, h, alpha, currentForegroundSave, currentBackgroundSave, cfg, cbg | |
local function processVariables(x, y, w, h, alpha, dontFloor) | |
if alpha == 0 then return false end -- No alpha no render | |
if not dontFloor then x, y, w, h = floor(x), floor(y), floor(w), floor(h) end | |
if alpha == nil then alpha = 1 end | |
checkMultiArg("number", x, y, w, h, alpha) | |
local currentForegroundSave, currentBackgroundSave = currentForeground, currentBackground | |
local cfg, cbg = normalizeColor(currentForeground), normalizeColor(currentBackground) | |
return true, x, y, w, h, alpha, currentForegroundSave, currentBackgroundSave, cfg, cbg | |
end | |
-- For thin drawing functions often each point in a braille char is | |
-- checked with some function that returns either 0 or 1 | |
local function brailleHelper(func, x, y, x0, y0, ...) | |
return format.getBrailleChar( | |
func(x, y, x0, y0, unpack({...})), | |
func(x + 0.5, y, x0, y0, unpack({...})), | |
func(x, y + 0.25, x0, y0, unpack({...})), | |
func(x + 0.5, y + 0.25, x0, y0, unpack({...})), | |
func(x, y + 0.5, x0, y0, unpack({...})), | |
func(x + 0.5, y + 0.5, x0, y0, unpack({...})), | |
func(x, y + 0.75, x0, y0, unpack({...})), | |
func(x + 0.5, y + 0.75, x0, y0, unpack({...})) | |
) | |
end | |
local function setSetAdaptive(x, y, cfg, cbg, alpha, blendBg, blendBgAlpha, symbol) | |
set(x, y, setAdaptive(x, y, cfg, cbg, alpha, blendBg, blendBgAlpha, symbol)) | |
end | |
local function rectangleOutlineHelper(x, y, w, h, alpha, tl, tr, bl, br, ht, hb, vl, vr, swapCfg, blendBg) | |
local _, x, y, w, h, alpha, currentForegroundSave, currentBackgroundSave, cfg, cbg = processVariables(x, y, w, h, alpha) | |
if not _ then return false end | |
-- Corners | |
if swapCfg then cfg = cbg end -- All geometry should use background color | |
setSetAdaptive(x, y, cfg, cbg, alpha, true, blendBg, tl) | |
setSetAdaptive(x + w - 1, y, cfg, cbg, alpha, true, blendBg, tr) | |
setSetAdaptive(x, y + h - 1, cfg, cbg, alpha, true, blendBg, bl) | |
setSetAdaptive(x + w - 1, y + h - 1, cfg, cbg, alpha, true, blendBg, br) | |
-- Top and bottom | |
for x1 = x + 1, x + w - 2 do | |
setSetAdaptive(x1, y, cfg, cbg, alpha, true, blendBg, ht) | |
setSetAdaptive(x1, y + h - 1, cfg, cbg, alpha, true, blendBg, hb) | |
end | |
-- Sides | |
for y1 = y + 1, y + h - 2 do | |
setSetAdaptive(x, y1, cfg, cbg, alpha, true, blendBg, vl) | |
setSetAdaptive(x + w - 1, y1, cfg, cbg, alpha, true, blendBg, vr) | |
end | |
-- Reset original bg / fg colors | |
currentBackground, currentForeground = currentBackgroundSave, currentForegroundSave | |
return true | |
end | |
-- Draw a rectangle (border) with current bg color, | |
-- with optional alpha | |
local function drawRectangleOutline(x, y, w, h, alpha, symbol) | |
checkArg(6, symbol, "string", "nil") | |
symbol = symbol or " " | |
return rectangleOutlineHelper(x, y, w, h, alpha, symbol, symbol, symbol, symbol, symbol, symbol, symbol, symbol, false, true) | |
end | |
-- Fill a rectangle with the current bg color, | |
-- with optional alpha. Because of alpha | |
-- we have to reimplement fill code | |
local function drawRectangle(x, y, w, h, alpha, symbol) | |
local _, x, y, w, h, alpha, currentForegroundSave, currentBackgroundSave, cfg, cbg = processVariables(x, y, w, h, alpha) | |
if not _ then return false end | |
checkArg(6, symbol, "string", "nil") | |
symbol = symbol or " " | |
for x1 = x, x + w - 1 do | |
for y1 = y, y + h - 1 do | |
if x1 < 1 or y1 < 1 or x1 > bufferWidth then goto continue end | |
if y1 > bufferHeight then break end | |
setSetAdaptive(x1, y1, cfg, cbg, alpha, true, true, symbol) | |
::continue:: | |
end | |
end | |
-- Reset original bg / fg colors | |
currentBackground, currentForeground = currentBackgroundSave, currentForegroundSave | |
return true | |
end | |
-- Draw a rectangle (border) with current bg color, | |
-- with optional alpha | |
local function drawThinRectangleOutline(x, y, w, h, alpha) | |
return rectangleOutlineHelper(x, y, w, h, alpha, "┌", "┐", "└", "┘", "─", "─", "│", "│", true, false) | |
end | |
local function drawBrailleRectangleOutline(x, y, w, h, alpha) | |
return rectangleOutlineHelper(x, y, w, h, alpha, "⡏", "⢹", "⣇", "⣸", "⠉", "⣀", "⡇", "⢸", true, false) | |
end | |
local function subRectangleHelper(x1, y1, x, y, w, h) | |
if x1 >= x and x1 < x + w and y1 >= y and y1 < y + h then return 1 end | |
return 0 | |
end | |
-- Fill a rectangle with the current bg color, | |
-- with optional alpha. Because of alpha | |
-- we have to reimplement fill code | |
local function drawBrailleRectangle(x, y, w, h, alpha) | |
local _, x, y, w, h, alpha, currentForegroundSave, currentBackgroundSave, cfg, cbg = processVariables(x, y, w, h, alpha, true) | |
if not _ then return false end | |
local subChar | |
cfg = cbg -- Use background color for all drawing | |
for x1 = floor(x), floor(x + w) do | |
for y1 = floor(y), floor(y + h) do | |
if x1 < 1 or y1 < 1 or x1 > bufferWidth then goto continue end | |
if y1 > bufferHeight then break end | |
subChar = brailleHelper(subRectangleHelper, x1, y1, x, y, w, h) | |
setSetAdaptive(x1, y1, cfg, cbg, alpha, true, false, subChar) | |
::continue:: | |
end | |
end | |
-- Reset original bg / fg colors | |
currentBackground, currentForeground = currentBackgroundSave, currentForegroundSave | |
return true | |
end | |
-- Draw a string at the location (Straight line, newlines ignore) | |
-- If alpha is enabled, foreground is blended w/ bg | |
-- If blendBg is enabled, the background will be selected to try | |
-- to camouflage itself with the existing buffer | |
local function drawText(x, y, string, alpha, blendBg) | |
if alpha == 0 then return false end -- No alpha no render | |
if blendBg == nil then blendBg = true end | |
x, y = floor(x), floor(y) | |
if y < 1 or y > bufferHeight then return end | |
-- Save current colors | |
local currentForegroundSave, currentBackgroundSave = currentForeground, currentBackground | |
local cfg, cbg = normalizeColor(currentForeground), normalizeColor(currentBackground) | |
if alpha == nil then alpha = 1 end | |
checkMultiArg("number", x, y) | |
checkArg(3, string, "string") | |
checkArg(4, alpha, "number") | |
checkArg(5, blendBg, "boolean") | |
for dx = 0, len(string) - 1 do | |
if x < 1 then goto continue end | |
if x > bufferWidth then break end | |
setSetAdaptive(x + dx, y, cfg, cbg, alpha, blendBg, false, sub(string, dx + 1, dx + 1)) | |
::continue:: | |
end | |
-- Reset original bg / fg colors | |
currentBackground, currentForeground = currentBackgroundSave, currentForegroundSave | |
return true | |
end | |
local function ellipseHelper(x, y, a, b, alpha, outlineOnly, symbol) | |
local _, x, y, a, b, alpha, currentForegroundSave, currentBackgroundSave, cfg, cbg = processVariables(x, y, a, b, alpha, false) | |
if not _ then return false end | |
checkArg(6, symbol, "string", "nil") | |
symbol = symbol or " " | |
if outlineOnly then | |
local thetaInc = 1 / max(a + 1, b + 1) | |
local dx1, dy1, prevdx1, prevdy1 | |
local halfpi = 0.5 * math.pi | |
for theta = 0, halfpi - thetaInc, thetaInc do | |
dx1 = floor(cos(theta) * a) | |
dy1 = floor(sin(theta) * b) | |
-- Overlapping point | |
if dx1 == prevdx1 and dy1 == prevdy1 then goto continue end | |
prevdx1, prevdy1 = dx1, dy1 | |
-- x1, y1 is in first quadrent, use symmetry | |
setSetAdaptive(x + dx1, y + dy1, cfg, cbg, alpha, true, true, symbol) | |
setSetAdaptive(x - dx1, y + dy1, cfg, cbg, alpha, true, true, symbol) | |
-- Avoid overlap on x-sides | |
if dy1 ~= 0 then | |
setSetAdaptive(x + dx1, y - dy1, cfg, cbg, alpha, true, true, symbol) | |
setSetAdaptive(x - dx1, y - dy1, cfg, cbg, alpha, true, true, symbol) | |
end | |
::continue:: | |
end | |
-- Fill in caps on top and bottom, since there is overlap and we subtracted thetaInc from loop | |
setSetAdaptive(x, y + b, cfg, cbg, alpha, true, true, symbol) | |
setSetAdaptive(x, y - b, cfg, cbg, alpha, true, true, symbol) | |
else | |
local a2, b2 = a * a, b * b -- Store the axis squared | |
local computedBound | |
for dx = 0, a do | |
for dy = 0, b do | |
computedBound = dx * dx / a2 + dy * dy / b2 | |
if computedBound > 1 then goto continue end | |
-- First quadrent | |
setSetAdaptive(x + dx, y + dy, cfg, cbg, alpha, true, true, symbol) | |
-- If dx = 0 and dy = 0 then don't draw any other quadrent to avoid overlap | |
if dx == 0 and dy == 0 then -- Do nothing | |
-- If dx = 0 then don't draw left side of ellipse to avoid overlap | |
elseif dx == 0 then | |
setSetAdaptive(x + dx, y - dy, cfg, cbg, alpha, true, true, symbol) | |
-- If dy = 0 then don't draw bottom half to avoid overlap | |
elseif dy == 0 then | |
setSetAdaptive(x - dx, y + dy, cfg, cbg, alpha, true, true, symbol) | |
-- Draw all other quadrents | |
else | |
setSetAdaptive(x - dx, y + dy, cfg, cbg, alpha, true, true, symbol) | |
setSetAdaptive(x + dx, y - dy, cfg, cbg, alpha, true, true, symbol) | |
setSetAdaptive(x - dx, y - dy, cfg, cbg, alpha, true, true, symbol) | |
end | |
::continue:: | |
end end | |
end | |
-- Reset original bg / fg colors | |
currentBackground, currentForeground = currentBackgroundSave, currentForegroundSave | |
return true | |
end | |
local function drawEllipseOutline(x, y, a, b, alpha, symbol) | |
return ellipseHelper(x, y, a, b, alpha, true, symbol) | |
end | |
local function drawEllipse(x, y, a, b, alpha, symbol) | |
ellipseHelper(x, y, a, b, alpha, false, symbol) | |
end | |
-- Sub ellipse pixel helper function | |
-- Returns 0 if outside ellipse else 1 | |
-- x1, y1 is current point | |
-- x, y is ellipse center | |
-- a2, b2 is the axii squared | |
local function subEllipseHelper(x1, y1, x, y, a2, b2, onEllipse) -- a2 = a^2, b2 = b^2 | |
-- If we're just doing an outline check distance = 1 (on ellipse) | |
if onEllipse then | |
-- For the given y1 value we'd expect a point at x1 = x - (sqrt(a2) sqrt(b2 - y^2 + 2 y y1 - y1^2))/sqrt(b2) | |
local expectedX = x - (sqrt(a2) * sqrt(b2 - y * y + 2 * y * y1 - y1 * y1)) / sqrt(b2) | |
expectedX = floor(expectedX * 2) / 2 -- Round to nearest 0.5 | |
if x1 == expectedX then return 1 end | |
if x + x - x1 == expectedX then return 1 end -- Since quadratic there are 2 x1s that satisfy | |
-- Since there could be multiple x values for a given y, we also need to check | |
-- expected y, which is y1 = y - (sqrt(b2) sqrt(a2 - x^2 + 2 x x1 - x1^2))/sqrt(a2) | |
local expectedY = y - (sqrt(b2) * sqrt(a2 - x * x + 2 * x * x1 - x1 * x1)) / sqrt(a2) | |
expectedY = floor(expectedY * 4) / 4 -- Round to nearest 0.25 | |
if y1 == expectedY then return 1 end | |
if y + y - y1 == expectedY then return 1 end -- Since quadratic there are 2 y1s that satisfy | |
return 0 | |
end | |
local dis = (x1 - x) * (x1 - x) / a2 + (y1 - y) * (y1 - y) / b2 | |
if dis > 1 then return 0 end | |
return 1 | |
end | |
local function subEllipseTemplate(x, y, a, b, alpha, justOutline) | |
local _, x, y, a, b, alpha, currentForegroundSave, currentBackgroundSave, cfg, cbg = processVariables(x, y, a, b, alpha, true) | |
if not _ then return false end | |
local a2, b2 = a * a, b * b -- Store the axis squared | |
local a2minus1, b2minus1 = (a - 1) * (a - 1), (b - 1) * (b - 1) | |
local interiorDistance, subChar, charToDraw | |
-- Directly fill if area is large enough | |
for x1 = floor(x - a), floor(x + a) do | |
for y1 = floor(y - b), floor(y + b) do | |
if y1 > bufferHeight then break end | |
if x1 < 1 or y1 < 1 or x1 > bufferWidth then goto continue end | |
-- More efficent to do additional comparison for interior, to skip | |
-- 8 subEllipseHelper calls for a full braille character | |
interiorDistance = (x1 - x) * (x1 - x) / a2minus1 + (y1 - y) * (y1 - y) / b2minus1 | |
if interiorDistance <= 1 then | |
if justOutline then subChar = " " | |
else subChar = "⣿" end | |
else | |
-- Iterate "subpixels" | |
subChar = brailleHelper(subEllipseHelper, x1, y1, x, y, a2, b2, justOutline) | |
end | |
if subChar == "⠀" or subChar == " " then goto continue end -- Skip empty braille or space | |
-- Same as drawText but without the checks | |
currentForegroundSave, currentBackgroundSave = currentForeground, currentBackground | |
cfg, cbg = normalizeColor(currentForeground), normalizeColor(currentBackground) | |
if subChar == "⣿" then | |
-- Filler in the middle of the ellipse, we can fill with a space | |
-- and use default set adaptive behaviour | |
subChar = " " | |
currentBackground = currentForeground | |
setSetAdaptive(x1, y1, cfg, cbg, alpha, true, true, subChar) | |
else | |
-- Side bars should have the background equal to the blended value | |
-- However since braille is filled with foreground we swap bg and fg | |
-- and set the bg to whatever the current bg is at that value | |
charToDraw = setAdaptive(x1, y1, cfg, cbg, alpha, true, true, subChar) | |
-- Just outline ellipse stuff needs proper color blending | |
if justOutline then | |
currentForeground = color.blend(currentBackgroundSave, getRaw(x1, y1), alpha) | |
else currentForeground = currentBackground end | |
currentBackground = getRaw(x1, y1) | |
set(x1, y1, charToDraw) | |
end | |
currentBackground, currentForeground = currentBackgroundSave, currentForegroundSave | |
::continue:: | |
end end | |
return true | |
end | |
local function drawBrailleEllipse(x, y, a, b, alpha) | |
return subEllipseTemplate(x, y, a, b, alpha, false) | |
end | |
local function drawBrailleEllipseOutline(x, y, a, b, alpha) | |
return subEllipseTemplate(x, y, a, b, alpha, true) | |
end | |
local function lineHelper(x1, y1, x2, y2, alpha, vertLineFunc, horzLineFunc, gtHalfLineFunc, ltHalfLineFunc) | |
if alpha == 0 then return false end -- No alpha no render | |
if alpha == nil then alpha = 1 end | |
checkMultiArg("number", x1, y1, x2, y2, alpha) | |
if x2 < x1 then -- Swap coordinates if needed | |
x1, x2, y1, y2 = x2, x1, y2, y1 | |
end | |
local cfg, cbg = normalizeColor(currentForeground), normalizeColor(currentBackground) | |
local currentForegroundSave, currentBackgroundSave = currentForeground, currentBackground | |
-- Vertical lines | |
if x1 == x2 then | |
if y1 > y2 then y1, y2 = y2, y1 end -- Swap if not in right order | |
for y = y1, y2 - 1 do | |
if y > bufferHeight or y < 1 then goto continue end | |
vertLineFunc(x1, y, cfg, cbg, alpha, x1, y1, 0, currentBackgroundSave, currentForegroundSave) | |
::continue:: | |
end | |
-- Reset original bg / fg colors | |
currentBackground, currentForeground = currentBackgroundSave, currentForegroundSave | |
return true | |
end | |
-- Horziontal lines | |
if y1 == y2 then | |
for x = x1, x2 - 1 do | |
if x < 1 then goto continue end | |
if x > bufferWidth then break end | |
horzLineFunc(x, y1, cfg, cbg, alpha, x1, y1, 0, currentBackgroundSave, currentForegroundSave) | |
::continue:: | |
end | |
-- Reset original bg / fg colors | |
currentBackground, currentForeground = currentBackgroundSave, currentForegroundSave | |
return true | |
end | |
local gradient = (y2 - y1) / (x2 - x1) | |
if abs(gradient) > 0.5 then | |
if y1 > y2 then x1, x2, y1, y2 = x2, x1, y2, y1 end -- Swap if not in right order | |
local x -- Store x coordinate to set | |
for y = 0, ceil(y2 - y1 - 1) do | |
x = floor(y / gradient + x1) | |
if y + y1 < 1 or y + y1 > bufferHeight or x > bufferWidth or x < 1 then goto continue end | |
gtHalfLineFunc(x, y + y1, cfg, cbg, alpha, x1, y1, gradient, currentBackgroundSave, currentForegroundSave) | |
::continue:: | |
end | |
else | |
local y -- Store y coordinate to set | |
for x = 0, ceil(x2 - x1 - 1) do | |
y = floor(gradient * x + y1) | |
if y < 1 or y > bufferHeight or x + x1 > bufferWidth or x + x1 < 1 then goto continue end | |
ltHalfLineFunc(x + x1, y, cfg, cbg, alpha, x1, y1, gradient, currentBackgroundSave, currentForegroundSave) | |
::continue:: | |
end | |
end | |
-- Reset original bg / fg colors | |
currentBackground, currentForeground = currentBackgroundSave, currentForegroundSave | |
return true | |
end | |
local function drawLine(x1, y1, x2, y2, alpha, lineChar) | |
x1, x2, y1, y2 = floor(x1), floor(x2), floor(y1), floor(y2) | |
lineChar = lineChar or " " | |
checkArg(6, lineChar, "string", "nil") | |
local function genericLineFunc(x, y, cfg, cbg, alpha) | |
setSetAdaptive(x, y, cfg, cbg, alpha, true, true, lineChar) | |
end | |
return lineHelper(x1, y1, x2, y2, alpha, genericLineFunc, genericLineFunc, genericLineFunc, genericLineFunc) | |
end | |
-- Returns 1 if x, y is on the line, else 0 | |
-- Note this differs from other functions, as | |
-- x1, y1 is lefmost point of the line, and x, y | |
-- is the current point | |
local function subLineHelper(x, y, x1, y1, gradient) | |
if abs(gradient) > 0.5 then | |
local expectedX = (y - y1) / gradient + x1 | |
expectedX = floor(expectedX * 2) / 2 | |
if expectedX == x then return 1 end | |
return 0 | |
else | |
local expectedY = (x - x1) * gradient + y1 | |
expectedY = floor(expectedY * 4) / 4 | |
if expectedY == y then return 1 end | |
return 0 | |
end | |
end | |
-- More helper functions for sub line to reduce code copy-paste | |
-- Performance is slightly impacted but it's not noticable | |
-- plus it saves a kilobyte of storage / ram to load this module | |
local function genericSubLineFunc(x, y, cfg, cbg, alpha, char, currentBackgroundSave, currentForegroundSave) | |
currentForeground = color.blend(currentBackgroundSave, getRaw(x, y), alpha) | |
currentBackground = getRaw(x, y) | |
set(x, y, char) | |
::continue:: | |
end | |
local function subLineSlopeHelperFunc(x, y, cfg, cbg, alpha, x1, y1, gradient, currentBackgroundSave, currentForegroundSave, useDx) | |
local x2, y2 = x, y | |
local charToDraw, subChar | |
for delta = -1, 1 do -- Margin of error to fix gaps on steep lines | |
if useDx then x2 = x + delta | |
else y2 = y + delta end | |
subChar = brailleHelper(subLineHelper, x2, y2, x1, y1, gradient) | |
if subChar ~= "⠀" then | |
charToDraw = setAdaptive(x2, y2, cfg, cbg, alpha, true, true, subChar) | |
currentForeground = color.blend(currentBackgroundSave, getRaw(x2, y2), alpha) | |
currentBackground = getRaw(x2, y2) | |
set(x2, y2, charToDraw) | |
end | |
end | |
end | |
local function subLineGtHalf(x, y, cfg, cbg, alpha, x1, y1, gradient, currentBackgroundSave, currentForegroundSave) | |
subLineSlopeHelperFunc(x, y, cfg, cbg, alpha, x1, y1, gradient, currentBackgroundSave, currentForegroundSave, true) | |
end | |
local function subLineLtHalf(x, y, cfg, cbg, alpha, x1, y1, gradient, currentBackgroundSave, currentForegroundSave) | |
subLineSlopeHelperFunc(x, y, cfg, cbg, alpha, x1, y1, gradient, currentBackgroundSave, currentForegroundSave) | |
end | |
-- Draw a line (Using braille characters) | |
-- from 1 point to another, optionally with alpha | |
local function drawBrailleLine(x1, y1, x2, y2, alpha) | |
x1, y1, x2, y2 = floor(2 * x1) / 2, floor(4 * y1) / 4, floor(2 * x2) / 2, floor(4 * y2) / 4 | |
local charToDraw | |
if y1 == y2 then | |
charToDraw = "⠉" | |
if y1 - floor(y1) ~= 0 then charToDraw = "⠒" end | |
elseif x1 == x2 then | |
charToDraw = "⡇" | |
if x1 - floor(x1) ~= 0 then charToDraw = "⢸" end | |
end | |
local function horzVertFunc(x, y, cfg, cbg, alpha, _1, _2, _3, currentBackgroundSave, currentForegroundSave) | |
genericSubLineFunc(x, y, cfg, cbg, alpha, charToDraw, currentBackgroundSave, currentForegroundSave) | |
end | |
return lineHelper(x1, y1, x2, y2, alpha, horzVertFunc, horzVertFunc, subLineGtHalf, subLineLtHalf) | |
end | |
----------------------------------------------------------- | |
-- Buffer functions | |
local function getCurrentBuffer() | |
return bufferBackground, bufferForeground, bufferSymbol | |
end | |
local function getChangeBuffer() | |
return changeBackgrounds, changeForegrounds, changeSymbols | |
end | |
local function rawGet(index) | |
checkArg(1, index, "number") | |
return bufferBackground[index], bufferForeground[index], bufferSymbol[index] | |
end | |
local function rawSet(index, background, foreground, symbol) | |
checkMultiArg("number", index, background, foreground) | |
checkArg(4, symbol, "string") | |
changeBackgrounds[index], changeForegrounds[index], changeSymbols[index] = background, foreground, symbol | |
end | |
-- Clear the screen by filling with colored whitespace chars -- | |
local function clear(color, alpha) | |
checkArg(1, color, "number", "nil") | |
checkArg(2, alpha, "number", "nil") | |
setBackground(color or 0x0) | |
drawRectangle(1, 1, bufferWidth, bufferHeight, alpha, " ") | |
updateBoundX1, updateBoundX2 = 1, bufferWidth | |
updateBoundY1, updateBoundY2 = 1, bufferHeight | |
end | |
-- Set GPU Proxy for the screen -- | |
local function setGPUProxy(gpu) | |
-- Define local variables | |
gpuProxy = gpu | |
GPUfill, GPUset, GPUcopy = gpu.fill, gpu.set, gpu.copy | |
GPUsetBackground, GPUsetForeground = gpu.setBackground, gpu.setForeground | |
GPUgetBackground, GPUgetForeground = gpu.getBackground, gpu.getForeground | |
GPUgetResolution, GPUsetResolution = gpu.getResolution, gpu.setResolution | |
GPUgetPaletteColor = gpu.getPaletteColor | |
GPUsetPaletteColor = gpu.setPaletteColor | |
-- Prefill buffer | |
flush() | |
end | |
-- Get GPU Proxy for the screen -- | |
local function getGPUProxy() | |
return gpuProxy | |
end | |
-- Bind the gpu proxy to a screen address -- | |
local function bind(screenAddress, reset) | |
local success, reason = gpuProxy.bind(address, reset) | |
if success then | |
if reset then setResolution(gpuProxy.maxResolution()) | |
else setResolution(bufferWidth, bufferHeight) end | |
end | |
return success, reason | |
end | |
-- Reset the palette to OpenOS defaults | |
local function resetPalette() | |
local n -- Temp variable | |
for i = 0, 15 do | |
n = 16 * i + (15 - i) | |
GPUsetPaletteColor(i, n + 256 * n + 65536 * n) | |
end | |
end | |
-- Set gpu proxy | |
setGPUProxy(gpuProxy) | |
return { | |
setGPUProxy = setGPUProxy, | |
getGPUProxy = getGPUProxy, | |
bind = bind, | |
flush = flush, | |
clear = clear, | |
setChar = setChar, | |
update = update, | |
getRaw = getRaw, | |
getIndex = getIndex, | |
getCoords = getCoords, | |
getCurrentBuffer = getCurrentBuffer, | |
getChangeBuffer = getChangeBuffer, | |
setDrawingBound = setDrawingBound, | |
getDrawingBound = getDrawingBound, | |
resetDrawingBound = resetDrawingBound, | |
resetPalette = resetPalette, | |
rawGet = rawGet, | |
rawSet = rawSet, | |
setBackground = setBackground, | |
setForeground = setForeground, | |
getBackground = getBackground, | |
getForeground = getForeground, | |
set = set, | |
copy = copy, | |
fill = fill, | |
setResolution = setResolution, | |
getResolution = getResolution, | |
getWidth = getWidth, | |
getHeight = getHeight, | |
drawRectangleOutline = drawRectangleOutline, | |
drawRectangle = drawRectangle, | |
drawThinRectangleOutline = drawThinRectangleOutline, | |
drawBrailleRectangle = drawBrailleRectangle, | |
drawBrailleRectangleOutline = drawBrailleRectangleOutline, | |
drawText = drawText, | |
drawLine = drawLine, | |
drawEllipseOutline = drawEllipseOutline, | |
drawEllipse = drawEllipse, | |
drawBrailleLine = drawBrailleLine, | |
drawBrailleEllipse = drawBrailleEllipse, | |
drawBrailleEllipseOutline = drawBrailleEllipseOutline | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment