Created
December 13, 2023 20:28
-
-
Save Glorp/21862cc44c397af5a50d2ec5a8c74808 to your computer and use it in GitHub Desktop.
This file contains 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
local function point(x, y) | |
return { x = x, y = y, key = x .. "," .. y } | |
end | |
local function points(lend) | |
local i = lend.idx | |
local off = 0 - lend.off | |
local other = lend.other.idx | |
local ended = false | |
local line = lend.line | |
return function() | |
if ended then return nil end | |
local currenti = i | |
i = i + off | |
local res = line[currenti] | |
if currenti == other then | |
ended = true | |
end | |
return currenti, res | |
end | |
end | |
local function samedir(a, b) | |
if a.x == b.x and a.y == b.y then | |
return true | |
elseif a.x == 0 and b.x == 0 then | |
return (a.y > 0 and b.y > 0) or (a.y < 0 and b.y < 0) | |
elseif a.y == 0 and b.y == 0 then | |
return (a.x > 0 and b.x > 0) or (a.x < 0 and b.x < 0) | |
else | |
return false | |
end | |
end | |
local function dir(from, to) | |
return point(to.x - from.x, to.y - from.y) | |
end | |
local function newlines() | |
local lines = {} | |
local extendible = {} | |
local function line(from, to) | |
local line = { from, to } | |
local first = { line = line, idx = 1, off = -1, dir = dir(to, from) } | |
local last = { line = line, idx = 2, off = 1, dir = dir(from, to) } | |
first.other = last | |
last.other = first | |
line["first"] = first | |
line["last"] = last | |
table.insert(lines, line) | |
extendible[from.key] = first | |
extendible[to.key] = last | |
return line | |
end | |
local function extend(lend, point) | |
local line = lend.line | |
local i = lend.idx | |
local prev = line[i] | |
extendible[prev.key] = nil | |
extendible[point.key] = lend | |
local newdir = dir(prev, point) | |
if not samedir(newdir, lend.dir) then | |
i = i + lend.off | |
end | |
line[i] = point | |
lend.idx = i | |
lend.dir = newdir | |
return line | |
end | |
local function unstend(lend) | |
local p = lend.line[lend.idx] | |
extendible[p.key] = nil | |
end | |
local function join(first, last) | |
local fline = first.line | |
local lline = last.line | |
if first == last then | |
error("oh no") | |
elseif fline == lline then | |
fline.closed = true | |
unstend(first) | |
unstend(last) | |
return fline | |
else | |
local flen = math.abs(first.idx - first.other.idx) | |
local llen = math.abs(last.idx - last.other.idx) | |
local keep, remove | |
if flen >= llen then keep, remove = first, last | |
else keep, remove = last, first end | |
local rline = remove.line | |
unstend(rline.first) | |
unstend(rline.last) | |
for i, p in (points(remove)) do | |
extend(keep, p) | |
rline[i] = nil | |
end | |
rline.first = nil | |
rline.last = nil | |
rline.nothing = true | |
return keep.line | |
end | |
end | |
lines.add = function(fromx, fromy, tox, toy) | |
local from, to = point(fromx, fromy), point(tox, toy) | |
local first = extendible[from.key] | |
local last = extendible[to.key] | |
if first and last then | |
return join(first, last) | |
elseif first then | |
return extend(first, to) | |
elseif last then | |
return extend(last, from) | |
else | |
return line(from, to) | |
end | |
end | |
return lines | |
end | |
local space, horiz, vert, bottom, top, lowhoriz = 32, 45, 124, 46, 39, 95 | |
local fslash, bslash = 47, 92 | |
local plus = 43 | |
local arrl, arrr, arru, arrd = 60, 62, 94, 86 | |
local function set(...) | |
local res = {} | |
for _, v in ipairs({...}) do res[v] = true end | |
return res | |
end | |
local verts = set(vert, plus, bottom) | |
local vertsu = set(vert, plus, bottom, arru) | |
local vertsd = set(vert, plus, top, arrd) | |
local horis = set(horiz, plus, top, bottom) | |
local horisl = set(horiz, plus, top, bottom, arrl) | |
local horisr = set(horiz, plus, top, bottom, arrr) | |
function newmap() | |
local map = { w = 0, h = 0 } | |
map.addline = function(str) | |
str = str or "" | |
local row = {} | |
local x = 0 | |
local y = map.h + 1 | |
map.h = y | |
for _, code in utf8.codes(str) do | |
x = x + 1 | |
if code ~= space then | |
local key = x .. "," .. y | |
local value = point(x, y) | |
value.code = code | |
map[key] = value | |
table.insert(map, value) | |
end | |
end | |
map.w = math.max(map.w, x ) | |
end | |
map.at = function(x, y) | |
local res = map[x .. "," .. y] | |
return res and res.code | |
end | |
return map | |
end | |
local function escapeHtmlChar(c) | |
if c == "<" then return "<" | |
elseif c == ">" then return ">" | |
elseif c == '"' then return """ | |
elseif c == "'" then return "'" | |
elseif c == "&" then return "&" | |
else return c | |
end | |
end | |
local function escapeHtml(str) | |
local res = (str or ""):gsub("[<>\"'&]", escapeHtmlChar) | |
return res | |
end | |
local function i(n) | |
local floored = math.floor(n) | |
return floored == n and floored or n | |
end | |
local function render(map, out, size) | |
size = size or 16 | |
local cellw, cellh = i(size * 0.55), i(size) | |
local xscale, yscale = i(cellw / 4), i(cellh / 8) | |
local res = nil | |
local lines = newlines() | |
local texts = {} | |
local text = nil | |
local function newtext(c, p) | |
text = { c } | |
text.first = p | |
text.last = p | |
table.insert(texts, text) | |
return text | |
end | |
local function addchar(c, p) | |
if text and text.last.y == p.y then | |
if text.last.x == p.x - 1 then | |
table.insert(text, c) | |
text.last = p | |
return | |
elseif text.last.x == p.x - 2 then | |
table.insert(text, space) | |
table.insert(text, c) | |
text.last = p | |
return | |
end | |
end | |
newtext(c, p) | |
end | |
for _, p in ipairs(map) do | |
local x = p.x | |
local y = p.y | |
local code = p.code | |
local bx = x * 4 | |
local by = y * 8 | |
if code == horiz then | |
lines.add(bx, by + 4, bx + 4, by + 4) | |
elseif code == vert then | |
lines.add(bx + 2, by, bx + 2, by + 8) | |
elseif code == bottom then | |
local px, py = bx + 2, by + 6 | |
local l, r = map.at(x - 1, y), map.at(x + 1, y) | |
local d = map.at(x, y + 1) | |
local dl, dr = map.at(x - 1, y + 1), map.at(x + 1, y + 1) | |
if horisl[l] then | |
lines.add(bx, by + 4, px, py) | |
end | |
if dl == fslash or l == lowhoriz then | |
lines.add(bx, by + 8, px, py) | |
end | |
if vertsd[d] then | |
lines.add(bx + 2, by + 8, px, py) | |
end | |
if dr == bslash or r == lowhoriz then | |
lines.add(px, py, bx + 4, by + 8) | |
end | |
if horisr[r] then | |
lines.add(px, py, bx + 4, by + 4) | |
end | |
elseif code == top then | |
local px, py = bx + 2, by + 2 | |
local l, r = map.at(x - 1, y), map.at(x + 1, y) | |
local u = map.at(x, y - 1) | |
local ul, ur = map.at(x - 1, y - 1), map.at(x + 1, y - 1) | |
if horisl[l] then | |
lines.add(bx, by + 4, px, py) | |
end | |
if ul == bslash then | |
lines.add(bx, by, px, py) | |
end | |
if vertsu[u] then | |
lines.add(bx + 2, by, px, py) | |
end | |
if ur == fslash then | |
lines.add(px, py, bx + 4, by) | |
end | |
if horisr[r] then | |
lines.add(px, py, bx + 4, by + 4) | |
end | |
elseif code == lowhoriz then | |
lines.add(bx, by + 8, bx + 4, by + 8) | |
elseif code == fslash then | |
lines.add(bx, by + 8, bx + 4, by) | |
elseif code == bslash then | |
lines.add(bx, by, bx + 4, by + 8) | |
elseif code == plus then | |
local px, py = bx + 2, by + 4 | |
local l, r = map.at(x - 1, y), map.at(x + 1, y) | |
local u, d = map.at(x, y - 1), map.at(x, y + 1) | |
if horisl[l] then | |
lines.add(bx, by + 4, px, py) | |
end | |
if vertsd[d] then | |
lines.add(bx + 2, by + 8, px, py) | |
end | |
if horisr[r] then | |
lines.add(px, py, bx + 4, by + 4) | |
end | |
if vertsu[u] then | |
lines.add(bx + 2, by, px, py) | |
end | |
elseif code == arrl then | |
local r = map.at(x + 1, y) | |
if horis[r] then | |
lines.add(bx + 4, by + 4, bx, by + 4) | |
lines.add(bx, by + 4, bx + 4, by + 2) | |
lines.add(bx, by + 4, bx + 4, by + 6) | |
else | |
addchar(code, p) | |
end | |
elseif code == arrr then | |
local l = map.at(x - 1, y) | |
if horis[l] then | |
lines.add(bx, by + 4, bx + 4, by + 4) | |
lines.add(bx + 4, by + 4, bx, by + 2) | |
lines.add(bx + 4, by + 4, bx, by + 6) | |
else | |
addchar(code, p) | |
end | |
elseif code == arru then | |
local d = map.at(x, y + 1) | |
if verts[d] then | |
lines.add(bx + 2, by + 8, bx + 2, by) | |
lines.add(bx + 2, by, bx, by + 4) | |
lines.add(bx + 2, by, bx + 4, by + 4) | |
else | |
addchar(code, p) | |
end | |
elseif code == arrd then | |
local u = map.at(x, y - 1) | |
if verts[u] then | |
lines.add(bx + 2, by, bx + 2, by + 8) | |
lines.add(bx + 2, by + 8, bx, by + 4) | |
lines.add(bx + 2, by + 8, bx + 4, by + 4) | |
else | |
addchar(code, p) | |
end | |
elseif code then | |
addchar(code, p) | |
end | |
end | |
local style = [[<style>svg { stroke: currentColor; fill: none; } ]] | |
style = style .. [[text { stroke: none; fill: currentColor; ]] | |
style = style .. [[font-family: monospace; ]] | |
style = style .. [[font-size: ]] .. size .. [[px; ]] | |
style = style .. [[dominant-baseline: hanging; text-anchor: start; }</style>]] | |
local function linesvg(line) | |
if line.nothing then | |
return "" | |
end | |
local strs = {} | |
for i, p in points(line.first) do | |
table.insert(strs, p.x * xscale .. "," .. p.y * yscale) | |
end | |
local type = line.closed and '<polygon points="' or '<polyline points="' | |
return type .. table.concat(strs, " ") .. '" />' | |
end | |
local w, h = ((map.w + 2) * cellw), ((map.h + 2) * cellh) | |
out('<svg width="' .. w .. '" height="' .. h .. '" viewBox="0 0 ' .. w .. ' ' .. h .. '" xmlns="http://www.w3.org/2000/svg">') | |
out(style) | |
for k, v in ipairs(lines) do | |
out(linesvg(v)) | |
end | |
for k, v in ipairs(texts) do | |
local x = v.first.x * 4 * xscale | |
local y = v.first.y * 8 * yscale | |
local str = escapeHtml(utf8.char(table.unpack(v))) | |
out('<text x="' .. x ..'" y="' .. y .. '">' .. str .. '</text>') | |
end | |
out("</svg>") | |
if res then return table.concat(res) end | |
end | |
function svg(str, size) | |
local map = newmap() | |
for line in str:gmatch("[^\n]*") do | |
map.addline(line) | |
end | |
local res = {} | |
render(map, function(str) table.insert(res, str) end, size) | |
return table.concat(res) | |
end | |
return { | |
newmap = newmap, | |
render = render, | |
svg = svg | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment