Last active
August 29, 2015 14:05
-
-
Save lecram/d6d040ac7bc57a7ff8f8 to your computer and use it in GitHub Desktop.
AltScript Prototype
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
local ffi = require "ffi" | |
ffi.cdef[[ | |
double hypot(double x, double y); | |
double copysign(double x, double y); | |
]] | |
local mathx = ffi.C | |
function round(x) | |
local i, f = math.modf(x + mathx.copysign(0.5, x)) | |
return i | |
end | |
function log(s) | |
io.stderr:write(s .. "\n") | |
end | |
function dptedge(x, y, x0, y0, x1, y1) | |
local dx = x1 - x0 | |
local dy = y1 - y0 | |
local s = (dx * (y - y0) + dy * (x0 - x)) / (dx * dx + dy * dy) | |
local qx = x + dy * s | |
local qy = y - dx * s | |
local between | |
if x0 ~= x1 then | |
between = (x0 <= qx and qx <= x1) or (x1 <= qx and qx <= x0) | |
else | |
between = (y0 <= qy and qy <= y1) or (y1 <= qy and qy <= y0) | |
end | |
local d | |
if between then | |
local dx, dy = qx - x, qy - y | |
d = dx*dx + dy*dy | |
else | |
local dx0, dy0 = x0 - x, y0 - y | |
local dx1, dy1 = x1 - x, y1 - y | |
d = math.min(dx0*dx0 + dy0*dy0, dx1*dx1 + dy1*dy1) | |
end | |
return math.sqrt(d) | |
end | |
function BBox(x0, y0, x1, y1) | |
return {x0 = x0, y0 = y0, x1 = x1, y1 = y1} | |
end | |
function bbalign(bb) | |
return { | |
x0 = math.floor(bb.x0), y0 = math.floor(bb.y0), | |
x1 = math.floor(bb.x1), y1 = math.floor(bb.y1) | |
} | |
end | |
function bbinter(bb1, bb2) | |
if bb1 == nil or bb2 == nil then | |
return nil | |
end | |
local x0 = math.max(bb1.x0, bb2.x0) | |
local y0 = math.max(bb1.y0, bb2.y0) | |
local x1 = math.min(bb1.x1, bb2.x1) | |
local y1 = math.min(bb1.y1, bb2.y1) | |
if x0 >= x1 or y0 >= y1 then | |
return nil | |
else | |
return BBox(x0, y0, x1, y1) | |
end | |
end | |
function bbunion(bb1, bb2) | |
if bb1 == nil then | |
return bb2 | |
elseif bb2 == nil then | |
return bb1 | |
end | |
local x0 = math.min(bb1.x0, bb2.x0) | |
local y0 = math.min(bb1.y0, bb2.y0) | |
local x1 = math.max(bb1.x1, bb2.x1) | |
local y1 = math.max(bb1.y1, bb2.y1) | |
return BBox(x0, y0, x1, y1) | |
end | |
function line(x0, y0, x1, y1) | |
local minx, maxx, miny, maxy | |
if x0 < x1 then | |
minx, maxx = x0, x1 | |
else | |
minx, maxx = x1, x0 | |
end | |
if y0 < y1 then | |
miny, maxy = y0, y1 | |
else | |
miny, maxy = y1, y0 | |
end | |
local bb = BBox(minx, miny, maxx, maxy) | |
local function func(x, y, w) | |
local d = dptedge(x, y, x0, y0, x1, y1) | |
return -d | |
end | |
return bb, func | |
end | |
function circle(cx, cy, r) | |
local bb = BBox(cx - r, cy - r, cx + r, cy + r) | |
local function func(x, y, w) | |
local d = mathx.hypot(x - cx, y - cy) | |
return r - d | |
end | |
return bb, func | |
end | |
local function scanrange(scan, n) | |
if scan == nil then return math.huge end | |
local i, j | |
for k, v in ipairs(scan) do | |
j = k | |
if v > n then break end | |
i = j | |
end | |
if j == nil then return math.huge end | |
if j == i then j = j + 1 end | |
local di = scan[i] == nil and math.huge or n - scan[i] | |
local dj = scan[j] == nil and math.huge or scan[j] - n | |
return math.min(di, dj) | |
end | |
function polygon(points) | |
local n = #points / 2 | |
local x, y = points[1], points[2] | |
local x0, y0, x1, y1 = x, y, x, y | |
for i = 1, n do | |
x, y = points[2*i-1], points[2*i] | |
if x < x0 then x0 = x | |
elseif x > x1 then x1 = x end | |
if y < y0 then y0 = y | |
elseif y > y1 then y1 = y end | |
end | |
x0, y0 = math.floor(x0), math.floor(y0) | |
x1, y1 = math.ceil(x1), math.ceil(y1) | |
if points[#points-1] ~= points[1] or points[#points] ~= points[2] then | |
push(points, points[1]) | |
push(points, points[2]) | |
n = n + 1 | |
end | |
local width = x1 - x0 | |
local height = y1 - y0 | |
local hscans, vscans = {}, {} | |
for y = 1, height+1 do push(hscans, {}) end | |
for x = 1, width+1 do push(vscans, {}) end | |
for i = 1, n-1 do | |
local xa, ya = points[2*i-1], points[2*i] | |
local xb, yb = points[2*(i+1)-1], points[2*(i+1)] | |
local hxa, hya, hxb, hyb | |
local vxa, vya, vxb, vyb | |
if xa < xb then | |
vxa, vya, vxb, vyb = xa, ya, xb, yb | |
else | |
vxa, vya, vxb, vyb = xb, yb, xa, ya | |
end | |
if ya < yb then | |
hxa, hya, hxb, hyb = xa, ya, xb, yb | |
else | |
hxa, hya, hxb, hyb = xb, yb, xa, ya | |
end | |
if ya == yb then | |
-- horizontal segment | |
local index = round(ya)-y0+1 | |
push(hscans[index], xa) | |
push(hscans[index], xb) | |
while vxa < vxb do | |
push(vscans[round(vxa)-x0+1], ya) | |
vxa = vxa + 1 | |
end | |
elseif xa == xb then | |
-- vertical segment | |
local index = round(xa)-x0+1 | |
push(vscans[index], ya) | |
push(vscans[index], yb) | |
while hya < hyb do | |
push(hscans[round(hya)-y0+1], xa) | |
hya = hya + 1 | |
end | |
else | |
local sx = hxa < hxb and 1 or -1 | |
local slope = math.abs((yb - ya) / (xb - xa)) | |
while hya < hyb do | |
push(hscans[round(hya)-y0+1], hxa) | |
hya = hya + 1 | |
hxa = hxa + sx / slope | |
end | |
local sy = vya < vyb and 1 or -1 | |
slope = 1 / slope | |
while vxa < vxb do | |
push(vscans[round(vxa)-x0+1], vya) | |
vxa = vxa + 1 | |
vya = vya + sy / slope | |
end | |
end | |
end | |
for y = 1, height+1 do table.sort(hscans[y]) end | |
for x = 1, width+1 do table.sort(vscans[x]) end | |
local function func(x, y, w) | |
local sgn = -1 | |
local xi, yi = x - x0, y - y0 | |
if x > x0 and x < x1 and y > y0 and y < y1 then | |
for i, v in ipairs(hscans[yi+1]) do | |
if v > x then break end | |
sgn = -sgn | |
end | |
end | |
local i = 0 | |
local mind = w*w | |
local mag = w / math.sqrt(2) | |
repeat | |
local d1 = i * i | |
local j = math.min( | |
scanrange(vscans[xi-i+1], y), | |
scanrange(vscans[xi+i+1], y), | |
scanrange(hscans[yi-i+1], x), | |
scanrange(hscans[yi+i+1], x) | |
) | |
local d2 = j * j | |
local d = d1 + d2 | |
if d < mind then | |
mind = d | |
mag = math.sqrt(d / 2) | |
end | |
i = i + 1 | |
until i > mag | |
return mathx.copysign(mag * math.sqrt(2), sgn) | |
end | |
return BBox(x0, y0, x1, y1), func | |
end | |
function move(bb, func, tx, ty) | |
local bb_ = BBox(bb.x0 + tx, bb.y0 + ty, bb.x1 + tx, bb.y1 + ty) | |
local function func_(x, y, w) | |
return func(x - tx, y - ty, w) | |
end | |
return bb_, func_ | |
end | |
function scalept(cx, cy, sx, sy, x, y) | |
return (x - cx) * sx + cx, (y - cy) * sy + cy | |
end | |
function scale(bb, func, cx, cy, sx, sy) | |
local x0, y0 = scalept(cx, cy, sx, sy, bb.x0, bb.y0) | |
local x1, y1 = scalept(cx, cy, sx, sy, bb.x1, bb.y1) | |
if x1 < x0 then x0, x1 = x1, x0 end | |
if y1 < y0 then y0, y1 = y1, y0 end | |
local bb_ = BBox(x0, y0, x1, y1) | |
local function func_(x, y, w) | |
local x_, y_ = scalept(cx, cy, 1/sx, 1/sy, x, y) | |
return func(x_, y_, w) | |
end | |
return bb_, func_ | |
end | |
function rotatept(cx, cy, a, x, y) | |
local c = math.sin(a) | |
local s = math.sin(a) | |
local dx, dy = x - cx, y - cy | |
return dx * c - dy * s + cx, dx * s + dy * c + cy | |
end | |
function rotate(bb, func, cx, cy, a) | |
a = math.rad(a) | |
local xtl, ytl = rotatept(cx, cy, a, bb.x0, bb.y0) | |
local xtr, ytr = rotatept(cx, cy, a, bb.x1, bb.y0) | |
local xbr, ybr = rotatept(cx, cy, a, bb.x1, bb.y1) | |
local xbl, ybl = rotatept(cx, cy, a, bb.x0, bb.y1) | |
local x0 = math.min(xtl, xtr, xbr, xbl) | |
local x1 = math.max(xtl, xtr, xbr, xbl) | |
local y0 = math.min(ytl, ytr, ybr, ybl) | |
local y1 = math.max(ytl, ytr, ybr, ybl) | |
local bb_ = BBox(x0, y0, x1, y1) | |
local function func_(x, y, w) | |
local x_, y_ = rotatept(cx, cy, -a, x, y) | |
return func(x_, y_, w) | |
end | |
return bb_, func_ | |
end | |
function inv(bb, func) | |
local bb_ = BBox(-math.huge, -math.huge, math.huge, math.huge) | |
local function func_(x, y, w) | |
return -func(x, y, w) | |
end | |
return bb_, func_ | |
end | |
function inter(bb1, func1, bb2, func2) | |
local bb = bbinter(bb1, bb2) | |
local function func(x, y, w) | |
return math.min(func1(x, y, w), func2(x, y, w)) | |
end | |
return bb, func | |
end | |
function union(bb1, func1, bb2, func2) | |
local bb = bbunion(bb1, bb2) | |
local function func(x, y, w) | |
return math.max(func1(x, y, w),func2(x, y, w)) | |
end | |
return bb, func | |
end | |
function freeze(state) | |
return { | |
width = state.width, | |
alpha = state.alpha, | |
fill = state.fill, | |
stroke = state.stroke | |
} | |
end | |
function push(stack, value) | |
table.insert(stack, value) | |
end | |
function pop(stack) | |
local value = stack[#stack] | |
table.remove(stack) | |
return value | |
end | |
function Action(bb, func, cmd, state) | |
return { | |
bb = bb, | |
func = func, | |
cmd = cmd, | |
state = state | |
} | |
end | |
function Color(r, g, b) | |
return {r = r, g = g, b = b} | |
end | |
function Canvas(width, height, bgcolor) | |
local canvas = {} | |
for i = 1, width*height do | |
push(canvas, bgcolor.r) | |
push(canvas, bgcolor.g) | |
push(canvas, bgcolor.b) | |
end | |
return canvas | |
end | |
function blend(canvas, i, fg, alpha) | |
local bg = Color(canvas[i], canvas[i+1], canvas[i+2]) | |
local beta = 1 - alpha | |
canvas[i] = math.floor(bg.r * beta + fg.r * alpha + 0.5) | |
canvas[i+1] = math.floor(bg.g * beta + fg.g * alpha + 0.5) | |
canvas[i+2] = math.floor(bg.b * beta + fg.b * alpha + 0.5) | |
end | |
function ppm(canvas, width, height, file) | |
file:write("P6\n") | |
file:write(width, " ", height, "\n") | |
file:write("255\n") | |
for i = 1, 3*width*height do | |
file:write(string.char(canvas[i])) | |
end | |
file:flush() | |
end | |
function update_bbox(state, action) | |
local x0, y0 = action.bb.x0, action.bb.y0 | |
local x1, y1 = action.bb.x1, action.bb.y1 | |
if action.cmd[2] then | |
local hw = action.state.width / 2 | |
x0, y0, x1, y1 = x0 - hw, y0 - hw, x1 + hw, y1 + hw | |
end | |
state.bbox = bbunion(state.bbox, BBox(x0, y0, x1, y1)) | |
end | |
function enqueue(state, stack, queue, token) | |
if token == string.match(token, "[%-%+]?[%d%.]+") then | |
push(stack, tonumber(token)) | |
elseif token == "[" then | |
push(stack, "[") | |
elseif token == "]" then | |
local array = {} | |
while stack[#stack] ~= "[" do | |
table.insert(array, 1, pop(stack)) | |
end | |
stack[#stack] = array | |
elseif token == "+" then | |
local b = pop(stack) | |
local a = pop(stack) | |
push(stack, a+b) | |
elseif token == "-" then | |
local b = pop(stack) | |
local a = pop(stack) | |
push(stack, a-b) | |
elseif token == "*" then | |
local b = pop(stack) | |
local a = pop(stack) | |
push(stack, a*b) | |
elseif token == "/" then | |
local b = pop(stack) | |
local a = pop(stack) | |
push(stack, a/b) | |
elseif token == "pop" then | |
pop(stack) | |
elseif token == "dup" then | |
push(stack, stack[#stack]) | |
elseif token == "exch" then | |
local top = pop(stack) | |
table.insert(stack, #stack, top) | |
elseif token == "setwidth" then | |
state.width = pop(stack) | |
elseif token == "setalpha" then | |
state.alpha = pop(stack) | |
elseif token == "setfill" then | |
local b = pop(stack) | |
local g = pop(stack) | |
local r = pop(stack) | |
state.fill = Color(r, g, b) | |
elseif token == "setstroke" then | |
local b = pop(stack) | |
local g = pop(stack) | |
local r = pop(stack) | |
state.stroke = Color(r, g, b) | |
elseif token == "line" then | |
local y1 = pop(stack) | |
local x1 = pop(stack) | |
local y0 = pop(stack) | |
local x0 = pop(stack) | |
local bb, func = line(x0, y0, x1, y1) | |
push(stack, {bb, func}) | |
elseif token == "circle" then | |
local r = pop(stack) | |
local cy = pop(stack) | |
local cx = pop(stack) | |
local bb, func = circle(cx, cy, r) | |
push(stack, {bb, func}) | |
elseif token == "polygon" then | |
local points = pop(stack) | |
local bb, func = polygon(points) | |
push(stack, {bb, func}) | |
elseif token == "move" then | |
local ty = pop(stack) | |
local tx = pop(stack) | |
local bb, func = unpack(pop(stack)) | |
bb, func = move(bb, func, tx, ty) | |
push(stack, {bb, func}) | |
elseif token == "scale" then | |
local sy = pop(stack) | |
local sx = pop(stack) | |
local cy = pop(stack) | |
local cx = pop(stack) | |
local bb, func = unpack(pop(stack)) | |
bb, func = scale(bb, func, cx, cy, sx, sy) | |
push(stack, {bb, func}) | |
elseif token == "rotate" then | |
local a = pop(stack) | |
local cy = pop(stack) | |
local cx = pop(stack) | |
local bb, func = unpack(pop(stack)) | |
bb, func = rotate(bb, func, cx, cy, a) | |
push(stack, {bb, func}) | |
elseif token == "inv" then | |
local bb, func = unpack(pop(stack)) | |
bb, func = inv(bb, func) | |
push(stack, {bb, func}) | |
elseif token == "inter" then | |
local bb2, func2 = unpack(pop(stack)) | |
local bb1, func1 = unpack(pop(stack)) | |
local bb, func = inter(bb1, func1, bb2, func2) | |
push(stack, {bb, func}) | |
elseif token == "union" then | |
local bb2, func2 = unpack(pop(stack)) | |
local bb1, func1 = unpack(pop(stack)) | |
local bb, func = union(bb1, func1, bb2, func2) | |
push(stack, {bb, func}) | |
elseif token == "bbox" then | |
local y1 = pop(stack) | |
local x1 = pop(stack) | |
local y0 = pop(stack) | |
local x0 = pop(stack) | |
push(stack, BBox(x0, y0, x1, y1)) | |
elseif token == "getbbox" then | |
stack[#stack] = stack[#stack][1] | |
elseif token == "getbboxall" then | |
push(stack, state.bbox) | |
elseif token == "left" then | |
stack[#stack] = stack[#stack].x0 | |
elseif token == "right" then | |
stack[#stack] = stack[#stack].x1 | |
elseif token == "top" then | |
stack[#stack] = stack[#stack].y0 | |
elseif token == "bottom" then | |
stack[#stack] = stack[#stack].y1 | |
elseif token == "center" then | |
local bb = pop(stack) | |
push(stack, (bb.x0 + bb.x1) / 2) | |
push(stack, (bb.y0 + bb.y1) / 2) | |
elseif token == "width" then | |
local bb = stack[#stack] | |
stack[#stack] = bb.x1 - bb.x0 | |
elseif token == "height" then | |
local bb = stack[#stack] | |
stack[#stack] = bb.y1 - bb.y0 | |
elseif token == "fill" then | |
local bb, func = unpack(pop(stack)) | |
local action = Action(bb, func, {true, false}, freeze(state)) | |
push(queue, action) | |
update_bbox(state, action) | |
elseif token == "strk" then | |
local bb, func = unpack(pop(stack)) | |
local action = Action(bb, func, {false, true}, freeze(state)) | |
push(queue, action) | |
update_bbox(state, action) | |
elseif token == "fillstrk" then | |
local bb, func = unpack(pop(stack)) | |
local action = Action(bb, func, {true, true}, freeze(state)) | |
push(queue, action) | |
update_bbox(state, action) | |
else | |
return "invalid token: " .. token | |
end | |
end | |
function dequeue(canvas, cvsbb, queue) | |
local height = cvsbb.y1 - cvsbb.y0 + 1 | |
local width = cvsbb.x1 - cvsbb.x0 + 1 | |
for _, action in ipairs(queue) do | |
local halfwidth = action.state.width / 2 | |
local bb = action.bb | |
if action.cmd[2] then | |
bb.x0 = bb.x0 - halfwidth | |
bb.y0 = bb.y0 - halfwidth | |
bb.x1 = bb.x1 + halfwidth | |
bb.y1 = bb.y1 + halfwidth | |
end | |
bb = bbalign(bb) | |
bb = bbinter(bb, cvsbb) | |
if bb ~= nil then | |
local i = 3 * width * (bb.y0 - cvsbb.y0) + 3 * (bb.x0 - cvsbb.x0) + 1 | |
local dx = bb.x1 - bb.x0 | |
local dy = bb.y1 - bb.y0 | |
local h = mathx.hypot(dx, dy) / 4 | |
for y = bb.y0, bb.y1 do | |
for x = bb.x0, bb.x1 do | |
local d = action.func(x, y, halfwidth + 0.5) | |
local pd = math.abs(d) | |
if action.cmd[1] and d > 0 then | |
local aa = d < 1 and d or 1 | |
blend(canvas, i, action.state.fill, aa * action.state.alpha) | |
end | |
if action.cmd[2] and pd < halfwidth + 0.5 then | |
local aa = halfwidth - pd + 0.5 | |
aa = aa > 1 and 1 or aa | |
blend(canvas, i, action.state.stroke, aa * action.state.alpha) | |
end | |
i = i + 3 | |
end | |
i = i + 3 * (width - dx - 1) | |
end | |
end | |
end | |
log(("succesfully rendered %dx%d image with %d objects.") | |
:format(width, height, #queue)) | |
end | |
function main(input, output) | |
local state = { | |
width = 1, alpha = 1, | |
fill = Color(128, 128, 128), stroke = Color(0, 0, 0) | |
} | |
local stack = {} | |
local queue = {} | |
for line in input:lines() do | |
for token in string.gmatch(line, "%S+") do | |
if token:sub(1, 1) == "#" then break end | |
local msg = enqueue(state, stack, queue, token) | |
if msg ~= nil then | |
log("error: " .. msg) | |
return | |
end | |
end | |
end | |
if #queue == 0 then | |
-- If there are no drawing commands, just print stack and exit. | |
for i, v in ipairs(stack) do | |
output:write(tostring(v) .. " ") | |
end | |
output:write("\n") | |
return | |
end | |
if #stack == 0 then | |
log("error: missing output bbox.") | |
return | |
elseif #stack > 1 then | |
log(("warning: %d item(s) left on stack."):format(#stack - 1)) | |
end | |
local frmbb = pop(stack) | |
frmbb = bbalign(frmbb) | |
local height = frmbb.y1 - frmbb.y0 + 1 | |
local width = frmbb.x1 - frmbb.x0 + 1 | |
local canvas = Canvas(width, height, Color(224, 224, 143)) | |
dequeue(canvas, frmbb, queue) | |
ppm(canvas, width, height, output) | |
end | |
main(io.input(), io.output()) |
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
# comments start with hash sign | |
# set stroke width to 30 pixels | |
30 setwidth | |
# draw line from (150, 150) to (650, 650) | |
150 150 650 650 line strk | |
# set alpha value; 0 means transparent, 1 means opaque | |
0.8 setalpha | |
# set RGB color for stroke (shape outlines) | |
20 20 255 setstroke | |
# set RGB color for fill (shape interiors) | |
220 220 0 setfill | |
# create a circle centered at (300, 400) with radius 200 | |
300 400 200 circle | |
# get the inversion of the circle (swap interior with exterior) | |
inv | |
# get intersection with another circle | |
450 400 200 circle | |
inter | |
# fill interior and stroke outline of resulting shape | |
fillstrk | |
# change colors and stroke width again | |
255 0 0 setstroke | |
0 255 0 setfill | |
10 setwidth | |
# draw some other circles | |
100 200 80 circle fill # only draw interior | |
100 400 80 circle strk # only draw outline | |
100 600 80 circle fillstrk # draw both interior and outline | |
# leave a bbox on the stack to specify the width and height of the final image | |
1 1 800 800 bbox | |
# render this file with the following command: | |
# $ luajit alt.lua < example.alt > example.ppm |
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
4 setwidth | |
# triangle | |
[ 20 80 50 20 80 80 ] polygon fillstrk | |
# square | |
[ 100 20 160 20 160 80 100 80 ] polygon fillstrk | |
# diamond | |
[ 180 50 210 20 240 50 210 80 ] polygon fillstrk | |
1 1 260 100 bbox |
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
# create a rounded square with four circles | |
400 400 200 circle | |
dup | |
-100 0 move | |
exch | |
dup | |
100 0 move | |
exch | |
dup | |
0 -100 move | |
exch | |
0 100 move | |
inter | |
inter | |
inter | |
5 setwidth | |
fillstrk | |
1 1 800 800 bbox |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment