-- pain2 local scr_x, scr_y = term.getSize() local mx, my = scr_x/2, scr_y/2 -- midpoint of screen local keysDown = {} -- list of all pushed keys local miceDown = {} -- list of all clicked mice buttons local miceQueue = {} -- queues all mouse events for use with tool engine local dragPoses = {{{},{}}, {{},{}}, {{},{}}} -- records initial and current mouse position per button while scrolling local TICKNO = 0 -- iterates every time main() loops local flashPaletteOnBar = 0 -- whether or not to flash the dot palette numbers on the bottom bar, 0 is false, greater than 0 is true -- debug renderer is slower, but the normal one isn't functional yet local useDebugRenderer = false local canvas = { {{},{},{}} } local render local pain = { scrollX = 0, -- x position of scroll scrollY = 0, -- y position of scroll frame = 1, dot = 1, brushSize = 2, -- size of brush for tools like brush or line barmsg = "Started PAIN.", -- message shown on the bottom bar for 'barlife' ticks barlife = 12, -- amount of time until barmsg will cease to render showBar = true, -- whether or not to show the bottom bar doRender = true, -- if true, will render and set doRender to false isInFocus = true, -- will not accept any non-mouse input while false exportMode = "nft", -- saving will use this format limitOneMouseButton = true, -- disallows using more than one mouse button at a time size = { x = 1, y = 1, width = scr_x, height = scr_y }, guideWidth = scr_x, guideHeight = scr_y, guideLeftAdj = 2, dots = { [0] = { " ", " ", " " }, [1] = { " ", "f", "0" }, [2] = { " ", "f", "a" }, [3] = { " ", "f", "b" }, [4] = { " ", "f", "c" }, [5] = { " ", "f", "d" }, [6] = { " ", "f", "2" }, [7] = { " ", "f", "3" }, [8] = { " ", "f", "4" }, [9] = { " ", "f", "5" }, }, tool = "pencil" } -- NFTE API START -- local nfte = {} local tchar = string.char(31) -- for text colors local bchar = string.char(30) -- for background colors local nchar = string.char(29) -- for differentiating multiple frames in ANFT -- every flippable block character that doesn't need a color swap local xflippable = { ["\129"] = "\130", ["\132"] = "\136", ["\133"] = "\138", ["\134"] = "\137", ["\137"] = "\134", ["\135"] = "\139", ["\140"] = "\140", ["\141"] = "\142", } -- every flippable block character that needs a color swap local xinvertable = { ["\144"] = "\159", ["\145"] = "\157", ["\146"] = "\158", ["\147"] = "\156", ["\148"] = "\151", ["\152"] = "\155", ["\149"] = "\149", ["\150"] = "\150", ["\153"] = "\153", ["\154"] = "\154" } for k,v in pairs(xflippable) do xflippable[v] = k end for k,v in pairs(xinvertable) do xinvertable[v] = k end local bl = { -- blit [' '] = 0, ['0'] = 1, ['1'] = 2, ['2'] = 4, ['3'] = 8, ['4'] = 16, ['5'] = 32, ['6'] = 64, ['7'] = 128, ['8'] = 256, ['9'] = 512, ['a'] = 1024, ['b'] = 2048, ['c'] = 4096, ['d'] = 8192, ['e'] = 16384, ['f'] = 32768, } local lb = {} -- tilb for k,v in pairs(bl) do lb[v] = k end local ldchart = { -- converts colors into a lighter shade ["0"] = "0", ["1"] = "4", ["2"] = "6", ["3"] = "0", ["4"] = "0", ["5"] = "0", ["6"] = "0", ["7"] = "8", ["8"] = "0", ["9"] = "3", ["a"] = "2", ["b"] = "9", ["c"] = "1", ["d"] = "5", ["e"] = "2", ["f"] = "7" } local dlchart = { -- converts colors into a darker shade ["0"] = "8", ["1"] = "c", ["2"] = "a", ["3"] = "9", ["4"] = "1", ["5"] = "d", ["6"] = "2", ["7"] = "f", ["8"] = "7", ["9"] = "b", ["a"] = "7", ["b"] = "7", ["c"] = "7", ["d"] = "7", ["e"] = "7", ["f"] = "f" } local round = function(num) return math.floor(num + 0.5) end local deepCopy deepCopy = function(tbl) local output = {} for k,v in pairs(tbl) do if type(v) == "table" then output[k] = deepCopy(v) else output[k] = v end end return output end local function stringWrite(str,pos,ins,exc) str, ins = tostring(str), tostring(ins) local output, fn1, fn2 = str:sub(1,pos-1)..ins..str:sub(pos+#ins) if exc then repeat fn1, fn2 = str:find(exc,fn2 and fn2+1 or 1) if fn1 then output = stringWrite(output,fn1,str:sub(fn1,fn2)) end until not fn1 end return output end local checkValid = function(image) if type(image) == "table" then if #image == 3 then return (#image[1] == #image[2] and #image[2] == #image[3]) end end return false end local checkIfANFT = function(image) if type(image) == "table" then return type(image[1][1]) == "table" elseif type(image) == "string" then return image:find(nchar) and true or false end end local getSizeNFP = function(image) local xsize = 0 if type(image) ~= "table" then return 0,0 end for y = 1, #image do xsize = math.max(xsize, #image[y]) end return xsize, #image end nfte.getSize = function(image) assert(checkValid(image), "Invalid image.") local x, y = 0, #image[1] for y = 1, #image[1] do x = math.max(x, #image[1][y]) end return x, y end nfte.crop = function(image, x1, y1, x2, y2) assert(checkValid(image), "Invalid image.") local output = {{},{},{}} for y = y1, y2 do output[1][#output[1]+1] = image[1][y]:sub(x1,x2) output[2][#output[2]+1] = image[2][y]:sub(x1,x2) output[3][#output[3]+1] = image[3][y]:sub(x1,x2) end return output end local loadImageDataNFT = function(image, background) -- string image local output = {{},{},{}} -- char, text, back local y = 1 background = (background or " "):sub(1,1) local text, back = " ", background local doSkip, c1, c2 = false local maxX = 0 local bx for i = 1, #image do if doSkip then doSkip = false else output[1][y] = output[1][y] or "" output[2][y] = output[2][y] or "" output[3][y] = output[3][y] or "" c1, c2 = image:sub(i,i), image:sub(i+1,i+1) if c1 == tchar then text = c2 doSkip = true elseif c1 == bchar then back = c2 doSkip = true elseif c1 == "\n" then maxX = math.max(maxX, #output[1][y]) y = y + 1 text, back = " ", background else output[1][y] = output[1][y]..c1 output[2][y] = output[2][y]..text output[3][y] = output[3][y]..back end end end for y = 1, #output[1] do output[1][y] = output[1][y] .. (" "):rep(maxX - #output[1][y]) output[2][y] = output[2][y] .. (" "):rep(maxX - #output[2][y]) output[3][y] = output[3][y] .. (background):rep(maxX - #output[3][y]) end return output end local loadImageDataNFP = function(image, background) local output = {} local x, y = 1, 1 for i = 1, #image do output[y] = output[y] or {} if bl[image:sub(i,i)] then output[y][x] = bl[image:sub(i,i)] x = x + 1 elseif image:sub(i,i) == "\n" then x, y = 1, y + 1 end end return output end nfte.convertFromNFP = function(image, background) background = background or " " local output = {{},{},{}} if type(image) == "string" then image = loadImageDataNFP(image) end local imageX, imageY = getSizeNFP(image) local bx for y = 1, imageY do output[1][y] = "" output[2][y] = "" output[3][y] = "" for x = 1, imageX do if image[y][x] then bx = (x % #background) + 1 output[1][y] = output[1][y]..lb[image[y][x] or background:sub(bx,bx)] output[2][y] = output[2][y]..lb[image[y][x] or background:sub(bx,bx)] output[3][y] = output[3][y]..lb[image[y][x] or background:sub(bx,bx)] end end end return output end nfte.loadImageData = function(image, background) assert(type(image) == "string", "NFT image data must be string.") local output = {} -- images can be ANFT, which means they have multiple layers if checkIfANFT(image) then local L, R = 1, 1 while L do R = (image:find(nchar, L + 1) or 0) output[#output+1] = loadImageDataNFT(image:sub(L, R - 1), background) L = image:find(nchar, R + 1) if L then L = L + 2 end end return output, "anft" elseif image:find(tchar) or image:find(bchar) then return loadImageDataNFT(image, background), "nft" else return convertFromNFP(image), "nfp" end end nfte.loadImage = function(path, background) local file = io.open(path, "r") if file then io.input(file) local output, format = loadImageData(io.read("*all"), background) io.close() return output, format else error("No such file exists, or is directory.") end end local unloadImageNFT = function(image) assert(checkValid(image), "Invalid image.") local output = "" local text, back = " ", " " local c, t, b for y = 1, #image[1] do for x = 1, #image[1][y] do c, t, b = image[1][y]:sub(x,x), image[2][y]:sub(x,x), image[3][y]:sub(x,x) if (t ~= text) or (x == 1) then output = output..tchar..t text = t end if (b ~= back) or (x == 1) then output = output..bchar..b back = b end output = output..c end if y ~= #image[1] then output = output.."\n" text, back = " ", " " end end return output end nfte.unloadImage = function(image) assert(checkValid(image), "Invalid image.") local output = "" if checkIfANFT(image) then for i = 1, #image do output = output .. unloadImageNFT(image[i]) if i ~= #image then output = output .. nchar .. "\n" end end else output = unloadImageNFT(image) end return output end nfte.drawImage = function(image, x, y, terminal) assert(checkValid(image), "Invalid image.") assert(type(x) == "number", "x value must be number, got " .. type(x)) assert(type(y) == "number", "y value must be number, got " .. type(y)) terminal = terminal or term.current() local cx, cy = terminal.getCursorPos() for iy = 1, #image[1] do terminal.setCursorPos(x, y + (iy - 1)) terminal.blit(image[1][iy], image[2][iy], image[3][iy]) end terminal.setCursorPos(cx,cy) end nfte.drawImageTransparent = function(image, x, y, terminal) assert(checkValid(image), "Invalid image.") assert(type(x) == "number", "x value must be number, got " .. type(x)) assert(type(y) == "number", "y value must be number, got " .. type(y)) terminal = terminal or term.current() local cx, cy = terminal.getCursorPos() local c, t, b for iy = 1, #image[1] do for ix = 1, #image[1][iy] do c, t, b = image[1][iy]:sub(ix,ix), image[2][iy]:sub(ix,ix), image[3][iy]:sub(ix,ix) if b ~= " " or c ~= " " then terminal.setCursorPos(x + (ix - 1), y + (iy - 1)) terminal.blit(c, t, b) end end end terminal.setCursorPos(cx,cy) end nfte.drawImageCenter = function(image, x, y, terminal) terminal = terminal or term.current() local scr_x, scr_y = terminal.getSize() local imageX, imageY = getSize(image) return drawImage( image, round(0.5 + (x and x or (scr_x/2)) - imageX/2), round(0.5 + (y and y or (scr_y/2)) - imageY/2), terminal ) end nfte.drawImageCenterTransparent = function(image, x, y, terminal) terminal = terminal or term.current() local scr_x, scr_y = terminal.getSize() local imageX, imageY = getSize(image) return drawImageTransparent( image, round(0.5 + (x and x or (scr_x/2)) - imageX/2), round(0.5 + (y and y or (scr_y/2)) - imageY/2), terminal ) end nfte.colorSwap = function(image, text, back) assert(checkValid(image), "Invalid image.") local output = {{},{},{}} for y = 1, #image[1] do output[1][y] = image[1][y] output[2][y] = image[2][y]:gsub(".", text) output[3][y] = image[3][y]:gsub(".", back or text) end return output end nfte.flipX = function(image) assert(checkValid(image), "Invalid image.") local output = {{},{},{}} for y = 1, #image[1] do output[1][y] = image[1][y]:gsub(".", xinvertable):gsub(".", xflippable):reverse() output[2][y] = "" output[3][y] = "" for x = 1, #image[1][y] do if xinvertable[image[1][y]:sub(x,x)] then output[2][y] = image[3][y]:sub(x,x) .. output[2][y] output[3][y] = image[2][y]:sub(x,x) .. output[3][y] else output[2][y] = image[2][y]:sub(x,x) .. output[2][y] output[3][y] = image[3][y]:sub(x,x) .. output[3][y] end end end return output end nfte.flipY = function(image) assert(checkValid(image), "Invalid image.") local output = {{},{},{}} for y = #image[1], 1, -1 do output[1][#output[1]+1] = image[1][y] output[2][#output[2]+1] = image[2][y] output[3][#output[3]+1] = image[3][y] end return output end nfte.makeRectangle = function(width, height, char, text, back) assert(type(width) == "number", "width must be number") assert(type(height) == "number", "height must be number") local output = {{},{},{}} for y = 1, height do output[1][y] = (char or " "):rep(width) output[2][y] = (text or " "):rep(width) output[3][y] = (back or " "):rep(width) end return output end nfte.grayOut = function(image) assert(checkValid(image), "Invalid image.") local output = {{},{},{}} local chart = { ["0"] = "0", ["1"] = "8", ["2"] = "8", ["3"] = "8", ["4"] = "8", ["5"] = "8", ["6"] = "8", ["7"] = "7", ["8"] = "8", ["9"] = "7", ["a"] = "7", ["b"] = "7", ["c"] = "7", ["d"] = "7", ["e"] = "7", ["f"] = "f" } for y = 1, #image[1] do output[1][y] = image[1][y] output[2][y] = image[2][y]:gsub(".", chart) output[3][y] = image[3][y]:gsub(".", chart) end return output end nfte.lighten = function(image, amount) assert(checkValid(image), "Invalid image.") if (amount or 1) < 0 then return nfte.darken(image, -amount) else local output = deepCopy(image) for i = 1, amount or 1 do for y = 1, #output[1] do output[1][y] = output[1][y] output[2][y] = output[2][y]:gsub(".",ldchart) output[3][y] = output[3][y]:gsub(".",ldchart) end end return output end end nfte.darken = function(image, amount) assert(checkValid(image), "Invalid image.") if (amount or 1) < 0 then return nfte.lighten(image, -amount) else local output = deepCopy(image) for i = 1, amount or 1 do for y = 1, #output[1] do output[1][y] = output[1][y] output[2][y] = output[2][y]:gsub(".",dlchart) output[3][y] = output[3][y]:gsub(".",dlchart) end end return output end end nfte.stretchImage = function(_image, sx, sy, noRepeat) assert(checkValid(_image), "Invalid image.") local output = {{},{},{}} local image = deepCopy(_image) if sx < 0 then image = flipX(image) end if sy < 0 then image = flipY(image) end sx, sy = math.abs(sx), math.abs(sy) local imageX, imageY = getSize(image) local tx, ty if sx == 0 or sy == 0 then for y = 1, math.max(sy, 1) do output[1][y] = "" output[2][y] = "" output[3][y] = "" end return output else for y = 1, sy do for x = 1, sx do tx = round((x / sx) * imageX) ty = math.ceil((y / sy) * imageY) if not noRepeat then output[1][y] = (output[1][y] or "")..image[1][ty]:sub(tx,tx) else output[1][y] = (output[1][y] or "").." " end output[2][y] = (output[2][y] or "")..image[2][ty]:sub(tx,tx) output[3][y] = (output[3][y] or "")..image[3][ty]:sub(tx,tx) end end if noRepeat then for y = 1, imageY do for x = 1, imageX do if image[1][y]:sub(x,x) ~= " " then tx = round(((x / imageX) * sx) - ((0.5 / imageX) * sx)) ty = round(((y / imageY) * sy) - ((0.5 / imageY) * sx)) output[1][ty] = stringWrite(output[1][ty], tx, image[1][y]:sub(x,x)) end end end end return output end end nfte.stretchImageKeepAspect = function(image, sx, sy, noRepeat) assert(checkValid(image), "Invalid image.") local imX, imY = nfte.getSize(image) local aspect = sx / sy local imAspect = imX / imY if imAspect > aspect then return nfte.stretchImage(image, sx, sx / imAspect, noRepeat) elseif imAspect < aspect then return nfte.stretchImage(image, sy * imAspect, sy, noRepeat) else return nfte.stretchImage(image, sx, sy, noRepeat) end end -- will stretch and unstretch an image to radically lower its resolution nfte.pixelateImage = function(image, amntX, amntY) assert(checkValid(image), "Invalid image.") local imageX, imageY = getSize(image) return stretchImage(stretchImage(image,imageX/math.max(amntX,1), imageY/math.max(amntY,1)), imageX, imageY) end nfte.merge = function(...) local images = {...} local output = {{},{},{}} local imageX, imageY = 0, 0 local imSX, imSY for i = 1, #images do imageY = math.max( imageY, #images[i][1][1] + (images[i][3] == true and 0 or (images[i][3] - 1)) ) for y = 1, #images[i][1][1] do imageX = math.max( imageX, #images[i][1][1][y] + (images[i][2] == true and 0 or (images[i][2] - 1)) ) end end -- if either coordinate is true, center it for i = 1, #images do imSX, imSY = getSize(images[i][1]) if images[i][2] == true then images[i][2] = round(1 + (imageX / 2) - (imSX / 2)) end if images[i][3] == true then images[i][3] = round(1 + (imageY / 2) - (imSY / 2)) end end -- will later add code to adjust X/Y positions if negative values are given local image, xadj, yadj local tx, ty for y = 1, imageY do output[1][y] = {} output[2][y] = {} output[3][y] = {} for x = 1, imageX do for i = #images, 1, -1 do image, xadj, yadj = images[i][1], images[i][2], images[i][3] tx, ty = x-(xadj-1), y-(yadj-1) output[1][y][x] = output[1][y][x] or " " output[2][y][x] = output[2][y][x] or " " output[3][y][x] = output[3][y][x] or " " if image[1][ty] then if (image[1][ty]:sub(tx,tx) ~= "") and (tx >= 1) then output[1][y][x] = (image[1][ty]:sub(tx,tx) == " " and output[1][y][x] or image[1][ty]:sub(tx,tx)) output[2][y][x] = (image[2][ty]:sub(tx,tx) == " " and output[2][y][x] or image[2][ty]:sub(tx,tx)) output[3][y][x] = (image[3][ty]:sub(tx,tx) == " " and output[3][y][x] or image[3][ty]:sub(tx,tx)) end end end end output[1][y] = table.concat(output[1][y]) output[2][y] = table.concat(output[2][y]) output[3][y] = table.concat(output[3][y]) end return output end local rotatePoint = function(x, y, angle, originX, originY) return round( (x-originX) * math.cos(angle) - (y-originY) * math.sin(angle) ) + originX, round( (x-originX) * math.sin(angle) + (y-originY) * math.cos(angle) ) + originY end nfte.rotateImage = function(image, angle, originX, originY) assert(checkValid(image), "Invalid image.") if imageX == 0 or imageY == 0 then return image end local output = {{},{},{}} local realOutput = {{},{},{}} local tx, ty, corners local imageX, imageY = getSize(image) local originX, originY = originX or math.floor(imageX / 2), originY or math.floor(imageY / 2) corners = { {rotatePoint(1, 1, angle, originX, originY)}, {rotatePoint(imageX, 1, angle, originX, originY)}, {rotatePoint(1, imageY, angle, originX, originY)}, {rotatePoint(imageX, imageY, angle, originX, originY)}, } local minX = math.min(corners[1][1], corners[2][1], corners[3][1], corners[4][1]) local maxX = math.max(corners[1][1], corners[2][1], corners[3][1], corners[4][1]) local minY = math.min(corners[1][2], corners[2][2], corners[3][2], corners[4][2]) local maxY = math.max(corners[1][2], corners[2][2], corners[3][2], corners[4][2]) for y = 1, (maxY - minY) + 1 do output[1][y] = {} output[2][y] = {} output[3][y] = {} for x = 1, (maxX - minX) + 1 do tx, ty = rotatePoint(x + minX - 1, y + minY - 1, -angle, originX, originY) output[1][y][x] = " " output[2][y][x] = " " output[3][y][x] = " " if image[1][ty] then if tx >= 1 and tx <= #image[1][ty] then output[1][y][x] = image[1][ty]:sub(tx,tx) output[2][y][x] = image[2][ty]:sub(tx,tx) output[3][y][x] = image[3][ty]:sub(tx,tx) end end end end for y = 1, #output[1] do output[1][y] = table.concat(output[1][y]) output[2][y] = table.concat(output[2][y]) output[3][y] = table.concat(output[3][y]) end return output, math.ceil(minX), math.ceil(minY) end local setBarMsg = function(message) pain.barmsg = message pain.barlife = 16 pain.doRender = true end local controlHoldCheck = {} -- used to prevent repeated inputs on non-repeating controls local control = { quit = { key = keys.q, holdDown = false, modifiers = { [keys.leftCtrl] = true }, }, scrollUp = { -- decrease scrollY 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 = {}, }, switchNextFrame = { key = keys.rightBracket, holdDown = false, modifiers = { [keys.leftShift] = true }, }, switchPrevFrame = { key = keys.leftBracket, holdDown = false, modifiers = { [keys.leftShift] = true }, }, swapNextFrame = { key = keys.rightBracket, holdDownn = false, modifiers = { [keys.leftShift] = true, [keys.leftAlt] = true, } }, swapPrevFrame = { key = keys.leftBracket, holdDownn = false, modifiers = { [keys.leftShift] = true, [keys.leftAlt] = true, } }, increaseBrushSize = { key = keys.equals, holdDown = false, modifiers = {}, }, increaseBrushSize_Alt = { key = keys.numPadAdd, holdDown = false, modifiers = {}, }, decreaseBrushSize = { key = keys.minus, holdDown = false, modifiers = {}, }, decreaseBrushSize_Alt = { key = keys.numPadSubtract, holdDown = false, modifiers = {}, }, moveMod = { key = keys.leftShift, holdDown = true, modifiers = { [keys.leftShift] = true }, }, creepMod = { key = keys.leftAlt, holdDown = true, modifiers = { [keys.leftAlt] = true }, }, toolMod = { key = keys.leftShift, holdDown = true, modifiers = { [keys.leftShift] = true }, }, pencilTool = { key = keys.p, holdDown = false, modifiers = { [keys.leftShift] = true }, }, brushTool = { key = keys.b, holdDown = false, modifiers = { [keys.leftShift] = true }, }, textTool = { key = keys.t, holdDown = false, modifiers = { [keys.leftShift] = true }, }, lineTool = { key = keys.l, holdDown = false, modifiers = { [keys.leftShift] = true }, }, adjustPaletteTextUp = { mouse = "mouse_scroll", direction = 1, holdDown = true, modifiers = { [keys.leftShift] = true }, }, adjustPaletteBackgroundUp = { mouse = "mouse_scroll", direction = 1, holdDown = true, modifiers = {}, }, adjustPaletteTextDown = { mouse = "mouse_scroll", direction = -1, holdDown = true, modifiers = { [keys.leftShift] = true }, }, adjustPaletteBackgroundDown = { mouse = "mouse_scroll", direction = -1, holdDown = true, modifiers = {}, }, adjustPaletteTextUp_Alt = { key = keys.rightBracket, modifiers = { [keys.leftShift] = true }, }, adjustPaletteBackgroundUp_Alt = { key = keys.rightBracket, modifiers = {}, }, adjustPaletteTextDown_Alt = { key = keys.leftBracket, modifiers = { [keys.leftShift] = true }, }, adjustPaletteBackgroundDown_Alt = { key = keys.leftBracket, modifiers = {}, }, selectPalette_0 = { key = keys.zero, holdDown = false, modifiers = {}, }, selectPalette_1 = { key = keys.one, holdDown = false, modifiers = {}, }, selectPalette_2 = { key = keys.two, holdDown = false, modifiers = {}, }, selectPalette_3 = { key = keys.three, holdDown = false, modifiers = {}, }, selectPalette_4 = { key = keys.four, holdDown = false, modifiers = {}, }, selectPalette_5 = { key = keys.five, holdDown = false, modifiers = {}, }, selectPalette_6 = { key = keys.six, holdDown = false, modifiers = {}, }, selectPalette_7 = { key = keys.seven, holdDown = false, modifiers = {}, }, selectPalette_8 = { key = keys.eight, holdDown = false, modifiers = {}, }, selectPalette_9 = { key = keys.nine, holdDown = false, modifiers = {}, }, selectNextPalette = { key = keys.rightBracket, holdDown = false, modifiers = { [keys.leftAlt] = true }, }, selectPrevPalette = { key = keys.leftBracket, holdDown = false, modifiers = { [keys.leftAlt] = true }, }, } local checkControl = function(name) local modlist = { keys.leftCtrl, -- keys.rightCtrl, keys.leftShift, -- keys.rightShift, keys.leftAlt, -- keys.rightAlt, } for i = 1, #modlist do if 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 control[name].key then if keysDown[control[name].key] then if control[name].holdDown then return true else if not controlHoldCheck[name] then controlHoldCheck[name] = true return true end end else controlHoldCheck[name] = false return false end elseif control[name].mouse then if miceQueue[#miceQueue] then if miceQueue[#miceQueue][1] == control[name].mouse and miceQueue[#miceQueue][2] == control[name].direction then if control[name].holdDown then return true else if not controlHoldCheck[name] then controlHoldCheck[name] = true return true end end else controlHoldCheck[name] = false return false end end end end -- converts hex colors to colors api, and back local to_colors, to_blit = { [' '] = 0, ['0'] = 1, ['1'] = 2, ['2'] = 4, ['3'] = 8, ['4'] = 16, ['5'] = 32, ['6'] = 64, ['7'] = 128, ['8'] = 256, ['9'] = 512, ['a'] = 1024, ['b'] = 2048, ['c'] = 4096, ['d'] = 8192, ['e'] = 16384, ['f'] = 32768, }, {} for k,v in pairs(to_colors) do to_blit[v] = k end -- takes two coordinates, and returns every point between the two 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 = {{x=startX,y=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=x,y=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] = {x=math.floor(x+0.5),y=y} x = x + dx end else for y=minY,maxY,-1 do out[#out+1] = {x=math.floor(x+0.5),y=y} x = x - dx end end end return out end -- deletes a dot on the canvas, fool local deleteDot = function(x, y, frame) x, y = 1 + x - pain.size.x, 1 + y - pain.size.y if canvas[frame][1][y] then if canvas[frame][1][y][x] then canvas[frame][1][y][x] = nil canvas[frame][2][y][x] = nil canvas[frame][3][y][x] = nil end end end -- places a dot on the canvas, predictably enough local placeDot = function(x, y, frame, dot) x, y = 1 - pain.size.x + x, 1 - pain.size.y + y if not canvas[frame][1][y] then canvas[frame][1][y] = {} canvas[frame][2][y] = {} canvas[frame][3][y] = {} end canvas[frame][1][y][x] = dot[1] canvas[frame][2][y][x] = dot[2] canvas[frame][3][y][x] = dot[3] end -- used for tools that involve dragging local dragPos = {} local getGridAtPos = function(x, y) local guide1 = "SCREEN SIZE" local guide2 = "BLITTLE SIZE" local grid = { "..%%", "..%%", "..%%", "%%..", "%%..", "%%..", } if x < 1 or y < 1 then return "/", "7", "f" else if y <= pain.guideHeight then if x == 1 + (pain.guideWidth) or x == 1 + 2 * pain.guideWidth then return "@", "7", "f" end elseif y == 1 + pain.guideHeight then if x == 1 + 2 * pain.guideWidth then return "@", "7", "f" elseif x <= pain.guideWidth - #guide1 - pain.guideLeftAdj or (x >= pain.guideWidth - pain.guideLeftAdj + 1 and x <= 1 + pain.guideWidth) then return "@", "7", "f" elseif x <= pain.guideWidth then local xx = x - (pain.guideWidth - #guide1 - pain.guideLeftAdj) return guide1:sub(xx, xx), "7", "f" end elseif y <= pain.guideHeight * 3 then if x == 1 + 2 * pain.guideWidth then return "@", "7", "f" end elseif y == pain.guideHeight * 3 + 1 then if x <= 2 * pain.guideWidth - #guide2 + -pain.guideLeftAdj or (x >= 2 * pain.guideWidth - pain.guideLeftAdj + 1 and x <= 1 + 2 * pain.guideWidth) then return "@", "7", "f" elseif x <= 2 * pain.guideWidth then local xx = x - (2 * pain.guideWidth - #guide2 - pain.guideLeftAdj) return guide2:sub(xx, xx), "7", "f" end end local sx, sy = 1 + (1 + x) % #grid[1], 1 + (2 + y) % #grid return grid[sy]:sub(sx,sx), "7", "f" end end local getEvents = function(...) local evt while true do evt = {os.pullEvent()} for i = 1, #arg do if evt[1] == arg[i] then return table.unpack(evt) end end end end -- every tool at your disposal local tools = { pencil = { info = { name = "Pencil", swapTool = "line", -- if swap button is held, will turn into this tool altTool = "text", -- if middle mouse button is held, will use this tool (overrides swapTool) swapArg = { -- any values in this table will override those in 'arg' if using swapTool size = 1 }, altArg = {}, -- any values in this table will override those in 'arg' if using altTool }, run = function(arg) if arg.event == "mouse_click" then if arg.actButton == 1 then placeDot(arg.sx, arg.sy, arg.frame, arg.dot) elseif arg.actButton == 2 then deleteDot(arg.sx, arg.sy, arg.frame) end dragPos = {arg.sx, arg.sy} else if #dragPos == 0 then dragPos = {arg.sx, arg.sy} end local poses = getDotsInLine(arg.sx, arg.sy, dragPos[1], dragPos[2]) for i = 1, #poses do if arg.actButton == 1 then placeDot(poses[i].x, poses[i].y, arg.frame, arg.dot) elseif arg.actButton == 2 then deleteDot(poses[i].x, poses[i].y, arg.frame) end end dragPos = {arg.sx, arg.sy} end end }, brush = { info = { name = "Brush", swapTool = "line", altTool = "text", swapArg = {}, altArg = {} }, run = function(arg) if arg.event == "mouse_click" then for y = -arg.size, arg.size do for x = -arg.size, arg.size do if math.sqrt(x^2 + y^2) <= arg.size / 2 then if arg.actButton == 1 then placeDot(arg.sx + x, arg.sy + y, arg.frame, arg.dot) elseif arg.actButton == 2 then deleteDot(arg.sx + x, arg.sy + y, arg.frame) end end end end dragPos = {arg.sx, arg.sy} else if #dragPos == 0 then dragPos = {arg.sx, arg.sy} end local poses = getDotsInLine(arg.sx, arg.sy, dragPos[1], dragPos[2]) for i = 1, #poses do for y = -arg.size, arg.size do for x = -arg.size, arg.size do if math.sqrt(x^2 + y^2) <= arg.size / 2 then if arg.actButton == 1 then placeDot(poses[i].x + x, poses[i].y + y, arg.frame, arg.dot) elseif arg.actButton == 2 then deleteDot(poses[i].x + x, poses[i].y + y, arg.frame) end end end end end dragPos = {arg.sx, arg.sy} end end }, text = { info = { name = "Text", swapTool = "pencil", altTool = "text", swapArg = {}, altArg = {} }, run = function(arg) pain.paused = true pain.barmsg = "Type text to add to canvas." pain.barlife = 1 render() term.setCursorPos(arg.x, arg.y) term.setTextColor(to_colors[arg.dot[2]]) term.setBackgroundColor(to_colors[arg.dot[3]]) local text = read() -- re-render every keypress, requires custom read function for i = 1, #text do placeDot(arg.sx + i - 1, arg.sy, arg.frame, {text:sub(i,i), pain.dots[pain.dot][2], pain.dots[pain.dot][3]}) end pain.paused = false keysDown = {} miceDown = {} end }, line = { info = { name = "Line", swapTool = "pencil", altTool = "brush", swapArg = {}, altArg = {} }, run = function(arg) local dots while miceDown[arg.button] do arg.size = arg.size or pain.brushSize dots = getDotsInLine( dragPoses[arg.button][1].x + (arg.scrollX - pain.scrollX), dragPoses[arg.button][1].y + (arg.scrollY - pain.scrollY), dragPoses[arg.button][2].x, dragPoses[arg.button][2].y ) render() for i = 1, #dots do if dots[i].x >= pain.size.x and dots[i].x < pain.size.x + pain.size.width then for y = -arg.size, arg.size do for x = -arg.size, arg.size do if math.sqrt(x^2 + y^2) <= arg.size / 2 then if (not pain.showBar) or dots[i].y + y < -1 + pain.size.y + pain.size.height then term.setCursorPos(dots[i].x + x, dots[i].y + y) if arg.actButton == 1 then term.blit(table.unpack(arg.dot)) elseif arg.actButton == 2 then term.blit(getGridAtPos(dots[i].x + pain.scrollX + x, dots[i].y + pain.scrollY + y)) end end end end end end end os.pullEvent() end -- write dots to canvas for i = 1, #dots do for y = -arg.size, arg.size do for x = -arg.size, arg.size do if math.sqrt(x^2 + y^2) <= arg.size / 2 then if arg.actButton == 1 then placeDot(dots[i].x + x + pain.scrollX, dots[i].y + y + pain.scrollY, arg.frame, arg.dot) elseif arg.actButton == 2 then deleteDot(dots[i].x + x + pain.scrollX, dots[i].y + y + pain.scrollY, arg.frame) end end end end end end }, } -- ran every event on separate coroutine -- will check if you should be using a tool given mouse and key inputs, then runs said tool local tryTool = function() local swapArg = {} local t = tools[pain.tool] if miceDown[3] then swapArg = t.info.altArg or {} t = tools[t.info.altTool] end if checkControl("toolMod") then swapArg = t.info.swapArg or {} t = tools[t.info.swapTool] end swapArg.actButton = miceDown[3] and 1 for butt = 1, 3 do if miceDown[butt] and t then t.run({ x = swapArg.x or miceDown[butt].x, y = swapArg.y or miceDown[butt].y, sx = swapArg.sx or ((swapArg.x or miceDown[butt].x) + pain.scrollX), sy = swapArg.sy or ((swapArg.y or miceDown[butt].y) + pain.scrollY), scrollX = swapArg.scrollX or pain.scrollX, scrollY = swapArg.scrollY or pain.scrollY, frame = swapArg.frame or pain.frame, dot = swapArg.dot or pain.dots[pain.dot], size = swapArg.size or pain.brushSize, button = swapArg.button or butt, actButton = swapArg.actButton or butt, -- will act as if this button is held, if not nil event = swapArg.event or miceDown[butt].event }) pain.doRender = true break end end end -- shows everything on screen render = function(x, y, width, height) local buffer = {{},{},{}} local cx, cy x = x or pain.size.x y = y or pain.size.y width = width or pain.size.width height = height or pain.size.height -- see, it wouldn't do if I just individually set the cursor position for every dot if useDebugRenderer then term.clear() local cx, cy for yy, line in pairs(canvas[pain.frame][1]) do for xx, dot in pairs(canvas[pain.frame][1][yy]) do cx = xx - pain.scrollX cy = yy - pain.scrollY if cx >= x and cx <= (x + width - 1) and cy >= y and cy <= (x + width - 1) then term.setCursorPos(cx, cy) term.blit( canvas[pain.frame][1][yy][xx], canvas[pain.frame][2][yy][xx], canvas[pain.frame][3][yy][xx] ) end end end else local gChar, gText, gBack for yy = 1, -1 + height + y do buffer[1][yy] = "" buffer[2][yy] = "" buffer[3][yy] = "" if pain.showBar and yy == height then term.setTextColor(colors.black) term.setBackgroundColor(colors.lightGray) term.setCursorPos(pain.size.x, -1 + pain.size.y + pain.size.height) term.write("[" .. pain.scrollX .. "," .. pain.scrollY .. "] ") for i = 1, #pain.dots do if flashPaletteOnBar > 0 then if i == pain.dot then term.blit(tostring(i), "0", pain.dots[i][3]) else term.blit(tostring(i), "7", pain.dots[i][3]) end else term.blit(table.unpack(pain.dots[i])) end end if pain.barlife > 0 then term.write(" " .. pain.barmsg) else term.write(" " .. tools[pain.tool].info.name .. " tool") end term.write((" "):rep(x + width - term.getCursorPos())) else for xx = 1, width do cx = xx + pain.scrollX cy = yy + pain.scrollY if canvas[pain.frame][1][cy] then if canvas[pain.frame][1][cy][cx] then for c = 1, 3 do buffer[c][yy] = buffer[c][yy] .. canvas[pain.frame][c][cy][cx] end else gChar, gText, gBack = getGridAtPos(cx, cy) buffer[1][yy] = buffer[1][yy] .. gChar buffer[2][yy] = buffer[2][yy] .. gText buffer[3][yy] = buffer[3][yy] .. gBack end else gChar, gText, gBack = getGridAtPos(cx, cy) buffer[1][yy] = buffer[1][yy] .. gChar buffer[2][yy] = buffer[2][yy] .. gText buffer[3][yy] = buffer[3][yy] .. gBack end end end end for yy = 0, height - 1 do term.setCursorPos(x, y + yy) term.blit(buffer[1][yy+1], buffer[2][yy+1], buffer[3][yy+1]) end end if false then term.setCursorPos(1,1) write(textutils.serialize(miceDown)) end end local getInput = function() local evt, adjX, adjY, paletteListX local keySwapList = { [keys.rightShift] = keys.leftShift, [keys.rightAlt] = keys.leftAlt, [keys.rightCtrl] = keys.leftCtrl, } while true do evt = {os.pullEvent()} if evt[1] == "mouse_click" or evt[1] == "mouse_drag" then -- start X for the list of color palettes to choose from paletteListX = 5 + #tostring(pain.scrollX) + #tostring(pain.scrollY) -- (x, y) relative to (pain.size.x, pain.size.y) adjX, adjY = 1 + evt[3] - pain.size.x, 1 + evt[4] - pain.size.y if adjX >= 1 and adjX <= pain.size.width and adjY >= 1 and adjY <= pain.size.height then pain.isInFocus = true if adjY == pain.size.height then if evt[1] == "mouse_click" then if adjX >= paletteListX and adjX <= -1 + paletteListX + #pain.dots then pain.dot = 1 + adjX - paletteListX setBarMsg("Selected palette " .. pain.dot .. ".") else -- openBarMenu() end end else if pain.limitOneMouseButton then dragPoses = { dragPoses[1] or {{},{}}, dragPoses[2] or {{},{}}, dragPoses[3] or {{},{}} } dragPoses = { [evt[2]] = { { x = dragPoses[evt[2]][1].x or evt[3], y = dragPoses[evt[2]][1].y or evt[4] }, { x = evt[3], y = evt[4] } } } if evt[1] == "mouse_click" or miceDown[evt[2]] then miceDown = {{},{},{}} miceDown = { [evt[2]] = { event = evt[1], button = evt[2], x = evt[3], y = evt[4], } } end else dragPoses[evt[2]] = { { x = dragPoses[evt[2]][1].x or evt[3], y = dragPoses[evt[2]][1].y or evt[4] }, { x = evt[3], y = evt[4] } } if evt[1] == "mouse_click" or miceDown[evt[2]] then miceDown[evt[2]] = { event = evt[1], button = evt[2], x = evt[3], y = evt[4], } end end end else pain.isInFocus = false end elseif evt[1] == "key" then if pain.isInFocus then keysDown[evt[2]] = true keysDown[keySwapList[evt[2]] or evt[2]] = true else keysDown = {} end elseif evt[1] == "mouse_up" then if pain.limitOneMouseButton then dragPoses = {{{},{}}, {{},{}}, {{},{}}} else dragPoses[evt[2]] = {{},{}}, {{},{}}, {{},{}} end miceDown[evt[2]] = false elseif evt[1] == "mouse_scroll" then -- capture scroll events for special use miceQueue[#miceQueue + 1] = evt elseif evt[1] == "key_up" then keysDown[evt[2]] = false keysDown[keySwapList[evt[2]] or evt[2]] = false end end end -- asynchronously renders the screen local asyncRender = function() while true do os.pullEvent("pain_main_looped") if pain.doRender and not pain.paused then render() pain.doRender = false end sleep(0.05) os.queueEvent("pain_render_looped") end end -- executes everything that doesn't run asynchronously main = function() local evt parallel.waitForAny(asyncRender, function() while true do if not pain.paused then if checkControl("quit") then return true end if checkControl("adjustPaletteTextDown") or checkControl("adjustPaletteTextDown_Alt") then pain.dots[pain.dot][2] = to_blit[math.max(1, to_colors[pain.dots[pain.dot][2]] / 2)] pain.doRender = true end if checkControl("adjustPaletteTextUp") or checkControl("adjustPaletteTextUp_Alt") then pain.dots[pain.dot][2] = to_blit[math.min(2^15, to_colors[pain.dots[pain.dot][2]] * 2)] pain.doRender = true end if checkControl("adjustPaletteBackgroundDown") or checkControl("adjustPaletteBackgroundDown_Alt") then pain.dots[pain.dot][3] = to_blit[math.max(1, to_colors[pain.dots[pain.dot][3]] / 2)] pain.doRender = true end if checkControl("adjustPaletteBackgroundUp") or checkControl("adjustPaletteBackgroundUp_Alt") then pain.dots[pain.dot][3] = to_blit[math.min(2^15, to_colors[pain.dots[pain.dot][3]] * 2)] pain.doRender = true end -- handle scrolling if checkControl("resetScroll") then pain.scrollX = 0 pain.scrollY = 0 pain.doRender = true else if checkControl("increaseBrushSize") or checkControl("increaseBrushSize_Alt") then pain.brushSize = math.min(pain.brushSize + 1, 16) setBarMsg("Increased brush size to " .. pain.brushSize .. ".") elseif checkControl("decreaseBrushSize") or checkControl("decreaseBrushSize_Alt") then pain.brushSize = math.max(pain.brushSize - 1, 1) setBarMsg("Decreased brush size to " .. pain.brushSize .. ".") elseif checkControl("scrollLeft") then pain.scrollX = pain.scrollX - 1 pain.doRender = true end if checkControl("scrollRight") then pain.scrollX = pain.scrollX + 1 pain.doRender = true end if checkControl("scrollUp") then pain.scrollY = pain.scrollY - 1 pain.doRender = true end if checkControl("scrollDown") then pain.scrollY = pain.scrollY + 1 pain.doRender = true end end if checkControl("selectNextPalette") then if pain.dot < #pain.dots then pain.dot = pain.dot + 1 flashPaletteOnBar = 6 setBarMsg("Switched to next palette " .. pain.dot .. ".") else setBarMsg("Reached end of palette list.") end end if checkControl("selectPrevPalette") then if pain.dot > 1 then pain.dot = pain.dot - 1 flashPaletteOnBar = 6 setBarMsg("Switched to previous palette " .. pain.dot .. ".") else setBarMsg("Reached beginning of palette list.") end end for i = 0, 9 do if checkControl("selectPalette_" .. i) then if pain.dots[i] then pain.dot = i flashPaletteOnBar = 6 setBarMsg("Selected palette " .. pain.dot .. ".") break else setBarMsg("There is no palette " .. i .. ".") break end end end if checkControl("pencilTool") then pain.tool = "pencil" setBarMsg("Selected pencil tool.") elseif checkControl("textTool") then pain.tool = "text" setBarMsg("Selected text tool.") elseif checkControl("brushTool") then pain.tool = "brush" setBarMsg("Selected brush tool.") elseif checkControl("lineTool") then pain.tool = "line" setBarMsg("Selected line tool.") end -- decrement bar life and palette number indicator -- if it's gonna hit zero, make sure it re-renders if pain.barlife == 1 then pain.doRender = true end pain.barlife = math.max(pain.barlife - 1, 0) if flashPaletteOnBar == 1 then pain.doRender = true end flashPaletteOnBar = math.max(flashPaletteOnBar - 1, 0) end if #miceQueue > 0 then miceQueue[#miceQueue] = nil end TICKNO = TICKNO + 1 os.queueEvent("pain_main_looped") os.pullEvent("pain_render_looped") end end) end local keepTryingTools = function() while true do os.pullEvent() tryTool() end end term.clear() parallel.waitForAny( main, getInput, keepTryingTools ) -- exit cleanly term.setCursorPos(1, scr_y) term.setBackgroundColor(colors.black) term.setTextColor(colors.white) term.clearLine()