Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- NPaintPro
- By NitrogenFingers
- ]]--
- --The screen size
- local w,h = term.getSize()
- --Whether or not the program is currently waiting on user input
- local inMenu = false
- --Whether or not animation tools are enabled (use -a to turn them on)
- local animated = false
- --The tool/mode npaintpro is currently in. Default is "paint"
- --For a list of modes, check out the help file
- local state = "paint"
- --Whether or not the program is presently running
- local isRunning = true
- --The list of every frame, containing every image in the picture/animation
- --Note: nfp files always have the picture at frame 1
- local frames = { }
- --How many frames are currently in the given animation.
- local frameCount = 1
- --The Colour Picker column
- local column = {}
- --The currently selected left and right colours
- local lSel,rSel = colours.white,nil
- --The amount of scrolling on the X and Y axis
- local sx,sy = 0,0
- --The alpha channel colour
- --Change this to change default canvas colour
- local alphaC = colours.cyan
- --The currently selected frame. Default is 1
- local sFrame = 1
- --The contents of the image buffer- contains contents, width and height
- local buffer = nil
- --The position, width and height of the selection rectangle
- local selectrect = nil
- --The animation state of the selection rectangle and image buffer
- local rectblink = 0
- --The ID for the timer
- local recttimer = nil
- --The radius of the brush tool
- local brushsize = 3
- --Whether or not "record" mode is activated (animation mode only)
- local record = false
- --The time between each frame when in play mode (animation mode only)
- local animtime = 0.3
- --A list of hexidecimal conversions from numbers to hex digits
- local hexnums = {[10] = "a", [11] = "b", [12] = "c", [13] = "d", [14] = "e" , [15] = "f"}
- --The NPaintPro logo (divine, isn't it?)
- local logo = {
- "fcc 3 339";
- " fcc 9333 33";
- " fcc 933 333 33";
- " fcc 933 33 33";
- " fcc 933 33 33";
- " c88 333 93333";
- " 888 333 9333";
- " 333 3 333 939";
- }
- --The available menu options in the ctrl menu
- local mChoices = {"Save","Exit"}
- --The available modes from the dropdown menu- tables indicate submenus (include a name!)
- local ddModes = { "paint", "brush", "pippette", "flood", "move", "select", { "save", "exit", name = "file" }, name = "modes" }
- --The available modes from the selection right-click menu
- local srModes = { "cut", "copy", "paste", "clear", "hide", name = "selection" }
- --The list of available help topics for each mode 127
- local helpTopics = {
- [1] = {
- name = "Paint Mode",
- key = nil,
- animonly = false,
- message = "The default mode for NPaintPro, for painting pixels."
- .." Controls here that are not overridden will apply for all other modes. Leaving a mode by selecting that mode "
- .." again will always send the user back to paint mode.",
- controls = {
- { "Arrow keys", "Scroll the canvas" },
- { "Left Click", "Paint/select left colour" },
- { "Right Click", "Paint/select right colour" },
- { "Z Key", "Clear image on screen" },
- { "Tab Key", "Hide selection rectangle if visible" },
- { "Q Key", "Set alpha mask to left colour" },
- { "W Key", "Set alpha mask to right colour" },
- { "Number Keys", "Swich between frames 1-9" },
- { "</> keys", "Move to the next/last frame" },
- { "R Key", "Removes every frame after the current frame"}
- }
- },
- [2] = {
- name = "Brush Mode",
- key = "b",
- animonly = false,
- message = "Brush mode allows painting a circular area of variable diameter rather than a single pixel, working in "..
- "the exact same way as paint mode in all other regards.",
- controls = {
- { "Left Click", "Paints a brush blob with the left colour" },
- { "Right Click", "Paints a brush blob with the right colour" },
- { "Number Keys", "Changes the radius of the brush blob from 2-9" }
- }
- },
- [3] = {
- name = "Pippette Mode",
- key = "p",
- animonly = false,
- message = "Pippette mode allows the user to click the canvas and set the colour clicked to the left or right "..
- "selected colour, for later painting.",
- controls = {
- { "Left Click", "Sets clicked colour to the left selected colour" },
- { "Right Click", "Sets clicked colour to the right selected colour" }
- }
- },
- [4] = {
- name = "Move Mode",
- key = "m",
- animonly = false,
- message = "Mode mode allows the moving of the entire image on the screen. This is especially useful for justifying"..
- " the image to the top-left for animations or game assets.",
- controls = {
- { "Left/Right Click", "Moves top-left corner of image to selected square" },
- { "Arrow keys", "Moves image one pixel in any direction" }
- }
- },
- [5] = {
- name = "Flood Mode (NYI)",
- key = "f",
- animonly = false,
- message = "Flood mode allows the changing of an area of a given colour to that of the selected colour. "..
- "The tool uses a flood4 algorithm and will not fill diagonally. Transparency cannot be flood filled.",
- controls = {
- { "Left Click", "Flood fills selected area to left colour" },
- { "Right Click", "Flood fills selected area to right colour" }
- }
- },
- [6] = {
- name = "Select Mode",
- key = "s",
- animonly = false,
- message = "Select mode allows the creation and use of the selection rectangle, to highlight specific areas on "..
- "the screen and perform operations on the selected area of the image. The selection rectangle can contain an "..
- "image on the clipboard- if it does, the image will flash inside the rectangle, and the rectangle edges will "..
- "be light grey instead of dark grey.",
- controls = {
- { "C Key", "Copy: Moves selection into the clipboard" },
- { "X Key", "Cut: Clears canvas under the rectangle, and moves it into the clipboard" },
- { "V Key", "Paste: Copys clipboard to the canvas" },
- { "Z Key", "Clears clipboard" },
- { "Left Click", "Moves top-left corner of rectangle to selected pixel" },
- { "Right Click", "Opens selection menu" },
- { "Arrow Keys", "Moves rectangle one pixel in any direction" }
- }
- },
- [7] = {
- name = "Corner Select Mode",
- key = nil,
- animonly = false,
- message = "If a selection rectangle isn't visible, this mode is selected automatically. It allows the "..
- "defining of the corners of the rectangle- one the top-left and bottom-right corners have been defined, "..
- "NPaintPro switches to selection mode. Note rectangle must be at least 2 pixels wide and high.",
- controls = {
- { "Left/Right Click", "Defines a corner of the selection rectangle" }
- }
- },
- [8] = {
- name = "Play Mode",
- key = "space",
- animonly = true,
- message = "Play mode will loop through each frame in your animation at a constant rate. Editing tools are "..
- "locked in this mode, and the coordinate display will turn green to indicate it is on.",
- controls = {
- { "</> Keys", "Increases/Decreases speed of the animation" },
- { "Space Bar", "Returns to paint mode" }
- }
- },
- [9] = {
- name = "Record Mode",
- key = "\\",
- animonly = true,
- message = "Record mode is not a true mode, but influences how other modes work. Changes made that modify the "..
- "canvas in record mode will affect ALL frames in the animation. The coordinates will turn red to indicate that "..
- "record mode is on.",
- controls = {
- { "", "Affects:" },
- { "- Paint Mode", "" },
- { "- Brush Mode", "" },
- { "- Cut and Paste in Select Mode", ""},
- { "- Move Mode", ""}
- }
- },
- [10] = {
- name = "File Mode",
- keys = nil,
- animonly = false,
- message = "Clicking on the mode display at the bottom of the screen will open the options menu. Here you can"..
- " activate all of the modes in the program with a simple mouse click."
- controls = { }
- }
- }
- --The "bounds" of the image- the first/last point on both axes where a pixel appears
- local toplim,botlim,leflim,riglim = nil,nil,nil,nil
- --The selected path
- local sPath = nil
- --[[
- Section: Helpers
- ]]--
- --[[Converts a colour parameter into a single-digit hex coordinate for the colour
- Params: colour:int = The colour to be converted
- Returns:string A string conversion of the colour
- ]]--
- local function getHexOf(colour)
- if not colour or not tonumber(colour) then
- return " "
- end
- local value = math.log(colour)/math.log(2)
- if value > 9 then
- value = hexnums[value]
- end
- return value
- end
- --[[Converts a hex digit into a colour value
- Params: hex:?string = the hex digit to be converted
- Returns:string A colour value corresponding to the hex, or nil if the character is invalid
- ]]--
- local function getColourOf(hex)
- local value = tonumber(hex, 16)
- if not value then return nil end
- value = math.pow(2,value)
- return value
- end
- --[[Finds the biggest and smallest bounds of the image- the outside points beyond which pixels do not appear
- These values are assigned to the "lim" parameters for access by other methods
- Params: forAllFrames:bool = True if all frames should be used to find bounds, otherwise false or nil
- Returns:nil
- ]]--
- local function updateImageLims(forAllFrames)
- local f,l = sFrame,sFrame
- if forAllFrames == true then f,l = 1,framecount end
- toplim,botlim,leflim,riglim = nil,nil,nil,nil
- for locf = f,l do
- for y,_ in pairs(frames[locf]) do
- for x,_ in pairs(frames[locf][y]) do
- if frames[locf][y][x] ~= nil then
- if leflim == nil or x < leflim then leflim = x end
- if toplim == nil or y < toplim then toplim = y end
- if riglim == nil or x > riglim then riglim = x end
- if botlim == nil or y > botlim then botlim = y end
- end
- end
- end
- end
- end
- --[[Updates the rectangle blink timer. Should be called anywhere events are captured, along with a timer capture.
- Params: nil
- Returns:nil
- ]]--
- local function updateTimer(id)
- if id == recttimer then
- recttimer = os.startTimer(0.5)
- rectblink = (rectblink % 2) + 1
- end
- end
- --[[
- Section: Loading
- ]]--
- --[[Loads a non-animted paint file into the program
- Params: path:string = The path in which the file is located
- Returns:nil
- ]]--
- local function loadNFP(path)
- sFrame = 1
- frames[sFrame] = { }
- if fs.exists(path) then
- local file = io.open(path, "r" )
- local sLine = file:read()
- local num = 1
- while sLine do
- table.insert(frames[sFrame], num, {})
- for i=1,#sLine do
- frames[sFrame][num][i] = getColourOf(string.sub(sLine,i,i))
- end
- num = num+1
- sLine = file:read()
- end
- file:close()
- end
- end
- --[[Saves a non-animated paint file to the specified path
- Params: path:string = The path to save the file to
- Returns:nil
- ]]--
- local function saveNFP(path)
- local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath))
- if not fs.exists(sDir) then
- fs.makeDir(sDir)
- end
- local file = io.open(path, "w")
- updateImageLims(false)
- if not toplim then
- file:close()
- return
- end
- for y=1,botlim do
- local line = ""
- if frames[sFrame][y] then
- for x=1,riglim do
- line = line..getHexOf(frames[sFrame][y][x])
- end
- end
- file:write(line.."\n")
- end
- file:close()
- end
- --[[Loads an animated paint file into the program
- Params: path:string = The path in which the file is located
- Returns:nil
- ]]--
- local function loadNFA(path)
- frames[sFrame] = { }
- if fs.exists(path) then
- local file = io.open(path, "r" )
- local sLine = file:read()
- local num = 1
- while sLine do
- table.insert(frames[sFrame], num, {})
- if sLine == "~" then
- sFrame = sFrame + 1
- frames[sFrame] = { }
- num = 1
- else
- for i=1,#sLine do
- frames[sFrame][num][i] = getColourOf(string.sub(sLine,i,i))
- end
- num = num+1
- end
- sLine = file:read()
- end
- file:close()
- end
- framecount = sFrame
- sFrame = 1
- end
- --[[Saves a animated paint file to the specified path
- Params: path:string = The path to save the file to
- Returns:nil
- ]]--
- local function saveNFA(path)
- local file = io.open(path, "w")
- updateImageLims(true)
- if not toplim then
- file:close()
- return
- end
- for i=1,#frames do
- for y=1,botlim do
- local line = ""
- if frames[i][y] then
- for x=1,riglim do
- line = line..getHexOf(frames[i][y][x])
- end
- end
- file:write(line.."\n")
- end
- if i < #frames then file:write("~\n") end
- end
- file:close()
- end
- --[[Initializes the program, by loading in the paint file. Called at the start of each program.
- Params: none
- Returns:nil
- ]]--
- local function init()
- if animated then
- loadNFA(sPath)
- table.insert(ddModes, { "record", "play", name = "anim" })
- table.insert(ddModes, { "go to", "remove", name = "frames"})
- else loadNFP(sPath) end
- for i=0,15 do
- table.insert(column, math.pow(2,i))
- end
- end
- --[[
- Section: Drawing
- ]]--
- --[[Draws the rather superflous logo. Takes about 1 second, before user is able to move to the
- actual program.
- Params: none
- Returns:nil
- ]]--
- local function drawLogo()
- term.setBackgroundColour(colours.white)
- term.clear()
- for y=1,#logo do
- for x=1,#logo[y] do
- term.setCursorPos(w/2 - #logo[y]/2 + x, h/2 - #logo/2 + y)
- local col = getColourOf(string.sub(logo[y], x, x))
- if not col then col = 1 end
- term.setBackgroundColour(col)
- term.write(" ")
- end
- end
- sleep(1)
- term.setBackgroundColour(colours.white)
- term.setTextColour(colours.black)
- local msg = "NPaintPro"
- term.setCursorPos(w/2 - #msg/2, h-3)
- term.write(msg)
- msg = "By NitrogenFingers"
- term.setCursorPos(w/2 - #msg/2, h-2)
- term.write(msg)
- os.pullEvent()
- end
- --[[Clears the display to the alpha channel colour, draws the canvas, the image buffer and the selection
- rectanlge if any of these things are present.
- Params: none
- Returns:nil
- ]]--
- local function drawCanvas()
- term.setBackgroundColor(alphaC)
- --term.clear()
- --Picture first
- for y=sy+1,sy+h-1 do
- if frames[sFrame][y] then
- for x=sx+1,sx+w-2 do
- if frames[sFrame][y][x] then
- term.setBackgroundColour(frames[sFrame][y][x])
- else
- term.setBackgroundColour(alphaC)
- end
- term.setCursorPos(x-sx,y-sy)
- term.write(" ")
- end
- else
- for x=sx+1,sx+w-2 do
- term.setBackgroundColour(alphaC)
- term.setCursorPos(x-sx,y-sy)
- term.write(" ")
- end
- end
- end
- --Then the buffer
- if selectrect then
- if buffer and rectblink == 1 then
- for y=selectrect.y1, math.min(selectrect.y2, selectrect.y1 + buffer.height-1) do
- for x=selectrect.x1, math.min(selectrect.x2, selectrect.x1 + buffer.width-1) do
- if buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1] then
- term.setCursorPos(x+sx,y+sy)
- term.setBackgroundColour(buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1])
- term.write(" ")
- end
- end
- end
- end
- --This draws the "selection" box
- local add = nil
- if buffer then
- term.setBackgroundColour(colours.lightGrey)
- else
- term.setBackgroundColour(colours.grey)
- end
- for i=selectrect.x1, selectrect.x2 do
- add = (i + selectrect.y1 + rectblink) % 2 == 0
- term.setCursorPos(i-sx,selectrect.y1-sy)
- if add then term.write(" ") end
- add = (i + selectrect.y2 + rectblink) % 2 == 0
- term.setCursorPos(i-sx,selectrect.y2-sy)
- if add then term.write(" ") end
- end
- for i=selectrect.y1 + 1, selectrect.y2 - 1 do
- add = (i + selectrect.x1 + rectblink) % 2 == 0
- term.setCursorPos(selectrect.x1-sx,i-sy)
- if add then term.write(" ") end
- add = (i + selectrect.x2 + rectblink) % 2 == 0
- term.setCursorPos(selectrect.x2-sx,i-sy)
- if add then term.write(" ") end
- end
- end
- end
- --[[Draws the colour picker on the right side of the screen, the colour pallette and the footer with any
- messages currently being displayed
- Params: none
- Returns:nil
- ]]--
- local function drawInterface()
- --Picker
- for i=1,#column do
- term.setCursorPos(w-1, i)
- term.setBackgroundColour(column[i])
- term.write(" ")
- end
- term.setCursorPos(w-1,#column+1)
- term.setBackgroundColour(colours.black)
- term.setTextColour(colours.red)
- term.write("XX")
- --Pallette
- term.setCursorPos(w-1,h-1)
- if not lSel then
- term.setBackgroundColour(colours.black)
- term.setTextColour(colours.red)
- term.write("X")
- else
- term.setBackgroundColour(lSel)
- term.setTextColour(lSel)
- term.write(" ")
- end
- if not rSel then
- term.setBackgroundColour(colours.black)
- term.setTextColour(colours.red)
- term.write("X")
- else
- term.setBackgroundColour(rSel)
- term.setTextColour(rSel)
- term.write(" ")
- end
- --Footer
- if inMenu then return end
- term.setCursorPos(1, h)
- term.setBackgroundColour(colours.lightGrey)
- term.setTextColour(colours.grey)
- term.clearLine()
- term.write(string.upper(string.sub(state, 1, 1))..string.sub(state, 2, #state).." mode")
- if state == "brush" then term.write(", size="..brushsize) end
- term.write(" (H for help)")
- local coords="X:"..sx.." Y:"..sy
- if animated then coords = coords.." Frame:"..sFrame.."/"..framecount.." " end
- term.setCursorPos(w-#coords+1,h)
- if state == "play" then term.setBackgroundColour(colours.lime)
- elseif record then term.setBackgroundColour(colours.red) end
- term.write(coords)
- if animated then
- term.setCursorPos(w-1,h)
- term.setBackgroundColour(colours.grey)
- term.setTextColour(colours.lightGrey)
- term.write("<>")
- end
- end
- --[[Runs an interface where users can select topics of help. Will return once the user quits the help screen.
- Params: none
- Returns:nil
- ]]--
- local function drawHelpScreen()
- local selectedHelp = nil
- while true do
- term.setBackgroundColour(colours.lightGrey)
- term.clear()
- if not selectedHelp then
- term.setCursorPos(4, 1)
- term.setTextColour(colours.brown)
- term.write("Available modes (click for info):")
- for i=1,#helpTopics do
- term.setCursorPos(2, 2 + i)
- term.setTextColour(colours.black)
- term.write(helpTopics[i].name)
- if helpTopics[i].key then
- term.setTextColour(colours.red)
- term.write(" ("..helpTopics[i].key..")")
- end
- end
- term.setCursorPos(4,h)
- term.setTextColour(colours.black)
- term.write("Press any key to exit")
- else
- term.setCursorPos(4,1)
- term.setTextColour(colours.brown)
- term.write(helpTopics[selectedHelp].name)
- if helpTopics[selectedHelp].key then
- term.setTextColour(colours.red)
- term.write(" ("..helpTopics[selectedHelp].key..")")
- end
- term.setCursorPos(1,3)
- term.setTextColour(colours.black)
- print(helpTopics[selectedHelp].message.."\n")
- for i=1,#helpTopics[selectedHelp].controls do
- term.setTextColour(colours.brown)
- term.write(helpTopics[selectedHelp].controls[i][1].." ")
- term.setTextColour(colours.black)
- print(helpTopics[selectedHelp].controls[i][2])
- end
- end
- local id,p1,p2,p3 = os.pullEvent()
- if id == "timer" then updateTimer(p1)
- elseif id == "key" then
- if selectedHelp then selectedHelp = nil
- else break end
- elseif id == "mouse_click" then
- if not selectedHelp then
- if p3 >=3 and p3 <= 2+#helpTopics then
- selectedHelp = p3-2
- else break end
- else
- selectedHelp = nil
- end
- end
- end
- end
- --[[Draws a message in the footer bar. A helper for DrawInterface, but can be called for custom messages, if the
- inMenu paramter is set to true while this is being done (remember to set it back when done!)
- Params: message:string = The message to be drawn
- Returns:nil
- ]]--
- local function drawMessage(message)
- term.setCursorPos(1,h)
- term.setBackgroundColour(colours.lightGrey)
- term.setTextColour(colours.grey)
- term.clearLine()
- term.write(message)
- end
- --[[ Image tools ]]--
- --[[Copies all pixels beneath the selection rectangle into the image buffer. Empty buffers are converted to nil.
- Params: removeImage:bool = true if the image is to be erased after copying, false otherwise
- Returns:nil
- ]]--
- local function copyToBuffer(removeImage)
- buffer = { width = selectrect.x2 - selectrect.x1 + 1, height = selectrect.y2 - selectrect.y1 + 1, contents = { } }
- local containsSomething = false
- for y=1,buffer.height do
- buffer.contents[y] = { }
- local f,l = sFrame,sFrame
- if record then f,l = 1, framecount end
- for fra = f,l do
- if frames[fra][selectrect.y1 + y - 1] then
- for x=1,buffer.width do
- buffer.contents[y][x] = frames[sFrame][selectrect.y1 + y - 1][selectrect.x1 + x - 1]
- if removeImage then frames[fra][selectrect.y1 + y - 1][selectrect.x1 + x - 1] = nil end
- if buffer.contents[y][x] then containsSomething = true end
- end
- end
- end
- end
- --I don't classify an empty buffer as a real buffer- confusing to the user.
- if not containsSomething then buffer = nil end
- end
- --[[Replaces all pixels under the selection rectangle with the image buffer (or what can be seen of it). Record-dependent.
- Params: removeBuffer:bool = true if the buffer is to be emptied after copying, false otherwise
- Returns:nil
- ]]--
- local function copyFromBuffer(removeBuffer)
- if not buffer then return end
- for y = 1, math.min(buffer.height,selectrect.y2-selectrect.y1) do
- local f,l = sFrame, sFrame
- if record then f,l = 1, framecount end
- for fra = f,l do
- if not frames[fra][selectrect.y1+y-1] then frames[fra][selectrect.y1+y-1] = { } end
- for x = 1, math.min(buffer.width,selectrect.x2-selectrect.x1) do
- frames[fra][selectrect.y1+y-1][selectrect.x1+x-1] = buffer.contents[y][x]
- end
- end
- end
- if removeBuffer then buffer = nil end
- end
- --[[Moves the entire image (or entire animation) to the specified coordinates. Record-dependent.
- Params: newx:int = the X coordinate to move the image to
- newy:int = the Y coordinate to move the image to
- Returns:nil
- ]]--
- local function moveImage(newx,newy)
- if not leflim or not toplim then return end
- if newx <=0 or newy <=0 then return end
- local f,l = sFrame,sFrame
- if record then f,l = 1,framecount end
- for i=f,l do
- local newlines = { }
- for y,line in pairs(frames[i]) do
- newlines[y-toplim+newy] = { }
- for x,char in pairs(line) do
- newlines[y-toplim+newy][x-leflim+newx] = char
- end
- end
- frames[i] = newlines
- end
- end
- --[[Prompts the user to clear the current frame or all frames. Record-dependent.,
- Params: none
- Returns:nil
- ]]--
- local function clearImage()
- inMenu = true
- if not animated then
- drawMessage("Clear image? Y/N: ")
- elseif record then
- drawMessage("Clear ALL frames? Y/N: ")
- else
- drawMessage("Clear current frame? Y/N :")
- end
- if string.find(string.upper(readInput(1)), "Y") then
- local f,l = sFrame,sFrame
- if record then f,l = 1,framecount end
- for i=f,l do
- frames[i] = { }
- end
- end
- inMenu = false
- end
- --[[A recursively called method (watch out for big calls!) in which every pixel of a set colour is
- changed to another colour. Does not work on the nil colour, for obvious reasons.
- Params: x:int = The X coordinate of the colour to flood-fill
- y:int = The Y coordinate of the colour to flood-fill
- targetColour:colour = the colour that is being flood-filled
- newColour:colour = the colour with which to replace the target colour
- Returns:nil
- ]]--
- local function floodFill(x, y, targetColour, newColour)
- if not frames[sFrame][y] or frames[sFrame][y][x] ~= targetColour or targetColour == newColour then return end
- frames[sFrame][y][x] = newColour
- if x > 1 then floodFill(x-1, y, targetColour, newColour) end
- if y > 1 then floodFill(x, y-1, targetColour, newColour) end
- floodFill(x+1, y, targetColour, newColour)
- floodFill(x, y+1, targetColour, newColour)
- end
- --[[ Animation Tools ]]--
- --[[Enters play mode, allowing the animation to play through. Interface is restricted to allow this,
- and method only leaves once the player leaves play mode.
- Params: none
- Returns:nil
- ]]--
- local function playAnimation()
- state = "play"
- selectedrect = nil
- os.pullEvent("timer")
- os.startTimer(animtime)
- repeat
- drawCanvas()
- drawInterface()
- local id,key,_,y = os.pullEvent()
- if id=="timer" then
- os.startTimer(animtime)
- sFrame = (sFrame % framecount) + 1
- elseif id=="key" then
- if key == keys.comma and animtime > 0.1 then animtime = animtime - 0.05
- elseif key == keys.period and animtime < 0.5 then animtime = animtime + 0.05
- elseif key == keys.space then state = "paint" end
- elseif id=="mouse_click" and y == h then
- state = "paint"
- end
- until state ~= "play"
- os.startTimer(0.5)
- end
- --[[Changes the selected frame (sFrame) to the chosen frame. If this frame is above the framecount,
- additional frames are created with a copy of the image on the selected frame.
- Params: newframe:int = the new frame to move to
- Returns:nil
- ]]--
- local function changeFrame(newframe)
- inMenu = true
- if not tonumber(newframe) then
- term.setCursorPos(1,h)
- term.setBackgroundColour(colours.lightGrey)
- term.setTextColour(colours.grey)
- term.clearLine()
- term.write("Go to frame: ")
- newframe = tonumber(readInput(2))
- if not newframe or newframe <= 0 then
- inMenu = false
- return
- end
- elseif newframe <= 0 then return end
- if newframe > framecount then
- for i=framecount+1,newframe do
- frames[i] = {}
- for y,line in pairs(frames[sFrame]) do
- frames[i][y] = { }
- for x,v in pairs(line) do
- frames[i][y][x] = v
- end
- end
- end
- framecount = newframe
- end
- sFrame = newframe
- inMenu = false
- end
- --[[Removes every frame leading after the frame passed in
- Params: frame:int the non-inclusive lower bounds of the delete
- Returns:nil
- ]]--
- local function removeFramesAfter(frame)
- inMenu = true
- if frame==framecount then return end
- drawMessage("Remove frames "..(frame+1).."/"..framecount.."? Y/N :")
- local answer = string.upper(readInput(1))
- if string.find(answer, string.upper("Y")) ~= 1 then
- inMenu = false
- return
- end
- for i=frame+1, framecount do
- frames[i] = nil
- end
- framecount = frame
- inMenu = false
- end
- --[[
- Section: Interface
- ]]--
- --[[This function changes the current paint program to another tool or mode, depending on user input. Handles
- any necessary changes in logic involved in that.
- Params: mode:string = the name of the mode to change to
- Returns:nil
- ]]--
- local function performSelection(mode)
- if not mode or mode == "" then return
- elseif mode == "go to" then
- changeFrame()
- elseif mode == "remove" then
- removeFramesAfter(sFrame)
- elseif mode == "play" then
- playAnimation()
- elseif mode == "copy" and selectrect and selectrect.x1 ~= selectrect.x2 then
- copyToBuffer(false)
- elseif mode == "cut" and selectrect and selectrect.x1 ~= selectrect.x2 then
- copyToBuffer(true)
- elseif mode == "paste" and selectrect and selectrect.x1 ~= selectrect.x2 then
- copyFromBuffer(false)
- elseif mode == "hide" then
- selectrect = nil
- if state == "select" then state = "corner select" end
- elseif mode == "record" then
- record = not record
- elseif mode == "clear" then
- if state=="select" then buffer = nil
- else clearImage() end
- elseif mode == "select" then
- if state=="corner select" or state=="select" then
- state = "paint"
- elseif selectrect and selectrect.x1 ~= selectrect.x2 then
- state = "select"
- else
- state = "corner select"
- end
- elseif mode == "save" then
- if animated then saveNFA(sPath)
- else saveNFP(sPath) end
- elseif mode == "exit" then
- isRunning = false
- elseif mode ~= state then state = mode
- else state = "paint"
- end
- end
- --[[Produces a nice dropdown menu based on a table of strings. Depending on the position, this will auto-adjust the position
- of the menu drawn, and allows nesting of menus and sub menus. Clicking anywhere outside the menu will cancel and return nothing
- Params: x:int = the x position the menu should be displayed at
- y:int = the y position the menu should be displayed at
- options:table = the list of options available to the user, as strings or submenus (tables of strings, with a name parameter)
- Returns:string the selected menu option.
- ]]--
- local function displayDropDown(x, y, options)
- --Figures out the dimensions of our thing
- local longestX = #options.name
- for i=1,#options do
- local currVal = options[i]
- if type(currVal) == "table" then currVal = currVal.name end
- longestX = math.max(longestX, #currVal)
- end
- local xOffset = math.max(0, longestX - ((w-2) - x))
- local yOffset = math.max(0, #options - ((h-1) - y))
- local clickTimes = 0
- local tid = nil
- local selection = nil
- while clickTimes < 2 do
- drawCanvas()
- drawInterface()
- term.setCursorPos(x-xOffset,y-yOffset)
- term.setBackgroundColour(colours.grey)
- term.setTextColour(colours.lightGrey)
- term.write(options.name..string.rep(" ", longestX-#options.name + 1))
- for i=1,#options do
- term.setCursorPos(x-xOffset, y-yOffset+i)
- if i==selection and clickTimes % 2 == 0 then
- term.setBackgroundColour(colours.grey)
- term.setTextColour(colours.lightGrey)
- else
- term.setBackgroundColour(colours.lightGrey)
- term.setTextColour(colours.grey)
- end
- local currVal = options[i]
- if type(currVal) == "table" then currVal = currVal.name end
- term.write(currVal..string.rep(" ", longestX-#currVal + 1))
- end
- local id, p1, p2, p3 = os.pullEvent()
- if id == "timer" then
- if p1 == tid then
- clickTimes = clickTimes + 1
- if clickTimes > 2 then
- break
- else
- tid = os.startTimer(0.1)
- end
- else
- updateTimer(p1)
- drawCanvas()
- drawInterface()
- end
- elseif id == "mouse_click" then
- if p2 >=x-xOffset and p2 <= x-xOffset + longestX and p3 >= y-yOffset+1 and p3 <= y-yOffset+#options+1 then
- selection = p3-(y-yOffset)
- tid = os.startTimer(0.1)
- else
- selection = ""
- break
- end
- end
- end
- if type(selection) == "number" then
- selection = options[selection]
- end
- if type(selection) == "string" then
- return selection
- elseif type(selection) == "table" then
- return displayDropDown(x, y, selection)
- end
- end
- --[[A custom io.read() function with a few differences- it limits the number of characters being printed,
- waits a 1/100th of a second so any keys still in the event library are removed before input is read and
- the timer for the selectionrectangle is continuously updated during the process.
- Params: lim:int = the number of characters input is allowed
- Returns:string the inputted string, trimmed of leading and tailing whitespace
- ]]--
- function readInput(lim)
- term.setCursorBlink(true)
- local inputString = ""
- if not lim or type(lim) ~= "number" or lim < 1 then lim = w - ox end
- local ox,oy = term.getCursorPos()
- --We only get input from the footer, so this is safe. Change if recycling
- term.setBackgroundColour(colours.lightGrey)
- term.setTextColour(colours.grey)
- term.write(string.rep(" ", lim))
- term.setCursorPos(ox, oy)
- --As events queue immediately, we may get an unwanted key... this will solve that problem
- local inputTimer = os.startTimer(0.01)
- local keysAllowed = false
- while true do
- local id,key = os.pullEvent()
- if keysAllowed then
- if id == "key" and key == 14 and #inputString > 0 then
- inputString = string.sub(inputString, 1, #inputString-1)
- term.setCursorPos(ox + #inputString,oy)
- term.write(" ")
- elseif id == "key" and key == 28 and inputString ~= string.rep(" ", #inputString) then
- break
- elseif id == "key" and key == keys.leftCtrl then
- return ""
- elseif id == "char" and #inputString < lim then
- inputString = inputString..key
- end
- end
- if id == "timer" then
- if key == inputTimer then
- keysAllowed = true
- else
- updateTimer(key)
- drawCanvas()
- drawInterface()
- term.setBackgroundColour(colours.lightGrey)
- term.setTextColour(colours.grey)
- end
- end
- term.setCursorPos(ox,oy)
- term.write(inputString)
- term.setCursorPos(ox + #inputString, oy)
- end
- while string.sub(inputString, 1, 1) == " " do
- inputString = string.sub(inputString, 2, #inputString)
- end
- while string.sub(inputString, #inputString, #inputString) == " " do
- inputString = string.sub(inputString, 1, #inputString-1)
- end
- term.setCursorBlink(false)
- return inputString
- end
- --[[The main function of the program, reads and handles all events and updates them accordingly. Mode changes,
- painting to the canvas and general selections are done here.
- Params: none
- Returns:nil
- ]]--
- local function handleEvents()
- recttimer = os.startTimer(0.5)
- while isRunning do
- drawCanvas()
- drawInterface()
- local id,p1,p2,p3 = os.pullEvent()
- if id=="timer" then
- updateTimer(p1)
- elseif id=="mouse_click" or id=="mouse_drag" then
- if p2 >=w-1 and p3 < #column+1 then
- if p1==1 then lSel = column[p3]
- else rSel = column[p3] end
- elseif p2 >=w-1 and p3==#column+1 then
- if p1==1 then lSel = nil
- else rSel = nil end
- elseif p2==w-1 and p3==h and animated then
- changeFrame(sFrame-1)
- elseif p2==w and p3==h and animated then
- changeFrame(sFrame+1)
- elseif p2 < w-10 and p3==h then
- inMenu = true
- drawMessage("")
- local sel = displayDropDown(1, h-1, ddModes)
- inMenu = false
- performSelection(sel)
- elseif p2 < w-1 and p3 <= h-1 then
- if state=="pippette" then
- if p1==1 then
- if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
- lSel = frames[sFrame][p3+sy][p2+sx]
- end
- elseif p1==2 then
- if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
- rSel = frames[sFrame][p3+sy][p2+sx]
- end
- end
- elseif state=="move" then
- updateImageLims(record)
- moveImage(p2,p3)
- elseif state=="flood" then
- if p1 == 1 and lSel and frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
- floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],lSel)
- elseif p1 == 2 and rSel and frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then
- floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],rSel)
- end
- elseif state=="corner select" then
- if not selectrect then
- selectrect = { x1=p2, x2=p2, y1=p3, y2=p3 }
- elseif selectrect.x1 ~= p2 and selectrect.y1 ~= p3 then
- if p2<selectrect.x1 then selectrect.x1 = p2
- else selectrect.x2 = p2 end
- if p3<selectrect.y1 then selectrect.y1 = p3
- else selectrect.y2 = p3 end
- state = "select"
- end
- elseif state=="select" then
- if p1 == 1 then
- local swidth = selectrect.x2 - selectrect.x1
- local sheight = selectrect.y2 - selectrect.y1
- selectrect.x1 = p2
- selectrect.y1 = p3
- selectrect.x2 = p2 + swidth
- selectrect.y2 = p3 + sheight
- elseif p1 == 2 and p2 < w-2 and p3 < h-1 then
- drawMessage("")
- performSelection(displayDropDown(p2, p3, srModes))
- inMenu = false
- end
- else
- local f,l = sFrame,sFrame
- if record then f,l = 1,framecount end
- local bwidth = 0
- if state == "brush" then bwidth = brushsize-1 end
- for i=f,l do
- for x = math.max(1,p2+sx-bwidth),p2+sx+bwidth do
- for y = math.max(1,p3+sy-bwidth), p3+sy+bwidth do
- if math.abs(x - (p2+sx)) + math.abs(y - (p3+sy)) <= bwidth then
- if not frames[i][y] then frames[i][y] = {} end
- if p1==1 then frames[i][y][x] = lSel
- else frames[i][y][x] = rSel end
- end
- end
- end
- end
- end
- end
- elseif id=="key" then
- if p1==keys.h then drawHelpScreen()
- elseif p1==keys.x then
- performSelection("cut")
- elseif p1==keys.c then
- performSelection("copy")
- elseif p1==keys.v then
- performSelection("paste")
- elseif p1==keys.z then
- performSelection("clear")
- elseif p1==keys.s then
- performSelection("select")
- elseif p1==keys.tab then
- performSelection("hide")
- elseif p1==keys.q then
- if lSel then alphaC = lSel end
- elseif p1==keys.w then
- if rSel then alphaC = rSel end
- elseif p1==keys.f then
- performSelection("flood")
- elseif p1==keys.b then
- performSelection("brush")
- elseif p1==keys.m then
- performSelection("move")
- elseif p1==keys.backslash and animated then
- performSelection("record")
- elseif p1==keys.p then
- performSelection("pippette")
- elseif p1==keys.g and animated then
- performSelection("changeframe")
- elseif p1==keys.period and animated then
- changeFrame(sFrame+1)
- elseif p1==keys.comma and animated then
- changeFrame(sFrame-1)
- elseif p1==keys.r and animated then
- performSelection("removeframes")
- elseif p1==keys.space and animated then
- performSelection("play")
- elseif p1==keys.left then
- if state == "move" then
- updateImageLims(record)
- moveImage(leflim-1,toplim)
- elseif state=="select" and selectrect.x1 > 1 then
- selectrect.x1 = selectrect.x1-1
- selectrect.x2 = selectrect.x2-1
- elseif sx > 0 then sx=sx-1 end
- elseif p1==keys.right then
- if state == "move" then
- updateImageLims(record)
- moveImage(leflim+1,toplim)
- elseif state=="select" then
- selectrect.x1 = selectrect.x1+1
- selectrect.x2 = selectrect.x2+1
- else sx=sx+1 end
- elseif p1==keys.up then
- if state == "move" then
- updateImageLims(record)
- moveImage(leflim,toplim-1)
- elseif state=="select" and selectrect.y1 > 1 then
- selectrect.y1 = selectrect.y1-1
- selectrect.y2 = selectrect.y2-1
- elseif sy > 0 then sy=sy-1 end
- elseif p1==keys.down then
- if state == "move" then
- updateImageLims(record)
- moveImage(leflim,toplim+1)
- elseif state=="select" then
- selectrect.y1 = selectrect.y1+1
- selectrect.y2 = selectrect.y2+1
- else sy=sy+1 end
- end
- elseif id=="char" and tonumber(p1) then
- if state=="brush" and tonumber(p1) > 1 then
- brushsize = tonumber(p1)
- elseif tonumber(p1) > 0 then
- changeFrame(tonumber(p1))
- end
- end
- end
- end
- --[[
- Section: Main
- ]]--
- --Taken almost directly from edit (for consistency)
- local tArgs = {...}
- local ca = 1
- if tArgs[ca] == "-a" then
- animated = true
- ca = ca + 1
- end
- if #tArgs < ca then
- print("Usage: npaintpro [-a] <path>")
- return
- end
- sPath = shell.resolve(tArgs[ca])
- local bReadOnly = fs.isReadOnly(sPath)
- if fs.exists(sPath) then
- if fs.isDir(sPath) then
- print("Cannot edit a directory.")
- return
- elseif string.find(sPath, ".nfp") ~= #sPath-3 and string.find(sPath, ".nfa") ~= #sPath-3 then
- print("Can only edit .nfp and nfa files:",string.find(sPath, ".nfp"),#sPath-3)
- return
- end
- if string.find(sPath, ".nfa") == #sPath-3 then
- animated = true
- end
- if string.find(sPath, ".nfp") == #sPath-3 and animated then
- print("Convert to nfa? Y/N")
- if string.find(string.lower(io.read()), "y") then
- local nsPath = string.sub(sPath, 1, #sPath-1).."a"
- fs.move(sPath, nsPath)
- sPath = nsPath
- else
- animated = false
- end
- end
- else
- if not animated and string.find(sPath, ".nfp") ~= #sPath-3 then
- sPath = sPath..".nfp"
- elseif animated and string.find(sPath, ".nfa") ~= #sPath-3 then
- sPath = sPath..".nfa"
- end
- end
- if not term.isColour() then
- print("For colour computers only")
- return
- end
- drawLogo()
- init()
- handleEvents()
- term.setBackgroundColour(colours.black)
- shell.run("clear")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement