ldd-CC/pain2.lua

842 lines
19 KiB
Lua

--[[
8888888b. d8888 8888888 888b 888 .d8888b.
888 Y88b d88888 888 8888b 888 d88P Y88b
888 888 d88P888 888 88888b 888 888
888 d88P d88P 888 888 888Y88b 888 .d88P
8888888P' d88P 888 888 888 Y88b888 .od888P"
888 d88P 888 888 888 Y88888 d88P"
888 d8888888888 888 888 Y8888 888"
888 d88P 888 8888888 888 Y888 888888888
Download with:
wget https://github.com/LDDestroier/CC/raw/master/pain2.lua
To-do:
* Add more tools, such as Fill or Color Picker.
* Add an actual menu.
* Add a help screen, and don't make it as bland-looking as PAIN 1's.
* Add support for every possible image format under the sun.
* Add the ability to add/remove layers.
--]]
local pain = {
running = true, -- if true, will run. otherwise, quit
layer = 1, -- current layer selected
image = {}, -- table of 2D canvases
manip = {}, -- basic canvas manipulation functions
timers = {}, -- built-in timer system
windows = {}, -- various windows drawn to the screen
}
keys.ctrl = 256
keys.alt = 257
keys.shift = 258
local keysDown = {}
local miceDown = {}
pain.color = {
char = " ",
text = "f",
back = "0"
}
pain.controlHoldCheck = {} -- used to check if an input has just been used or not
pain.control = {
quit = {
key = keys.q,
holdDown = false,
modifiers = {
[keys.leftCtrl] = true
},
},
scrollUp = {
key = keys.up,
holdDown = true,
modifiers = {},
},
scrollDown = {
key = keys.down,
holdDown = true,
modifiers = {},
},
scrollLeft = {
key = keys.left,
holdDown = true,
modifiers = {},
},
scrollRight = {
key = keys.right,
holdDown = true,
modifiers = {},
},
singleScroll = {
key = keys.tab,
holdDown = true,
modifiers = {},
},
resetScroll = {
key = keys.a,
holdDown = false,
modifiers = {},
},
cancelTool = {
key = keys.space,
holdDown = false,
modifiers = {},
},
nextTextColor = {
key = keys.rightBracket,
holdDown = false,
modifiers = {
[keys.shift] = true
},
},
prevTextColor = {
key = keys.leftBracket,
holdDown = false,
modifiers = {
[keys.shift] = true
},
},
nextBackColor = {
key = keys.rightBracket,
holdDown = false,
modifiers = {},
},
prevBackColor = {
key = keys.leftBracket,
holdDown = false,
modifiers = {},
},
shiftDotsRight = {
key = keys.right,
holdDown = false,
modifiers = {
[keys.shift] = true
}
},
shiftDotsLeft = {
key = keys.left,
holdDown = false,
modifiers = {
[keys.shift] = true
}
},
shiftDotsUp = {
key = keys.up,
holdDown = false,
modifiers = {
[keys.shift] = true
}
},
shiftDotsDown = {
key = keys.down,
holdDown = false,
modifiers = {
[keys.shift] = true
}
},
toggleLayerMenu = {
key = keys.l,
holdDown = false,
modifiers = {}
}
}
local checkControl = function(name, forceHoldDown)
local modlist = {
keys.ctrl,
keys.shift,
keys.alt,
}
for i = 1, #modlist do
if pain.control[name].modifiers[modlist[i]] then
if not keysDown[modlist[i]] then
return false
end
else
if keysDown[modlist[i]] then
return false
end
end
end
if pain.control[name].key then
if keysDown[pain.control[name].key] then
local holdDown = pain.control[name].holdDown
if forceHoldDown ~= nil then
holdDown = forceHoldDown
end
if holdDown then
return true
else
if not pain.controlHoldCheck[name] then
pain.controlHoldCheck[name] = true
return true
end
end
else
pain.controlHoldCheck[name] = false
return false
end
end
end
-- stores the native color palettes, in case the current iteration of ComputerCraft doesn't come with term.nativePaletteColor
-- if you're using ATOM, feel free to minimize this whole table
pain.nativePalette = {
[ 1 ] = {
0.94117647409439,
0.94117647409439,
0.94117647409439,
},
[ 2 ] = {
0.94901961088181,
0.69803923368454,
0.20000000298023,
},
[ 4 ] = {
0.89803922176361,
0.49803921580315,
0.84705883264542,
},
[ 8 ] = {
0.60000002384186,
0.69803923368454,
0.94901961088181,
},
[ 16 ] = {
0.87058824300766,
0.87058824300766,
0.42352941632271,
},
[ 32 ] = {
0.49803921580315,
0.80000001192093,
0.098039217293262,
},
[ 64 ] = {
0.94901961088181,
0.69803923368454,
0.80000001192093,
},
[ 128 ] = {
0.29803922772408,
0.29803922772408,
0.29803922772408,
},
[ 256 ] = {
0.60000002384186,
0.60000002384186,
0.60000002384186,
},
[ 512 ] = {
0.29803922772408,
0.60000002384186,
0.69803923368454,
},
[ 1024 ] = {
0.69803923368454,
0.40000000596046,
0.89803922176361,
},
[ 2048 ] = {
0.20000000298023,
0.40000000596046,
0.80000001192093,
},
[ 4096 ] = {
0.49803921580315,
0.40000000596046,
0.29803922772408,
},
[ 8192 ] = {
0.34117648005486,
0.65098041296005,
0.30588236451149,
},
[ 16384 ] = {
0.80000001192093,
0.29803922772408,
0.29803922772408,
},
[ 32768 ] = {
0.066666670143604,
0.066666670143604,
0.066666670143604,
}
}
local hexColors = "0123456789abcdef"
-- load Windon't API
-- if you're using ATOM, feel free to minimize this whole function
local windont = require "windont"
windont.default.alwaysRender = false
local scr_x, scr_y = term.getSize()
pain.windows.toolPreview = windont.newWindow(1, 1, scr_x, scr_y, {textColor = "-", backColor = "-"})
pain.windows.mainMenu = windont.newWindow(1, 1, scr_x, scr_y, {textColor = "-", backColor = "-"})
pain.windows.layerMenu = windont.newWindow(scr_x - 20, 1, 20, scr_y, {textColor = "-", backColor = "-"})
pain.windows.smallPreview = windont.newWindow(1, 1, scr_x, scr_y, {textColor = "-", backColor = "-"})
pain.windows.grid = windont.newWindow(1, 1, scr_x, scr_y, {textColor = "-", backColor = "-"})
local function tableCopy(tbl)
local output = {}
for k, v in next, tbl do
output[k] = type(v) == "table" and tableCopy(v) or v
end
return output
end
pain.startTimer = function(name, duration)
if type(duration) ~= "number" then
error("duration must be number")
elseif type(name) ~= "string" then
error("name must be string")
else
pain.timers[name] = duration
end
end
pain.cancelTimer = function(name)
if type(name) ~= "string" then
error("name must be string")
else
pain.timers[name] = nil
end
end
pain.tickTimers = function()
local done = {}
for k,v in next, pain.timers do
pain.timers[k] = v - 1
if pain.timers[k] <= 0 then
done[k] = true
end
end
for k,v in next, done do
pain.timers[k] = nil
end
return done
end
-- a 'canvas' refers to a single layer only
-- canvases are also windon't objects, like terminals
-- stolen from the paintutils API...nwehehehe
local getDotsInLine = function( startX, startY, endX, endY )
local out = {}
startX = math.floor(startX)
startY = math.floor(startY)
endX = math.floor(endX)
endY = math.floor(endY)
if startX == endX and startY == endY then
out = {{startX, startY}}
return out
end
local minX = math.min( startX, endX )
if minX == startX then
minY = startY
maxX = endX
maxY = endY
else
minY = endY
maxX = startX
maxY = startY
end
local xDiff = maxX - minX
local yDiff = maxY - minY
if xDiff > math.abs(yDiff) then
local y = minY
local dy = yDiff / xDiff
for x=minX,maxX do
out[#out+1] = {x, math.floor(y+0.5)}
y = y + dy
end
else
local x = minX
local dx = xDiff / yDiff
if maxY >= minY then
for y=minY,maxY do
out[#out+1] = {math.floor(x+0.5), y}
x = x + dx
end
else
for y=minY,maxY,-1 do
out[#out+1] = {math.floor(x+0.5), y}
x = x - dx
end
end
end
return out
end
pain.manip.touchDot = function(canvas, x, y)
for c = 1, 3 do
canvas.meta.buffer[c][y] = canvas.meta.buffer[c][y] or {}
for xx = 1, x do
canvas.meta.buffer[c][y][xx] = canvas.meta.buffer[c][y][xx] or "-"
end
end
return true
end
pain.manip.setDot = function(canvas, x, y, char, text, back)
if pain.manip.touchDot(canvas, x, y) then
canvas.meta.buffer[1][y][x] = char
canvas.meta.buffer[2][y][x] = text
canvas.meta.buffer[3][y][x] = back
end
end
pain.manip.setDotLine = function(canvas, x1, y1, x2, y2, char, text, back)
local dots = getDotsInLine(x1, y1, x2, y2)
for i = 1, #dots do
pain.manip.setDot(canvas, dots[i][1], dots[i][2], char, text, back)
end
end
pain.manip.changePainColor = function(mode, amount, doLoop)
local cNum = hexColors:find(pain.color[mode])
local sNum
if doLoop then
sNum = ((cNum + amount - 1) % 16) + 1
else
sNum = math.min(math.max(cNum + amount, 1), 16)
end
pain.color[mode] = hexColors:sub(sNum, sNum)
end
pain.manip.shiftDots = function(canvas, xDist, yDist)
local output = {{}, {}, {}}
for c = 1, 3 do
for y,vy in next, canvas.meta.buffer[c] do
output[c][y + yDist] = {}
for x,vx in next, vy do
output[c][y + yDist][x + xDist] = vx
end
end
end
canvas.meta.buffer = output
end
local whitespace = {
["\009"] = true,
["\010"] = true,
["\013"] = true,
["\032"] = true,
["\128"] = true
}
-- checks if a char/text/back combination should be considered "transparent"
pain.checkTransparent = function(char, text, back)
if whitespace[char] then
return (not back) or (back == "-")
else
return ((not back) or (back == "-")) and ((not text) or (text == "-") )
end
end
-- checks if a certain x,y position on the canvas exists
pain.checkDot = function(canvas, x, y)
if paint.manip.touchDot(canvas, x, y) then
if canvas[1][y][x] then
return canvas[1][y][x], canvas[2][y][x], canvas[3][y][x]
end
end
end
local tools = {}
tools.pencil = {
run = function(canvas, initEvent, toolInfo)
local mx, my, evt = initEvent[3], initEvent[4]
local oldX, oldY
local mode = initEvent[2] -- 1 = draw, 2 = erase
if keysDown[keys.shift] then
return tools.line.run(canvas, initEvent, toolInfo)
else
local setDot = function()
pain.manip.setDotLine(
canvas,
oldX or (mx - (canvas.meta.x - 1)),
oldY or (my - (canvas.meta.y - 1)),
mx - (canvas.meta.x - 1),
my - (canvas.meta.y - 1),
mode == 1 and pain.color.char or nil, -- " ",
mode == 1 and pain.color.text or nil, -- "-",
mode == 1 and pain.color.back or nil -- "-"
)
end
while miceDown[mode] do
evt = {os.pullEvent()}
if evt[1] == "mouse_click" or evt[1] == "mouse_drag" then
oldX, oldY = mx - (canvas.meta.x - 1), my - (canvas.meta.y - 1)
mx, my = evt[3], evt[4]
setDot()
elseif evt[1] == "refresh" then
oldX, oldY = mx - (canvas.meta.x - 1), my - (canvas.meta.y - 1)
setDot()
end
end
end
end,
options = {}
}
tools.line = {
run = function(canvas, initEvent, toolInfo)
local mx, my, evt = initEvent[3], initEvent[4]
local initX, initY
local oldX, oldY
local mode = initEvent[2] -- 1 = draw, 2 = erase
local setDot = function(sCanvas)
if initX and initY then
pain.manip.setDotLine(
sCanvas,
initX,
initY,
mx - (canvas.meta.x - 1),
my - (canvas.meta.y - 1),
mode == 1 and pain.color.char or nil, --" ",
mode == 1 and pain.color.text or nil, -- "-",
mode == 1 and pain.color.back or nil -- "-"
)
end
end
toolInfo.showToolPreview = true
while miceDown[mode] do
evt = {os.pullEvent()}
if evt[1] == "mouse_click" or evt[1] == "mouse_drag" then
oldX, oldY = mx - (canvas.meta.x - 1), my - (canvas.meta.y - 1)
mx, my = evt[3], evt[4]
if not (initX and initY) then
initX = mx - (canvas.meta.x - 1)
initY = my - (canvas.meta.y - 1)
end
setDot(pain.windows.toolPreview)
elseif evt[1] == "mouse_up" then
setDot(canvas)
elseif evt[1] == "refresh" then
oldX, oldY = mx - (canvas.meta.x - 1), my - (canvas.meta.y - 1)
setDot(pain.windows.toolPreview)
end
end
end,
options = {}
}
local genPalette = function()
local palette = {}
for i = 0, 15 do
palette[2^i] = pain.nativePalettes[2^i]
end
return palette
end
local newCanvas = function()
local canvas = windont.newWindow(1, 1, 1, 1, {textColor = "-", backColor = "-"})
canvas.meta.x = 1
canvas.meta.y = 1
return canvas
end
local getGridFromPos = function(x, y, scrollX, scrollY)
local grid
if (x >= 0 and y >= 0) then
grid = {
"$$..%%..%%..%%..",
"$$..%%..%%..%%..",
"$$..%%..%%..%%..",
"..$$..%%..%%..$$",
"..$$..%%..%%..$$",
"..$$..%%..%%..$$",
"%%..$$..%%..$$..",
"%%..$$..%%..$$..",
"%%..$$..%%..$$..",
"..%%..$$..$$..%%",
"..%%..$$..$$..%%",
"..%%..$$..$$..%%",
"%%..%%..$$..%%..",
"%%..%%..$$..%%..",
"%%..%%..$$..%%..",
"..%%..$$..$$..%%",
"..%%..$$..$$..%%",
"..%%..$$..$$..%%",
"%%..$$..%%..$$..",
"%%..$$..%%..$$..",
"%%..$$..%%..$$..",
"..$$..%%..%%..$$",
"..$$..%%..%%..$$",
"..$$..%%..%%..$$",
}
else
if (x < 0 and y >= 0) then
-- too far to the left, but not too far up
grid = {
"GO#RIGHT#",
"#---\16####",
"##---\16###",
"###---\16##",
"####---\16#",
"###---\16##",
"##---\16###",
"#---\16####",
}
elseif (x >= 0 and y < 0) then
-- too far up, but not too far to the left
grid = {
"#GO##DOWN#",
"#|#######|",
"#||#####||",
"#\31||###||\31",
"##\31||#||\31#",
"###\31|||\31##",
"####\31|\31###",
"#####\31####",
"##########",
}
else
grid = {
"\\##\\",
"\\\\##",
"#\\\\#",
"##\\\\",
}
end
end
local xx = (x % #grid[1]) + 1
return grid[(y % #grid) + 1]:sub(xx, xx), "7", "f"
end
local drawGrid = function(canvas)
local xx
for y = 1, pain.windows.grid.meta.height do
for x = 1, pain.windows.grid.meta.width do
pain.windows.grid.meta.buffer[1][y][x], pain.windows.grid.meta.buffer[2][y][x], pain.windows.grid.meta.buffer[3][y][x] = getGridFromPos(x - canvas.meta.x, y - canvas.meta.y)
end
end
end
local copyCanvasBuffer = function(buffer, x1, y1, x2, y2)
local output = {{}, {}, {}}
for c = 1, 3 do
for y = y1, y2 do
output[c][y] = {}
if buffer[c][y] then
for x = x1, x2 do
output[c][y][x] = buffer[c][y][x]
end
end
end
end
return output
end
local main = function()
local render = function(canvasList)
drawGrid(canvasList[1])
local rList = {
-- pain.windows.mainMenu,
-- pain.windows.layerMenu,
-- pain.windows.smallPreview,
pain.windows.toolPreview,
}
for i = 1, #canvasList do
rList[#rList + 1] = canvasList[i]
end
rList[#rList + 1] = pain.windows.grid
windont.render(
{baseTerm = term.current()},
table.unpack(rList)
)
end
local canvas, evt
local tCompleted = {}
local mainTimer = os.startTimer(0.05)
local resumeTimer = os.startTimer(0.05)
pain.startTimer("render", 0.05)
-- initialize first layer
pain.image[1] = newCanvas()
local cTool = {
name = "pencil",
lastEvent = nil,
active = false,
coroutine = nil,
doRender = false, -- if true after resuming the coroutine, renders directly after resuming
showToolPreview = false -- if true, will render the tool preview INSTEAD of the current canvas
}
local resume = function(newEvent)
if cTool.coroutine then
if (cTool.lastEvent == (newEvent or evt[1])) or (not cTool.lastEvent) then
cTool.doQuickResume = false
if cTool.showToolPreview then
pain.windows.toolPreview.meta.buffer = copyCanvasBuffer(
canvas.meta.buffer,
-canvas.meta.x,
-canvas.meta.y,
-canvas.meta.x + scr_x + 1,
-canvas.meta.y + scr_y + 1
)
pain.windows.toolPreview.meta.x = canvas.meta.x
pain.windows.toolPreview.meta.y = canvas.meta.y
pain.windows.toolPreview.meta.width = canvas.meta.width
pain.windows.toolPreview.meta.height = canvas.meta.height
end
cTool.active, cTool.lastEvent = coroutine.resume(cTool.coroutine, table.unpack(newEvent or evt))
end
if checkControl("cancelTool") then
cTool.active = false
end
if (not cTool.active) or coroutine.status(cTool.coroutine) == "dead" then
cTool.active = false
end
if not cTool.active then
if type(cTool.lastEvent) == "string" then
if cTool.lastEvent:sub(1,4) == "ERR:" then
error(cTool.lastEvent:sub(5))
end
end
cTool.coroutine = nil
cTool.lastEvent = nil
cTool.showToolPreview = false
pain.windows.toolPreview.clear()
end
if cTool.doRender then
render({canvas})
cTool.doRender = false
end
end
end
while pain.running do
evt = {os.pullEvent()}
if evt[1] == "timer" and evt[2] == mainTimer then
mainTimer = os.startTimer(0.05)
tCompleted = pain.tickTimers() -- get list of completed pain timers
canvas = pain.image[pain.layer] -- 'canvas' is a term object, you smarmy cunt
for k,v in next, keysDown do keysDown[k] = v + 1 end
local singleScroll = checkControl("singleScroll")
if checkControl("quit") then -- why did I call myself a cunt
pain.running = false
end
if checkControl("scrollRight", not singleScroll) then
canvas.meta.x = canvas.meta.x - 1
end
if checkControl("scrollLeft", not singleScroll) then
canvas.meta.x = canvas.meta.x + 1
end
if checkControl("scrollDown", not singleScroll) then
canvas.meta.y = canvas.meta.y - 1
end
if checkControl("scrollUp", not singleScroll) then
canvas.meta.y = canvas.meta.y + 1
end
if checkControl("shiftDotsRight") then
pain.manip.shiftDots(canvas, 1, 0)
end
if checkControl("shiftDotsLeft") then
pain.manip.shiftDots(canvas, -1, 0)
end
if checkControl("shiftDotsUp") then
pain.manip.shiftDots(canvas, 0, -1)
end
if checkControl("shiftDotsDown") then
pain.manip.shiftDots(canvas, 0, 1)
end
if checkControl("resetScroll") then
canvas.meta.x = 1
canvas.meta.y = 1
end
if checkControl("nextTextColor") then
pain.manip.changePainColor("text", 1, false)
end
if checkControl("nextBackColor") then
pain.manip.changePainColor("back", 1, false)
end
if checkControl("prevTextColor") then
pain.manip.changePainColor("text", -1, false)
end
if checkControl("prevBackColor") then
pain.manip.changePainColor("back", -1, false)
end
resume({"refresh"})
if tCompleted.render then
pain.startTimer("render", 0.05)
render({cTool.showToolPreview and pain.windows.toolPreview or canvas})
end
else
if evt[1] == "term_resize" then
scr_x, scr_y = term.getSize()
elseif evt[1] == "key" or evt[1] == "key_up" then
if evt[1] == "key" then
if not evt[3] then
keysDown[evt[2]] = 0
end
elseif evt[1] == "key_up" then
keysDown[evt[2]] = nil
end
keysDown[keys.ctrl] = keysDown[keys.leftCtrl] or keysDown[keys.rightCtrl]
keysDown[keys.shift] = keysDown[keys.leftShift] or keysDown[keys.rightShift]
keysDown[keys.alt] = keysDown[keys.leftAlt] or keysDown[keys.rightAlt]
elseif evt[1] == "mouse_up" then
miceDown[evt[2]] = nil
elseif (evt[1] == "mouse_click" or evt[1] == "mouse_drag") then
miceDown[evt[2]] = {evt[3], evt[4]}
if evt[1] == "mouse_click" then
if not cTool.active then
cTool.coroutine = coroutine.create(function(...)
local result, message = pcall(tools[cTool.name].run, ...)
if not result then
error("ERR:" .. message, 2)
end
end)
cTool.active = coroutine.resume(cTool.coroutine, canvas, evt, cTool)
end
end
end
resume()
end
end
term.setCursorPos(1, scr_y)
term.clearLine()
end
main()