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 = {}, }, resetScroll = { key = keys.a, holdDown = false, modifiers = {}, }, cancelTool = { key = keys.space, holdDown = false, modifiers = {}, } } local checkControl = function(name) 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 if pain.control[name].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, } } -- 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.menu = windont.newWindow(1, 1, scr_x, 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 pairs(pain.timers) do pain.timers[k] = v - 1 if pain.timers[k] <= 0 then done[k] = true end end for k,v in pairs(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) if false then if (x > canvas.meta.width or y > canvas.meta.height) and (x >= 1 and y >= 1) then canvas.meta.width = x canvas.meta.height = y canvas.meta.buffer = canvas.meta.newBuffer( x, y, " ", "-", "-", canvas.meta.buffer ) end return true else 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 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 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 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 " ", mode == 1 and pain.color.text or "-", mode == 1 and pain.color.back or "-" ) 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, 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 " ", mode == 1 and pain.color.text or "-", mode == 1 and pain.color.back or "-" ) 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 makeMenu = function() end local main = function() local render = function(canvasList) drawGrid(canvasList[1]) local rList = { pain.windows.menu, 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 = "line", 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 isToolGood = false 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 = tableCopy(canvas.meta.buffer) 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 pairs(keysDown) do keysDown[k] = v + 1 end if checkControl("quit") then -- why did I call myself a cunt pain.running = false end if checkControl("scrollRight") then canvas.meta.x = canvas.meta.x - 1 end if checkControl("scrollLeft") then canvas.meta.x = canvas.meta.x + 1 end if checkControl("scrollDown") then canvas.meta.y = canvas.meta.y - 1 end if checkControl("scrollUp") then canvas.meta.y = canvas.meta.y + 1 end if checkControl("resetScroll") then canvas.meta.x = 1 canvas.meta.y = 1 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" then if not evt[3] then keysDown[evt[2]] = 0 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] end elseif evt[1] == "mouse_up" then miceDown[evt[2]] = nil elseif evt[1] == "key_up" then keysDown[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()