Files
Basalt2/src/elements/Image.lua
2025-03-14 17:00:13 +01:00

456 lines
15 KiB
Lua

local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement")
local tHex = require("libraries/colorHex")
---@configDescription An element that displays an image in bimg format
---@configDefault false
--- This is the Image element class which can be used to display bimg formatted images.
--- Bimg is a universal ComputerCraft image format.
--- See: https://github.com/SkyTheCodeMaster/bimg
---@class Image : VisualElement
local Image = setmetatable({}, VisualElement)
Image.__index = Image
---@property bimg table {} The bimg image data
Image.defineProperty(Image, "bimg", {default = {{}}, type = "table", canTriggerRender = true})
---@property currentFrame number 1 Current animation frame
Image.defineProperty(Image, "currentFrame", {default = 1, type = "number", canTriggerRender = true})
---@property autoResize boolean false Whether to automatically resize the image when content exceeds bounds
Image.defineProperty(Image, "autoResize", {default = false, type = "boolean"})
---@property offsetX number 0 Horizontal offset for viewing larger images
Image.defineProperty(Image, "offsetX", {default = 0, type = "number", canTriggerRender = true})
---@property offsetY number 0 Vertical offset for viewing larger images
Image.defineProperty(Image, "offsetY", {default = 0, type = "number", canTriggerRender = true})
---@combinedProperty offset {offsetX offsetY} Combined property for offsetX and offsetY
Image.combineProperties(Image, "offset", "offsetX", "offsetY")
--- Creates a new Image instance
--- @shortDescription Creates a new Image instance
--- @return Image self The newly created Image instance
--- @private
function Image.new()
local self = setmetatable({}, Image):__init()
self.set("width", 12)
self.set("height", 6)
self.set("background", colors.black)
self.set("z", 5)
return self
end
--- @shortDescription Initializes the Image instance
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return Image self The initialized instance
--- @protected
function Image:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Image")
return self
end
--- Resizes the image to the specified width and height
--- @shortDescription Resizes the image to the specified width and height
--- @param width number The new width of the image
--- @param height number The new height of the image
--- @return Image self The Image instance
function Image:resizeImage(width, height)
local frames = self.get("bimg")
for frameIndex, frame in ipairs(frames) do
local newFrame = {}
for y = 1, height do
local text = string.rep(" ", width)
local fg = string.rep("f", width)
local bg = string.rep("0", width)
if frame[y] and frame[y][1] then
local oldText = frame[y][1]
local oldFg = frame[y][2]
local oldBg = frame[y][3]
text = (oldText .. string.rep(" ", width)):sub(1, width)
fg = (oldFg .. string.rep("f", width)):sub(1, width)
bg = (oldBg .. string.rep("0", width)):sub(1, width)
end
newFrame[y] = {text, fg, bg}
end
frames[frameIndex] = newFrame
end
self:updateRender()
return self
end
--- Gets the size of the image
--- @shortDescription Gets the size of the image
--- @return number width The width of the image
--- @return number height The height of the image
function Image:getImageSize()
local bimg = self.get("bimg")
if not bimg[1] or not bimg[1][1] then return 0, 0 end
return #bimg[1][1][1], #bimg[1]
end
--- Gets pixel information at position
--- @shortDescription Gets pixel information at position
--- @param x number X position
--- @param y number Y position
--- @return number? fg Foreground color
--- @return number? bg Background color
--- @return string? char Character at position
function Image:getPixelData(x, y)
local frame = self.get("bimg")[self.get("currentFrame")]
if not frame or not frame[y] then return end
local text = frame[y][1]
local fg = frame[y][2]
local bg = frame[y][3]
if not text or not fg or not bg then return end
local fgColor = tonumber(fg:sub(x,x), 16)
local bgColor = tonumber(bg:sub(x,x), 16)
local char = text:sub(x,x)
return fgColor, bgColor, char
end
local function ensureFrame(self, y)
local frame = self.get("bimg")[self.get("currentFrame")]
if not frame then
frame = {}
self.get("bimg")[self.get("currentFrame")] = frame
end
if not frame[y] then
frame[y] = {"", "", ""}
end
return frame
end
local function updateFrameSize(self, neededWidth, neededHeight)
if not self.get("autoResize") then return end
local frames = self.get("bimg")
local maxWidth = neededWidth
local maxHeight = neededHeight
for _, frame in ipairs(frames) do
for y, line in pairs(frame) do
maxWidth = math.max(maxWidth, #line[1])
maxHeight = math.max(maxHeight, y)
end
end
for _, frame in ipairs(frames) do
for y = 1, maxHeight do
if not frame[y] then
frame[y] = {"", "", ""}
end
local line = frame[y]
while #line[1] < maxWidth do line[1] = line[1] .. " " end
while #line[2] < maxWidth do line[2] = line[2] .. "f" end
while #line[3] < maxWidth do line[3] = line[3] .. "0" end
end
end
end
--- Sets the text at the specified position
--- @shortDescription Sets the text at the specified position
--- @param x number The x position
--- @param y number The y position
--- @param text string The text to set
--- @return Image self The Image instance
function Image:setText(x, y, text)
if type(text) ~= "string" or #text < 1 or x < 1 or y < 1 then return self end
if not self.get("autoResize")then
local imgWidth, imgHeight = self:getImageSize()
if y > imgHeight then return self end
end
local frame = ensureFrame(self, y)
if self.get("autoResize") then
updateFrameSize(self, x + #text - 1, y)
else
local maxLen = #frame[y][1]
if x > maxLen then return self end
text = text:sub(1, maxLen - x + 1)
end
local currentLine = frame[y][1]
frame[y][1] = currentLine:sub(1, x-1) .. text .. currentLine:sub(x + #text)
self:updateRender()
return self
end
--- Gets the text at the specified position
--- @shortDescription Gets the text at the specified position
--- @param x number The x position
--- @param y number The y position
--- @param length number The length of the text to get
--- @return string text The text at the specified position
function Image:getText(x, y, length)
if not x or not y then return "" end
local frame = self.get("bimg")[self.get("currentFrame")]
if not frame or not frame[y] then return "" end
local text = frame[y][1]
if not text then return "" end
if length then
return text:sub(x, x + length - 1)
else
return text:sub(x, x)
end
end
--- Sets the foreground color at the specified position
--- @shortDescription Sets the foreground color at the specified position
--- @param x number The x position
--- @param y number The y position
--- @param pattern string The foreground color pattern
--- @return Image self The Image instance
function Image:setFg(x, y, pattern)
if type(pattern) ~= "string" or #pattern < 1 or x < 1 or y < 1 then return self end
if not self.get("autoResize")then
local imgWidth, imgHeight = self:getImageSize()
if y > imgHeight then return self end
end
local frame = ensureFrame(self, y)
if self.get("autoResize") then
updateFrameSize(self, x + #pattern - 1, y)
else
local maxLen = #frame[y][2]
if x > maxLen then return self end
pattern = pattern:sub(1, maxLen - x + 1)
end
local currentFg = frame[y][2]
frame[y][2] = currentFg:sub(1, x-1) .. pattern .. currentFg:sub(x + #pattern)
self:updateRender()
return self
end
--- Gets the foreground color at the specified position
--- @shortDescription Gets the foreground color at the specified position
--- @param x number The x position
--- @param y number The y position
--- @param length number The length of the foreground color pattern to get
--- @return string fg The foreground color pattern
function Image:getFg(x, y, length)
if not x or not y then return "" end
local frame = self.get("bimg")[self.get("currentFrame")]
if not frame or not frame[y] then return "" end
local fg = frame[y][2]
if not fg then return "" end
if length then
return fg:sub(x, x + length - 1)
else
return fg:sub(x)
end
end
--- Sets the background color at the specified position
--- @shortDescription Sets the background color at the specified position
--- @param x number The x position
--- @param y number The y position
--- @param pattern string The background color pattern
--- @return Image self The Image instance
function Image:setBg(x, y, pattern)
if type(pattern) ~= "string" or #pattern < 1 or x < 1 or y < 1 then return self end
if not self.get("autoResize")then
local imgWidth, imgHeight = self:getImageSize()
if y > imgHeight then return self end
end
local frame = ensureFrame(self, y)
if self.get("autoResize") then
updateFrameSize(self, x + #pattern - 1, y)
else
local maxLen = #frame[y][3]
if x > maxLen then return self end
pattern = pattern:sub(1, maxLen - x + 1)
end
local currentBg = frame[y][3]
frame[y][3] = currentBg:sub(1, x-1) .. pattern .. currentBg:sub(x + #pattern)
self:updateRender()
return self
end
--- Gets the background color at the specified position
--- @shortDescription Gets the background color at the specified position
--- @param x number The x position
--- @param y number The y position
--- @param length number The length of the background color pattern to get
--- @return string bg The background color pattern
function Image:getBg(x, y, length)
if not x or not y then return "" end
local frame = self.get("bimg")[self.get("currentFrame")]
if not frame or not frame[y] then return "" end
local bg = frame[y][3]
if not bg then return "" end
if length then
return bg:sub(x, x + length - 1)
else
return bg:sub(x)
end
end
--- Sets the pixel at the specified position
--- @shortDescription Sets the pixel at the specified position
--- @param x number The x position
--- @param y number The y position
--- @param char string The character to set
--- @param fg string The foreground color pattern
--- @param bg string The background color pattern
--- @return Image self The Image instance
function Image:setPixel(x, y, char, fg, bg)
if char then self:setText(x, y, char) end
if fg then self:setFg(x, y, fg) end
if bg then self:setBg(x, y, bg) end
return self
end
--- Advances to the next frame in the animation
--- @shortDescription Advances to the next frame in the animation
--- @return Image self The Image instance
function Image:nextFrame()
if not self.get("bimg").animation then return self end
local frames = self.get("bimg")
local current = self.get("currentFrame")
local next = current + 1
if next > #frames then next = 1 end
self.set("currentFrame", next)
return self
end
--- Adds a new frame to the image
--- @shortDescription Adds a new frame to the image
--- @return Image self The Image instance
function Image:addFrame()
local frames = self.get("bimg")
local width = frames.width or #frames[1][1][1]
local height = frames.height or #frames[1]
local frame = {}
local text = string.rep(" ", width)
local fg = string.rep("f", width)
local bg = string.rep("0", width)
for y = 1, height do
frame[y] = {text, fg, bg}
end
table.insert(frames, frame)
return self
end
--- Updates the specified frame with the provided data
--- @shortDescription Updates the specified frame with the provided data
--- @param frameIndex number The index of the frame to update
--- @param frame table The new frame data
--- @return Image self The Image instance
function Image:updateFrame(frameIndex, frame)
local frames = self.get("bimg")
frames[frameIndex] = frame
self:updateRender()
return self
end
--- Gets the specified frame
--- @shortDescription Gets the specified frame
--- @param frameIndex number The index of the frame to get
--- @return table frame The frame data
function Image:getFrame(frameIndex)
local frames = self.get("bimg")
return frames[frameIndex or self.get("currentFrame")]
end
--- Gets the metadata of the image
--- @shortDescription Gets the metadata of the image
--- @return table metadata The metadata of the image
function Image:getMetadata()
local metadata = {}
local bimg = self.get("bimg")
for k,v in pairs(bimg)do
if(type(v)=="string")then
metadata[k] = v
end
end
return metadata
end
--- Sets the metadata of the image
--- @shortDescription Sets the metadata of the image
--- @param key string The key of the metadata to set
--- @param value string The value of the metadata to set
--- @return Image self The Image instance
function Image:setMetadata(key, value)
if(type(key)=="table")then
for k,v in pairs(key)do
self:setMetadata(k, v)
end
return self
end
local bimg = self.get("bimg")
if(type(value)=="string")then
bimg[key] = value
end
return self
end
--- @shortDescription Renders the Image
--- @protected
function Image:render()
VisualElement.render(self)
local frame = self.get("bimg")[self.get("currentFrame")]
if not frame then return end
local offsetX = self.get("offsetX")
local offsetY = self.get("offsetY")
local elementWidth = self.get("width")
local elementHeight = self.get("height")
for y = 1, elementHeight do
local frameY = y + offsetY
local line = frame[frameY]
if line then
local text = line[1]
local fg = line[2]
local bg = line[3]
if text and fg and bg then
local remainingWidth = elementWidth - math.max(0, offsetX)
if remainingWidth > 0 then
if offsetX < 0 then
local startPos = math.abs(offsetX) + 1
text = text:sub(startPos)
fg = fg:sub(startPos)
bg = bg:sub(startPos)
end
text = text:sub(1, remainingWidth)
fg = fg:sub(1, remainingWidth)
bg = bg:sub(1, remainingWidth)
self:blit(math.max(1, 1 + offsetX), y, text, fg, bg)
end
end
end
end
end
return Image