Skip to content

Instantly share code, notes, and snippets.

@lecram
Last active August 29, 2015 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lecram/d6d040ac7bc57a7ff8f8 to your computer and use it in GitHub Desktop.
Save lecram/d6d040ac7bc57a7ff8f8 to your computer and use it in GitHub Desktop.
AltScript Prototype
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())
# 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
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
# 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