--- An API for advanced systems which can draw pixels and lines, load and draw -- image files. You can use the `colors` API for easier color manipulation. -- -- @module paintutils local expect = dofile("rom/modules/main/cc/expect.lua").expect local function drawPixelInternal(xPos, yPos) term.setCursorPos(xPos, yPos) term.write(" ") end local tColourLookup = {} for n = 1, 16 do tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) end local function parseLine(tImageArg, sLine) local tLine = {} for x = 1, sLine:len() do tLine[x] = tColourLookup[string.byte(sLine, x, x)] or 0 end table.insert(tImageArg, tLine) end -- Sorts pairs of startX/startY/endX/endY such that the start is always the min local function sortCoords(startX, startY, endX, endY) local minX, maxX, minY, maxY if startX <= endX then minX, maxX = startX, endX else minX, maxX = endX, startX end if startY <= endY then minY, maxY = startY, endY else minY, maxY = endY, startY end return minX, maxX, minY, maxY end --- Parses an image from a multi-line string -- -- @tparam string image The string containing the raw-image data. -- @treturn table The parsed image data, suitable for use with -- @{paintutils.drawImage}. function parseImage(image) expect(1, image, "string") local tImage = {} for sLine in (image .. "\n"):gmatch("(.-)\n") do parseLine(tImage, sLine) end return tImage end --- Loads an image from a file. -- -- You can create a file suitable for being loaded using the `paint` program. -- -- @tparam string path The file to load. -- -- @treturn table|nil The parsed image data, suitable for use with -- @{paintutils.drawImage}, or `nil` if the file does not exist. -- @usage Load an image and draw it. -- -- local image = paintutils.loadImage("test-image.nfp") -- paintutils.drawImage(image, term.getCursorPos()) function loadImage(path) expect(1, path, "string") if fs.exists(path) then local file = io.open(path, "r") local sContent = file:read("*a") file:close() return parseImage(sContent) end return nil end --- Draws a single pixel to the current term at the specified position. -- -- Be warned, this may change the position of the cursor and the current -- background colour. You should not expect either to be preserved. -- -- @tparam number xPos The x position to draw at, where 1 is the far left. -- @tparam number yPos The y position to draw at, where 1 is the very top. -- @tparam[opt] number colour The @{colors|color} of this pixel. This will be -- the current background colour if not specified. function drawPixel(xPos, yPos, colour) expect(1, xPos, "number") expect(2, yPos, "number") expect(3, colour, "number", "nil") if colour then term.setBackgroundColor(colour) end return drawPixelInternal(xPos, yPos) end --- Draws a straight line from the start to end position. -- -- Be warned, this may change the position of the cursor and the current -- background colour. You should not expect either to be preserved. -- -- @tparam number startX The starting x position of the line. -- @tparam number startY The starting y position of the line. -- @tparam number endX The end x position of the line. -- @tparam number endY The end y position of the line. -- @tparam[opt] number colour The @{colors|color} of this pixel. This will be -- the current background colour if not specified. -- @usage paintutils.drawLine(2, 3, 30, 7, colors.red) function drawLine(startX, startY, endX, endY, colour) expect(1, startX, "number") expect(2, startY, "number") expect(3, endX, "number") expect(4, endY, "number") expect(5, colour, "number", "nil") startX = math.floor(startX) startY = math.floor(startY) endX = math.floor(endX) endY = math.floor(endY) if colour then term.setBackgroundColor(colour) end if startX == endX and startY == endY then drawPixelInternal(startX, startY) return end local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY) -- TODO: clip to screen rectangle? 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 drawPixelInternal(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 drawPixelInternal(math.floor(x + 0.5), y) x = x + dx end else for y = minY, maxY, -1 do drawPixelInternal(math.floor(x + 0.5), y) x = x - dx end end end end --- Draws the outline of a box on the current term from the specified start -- position to the specified end position. -- -- Be warned, this may change the position of the cursor and the current -- background colour. You should not expect either to be preserved. -- -- @tparam number startX The starting x position of the line. -- @tparam number startY The starting y position of the line. -- @tparam number endX The end x position of the line. -- @tparam number endY The end y position of the line. -- @tparam[opt] number colour The @{colors|color} of this pixel. This will be -- the current background colour if not specified. -- @usage paintutils.drawBox(2, 3, 30, 7, colors.red) function drawBox(startX, startY, endX, endY, nColour) expect(1, startX, "number") expect(2, startY, "number") expect(3, endX, "number") expect(4, endY, "number") expect(5, nColour, "number", "nil") startX = math.floor(startX) startY = math.floor(startY) endX = math.floor(endX) endY = math.floor(endY) if nColour then term.setBackgroundColor(nColour) -- Maintain legacy behaviour else nColour = term.getBackgroundColour() end local colourHex = colours.toBlit(nColour) if startX == endX and startY == endY then drawPixelInternal(startX, startY) return end local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY) local width = maxX - minX + 1 for y = minY, maxY do if y == minY or y == maxY then term.setCursorPos(minX, y) term.blit((" "):rep(width), colourHex:rep(width), colourHex:rep(width)) else term.setCursorPos(minX, y) term.blit(" ", colourHex, colourHex) term.setCursorPos(maxX, y) term.blit(" ", colourHex, colourHex) end end end --- Draws a filled box on the current term from the specified start position to -- the specified end position. -- -- Be warned, this may change the position of the cursor and the current -- background colour. You should not expect either to be preserved. -- -- @tparam number startX The starting x position of the line. -- @tparam number startY The starting y position of the line. -- @tparam number endX The end x position of the line. -- @tparam number endY The end y position of the line. -- @tparam[opt] number colour The @{colors|color} of this pixel. This will be -- the current background colour if not specified. -- @usage paintutils.drawFilledBox(2, 3, 30, 7, colors.red) function drawFilledBox(startX, startY, endX, endY, nColour) expect(1, startX, "number") expect(2, startY, "number") expect(3, endX, "number") expect(4, endY, "number") expect(5, nColour, "number", "nil") startX = math.floor(startX) startY = math.floor(startY) endX = math.floor(endX) endY = math.floor(endY) if nColour then term.setBackgroundColor(nColour) -- Maintain legacy behaviour else nColour = term.getBackgroundColour() end local colourHex = colours.toBlit(nColour) if startX == endX and startY == endY then drawPixelInternal(startX, startY) return end local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY) local width = maxX - minX + 1 for y = minY, maxY do term.setCursorPos(minX, y) term.blit((" "):rep(width), colourHex:rep(width), colourHex:rep(width)) end end --- Draw an image loaded by @{paintutils.parseImage} or @{paintutils.loadImage}. -- -- @tparam table image The parsed image data. -- @tparam number xPos The x position to start drawing at. -- @tparam number xPos The y position to start drawing at. function drawImage(image, xPos, yPos) expect(1, image, "table") expect(2, xPos, "number") expect(3, yPos, "number") for y = 1, #image do local tLine = image[y] for x = 1, #tLine do if tLine[x] > 0 then term.setBackgroundColor(tLine[x]) drawPixelInternal(x + xPos - 1, y + yPos - 1) end end end end