Files
FlexLove/modules/ImageScaler.lua
2025-12-03 22:19:27 -05:00

175 lines
5.6 KiB
Lua

-- ====================
-- ImageScaler
-- ====================
local ImageScaler = {}
-- ErrorHandler will be injected via init
local ErrorHandler = nil
--- Initialize ImageScaler with dependencies
---@param deps table Dependencies table with ErrorHandler
function ImageScaler.init(deps)
if deps and deps.ErrorHandler then
ErrorHandler = deps.ErrorHandler
end
end
--- Scale an ImageData region using nearest-neighbor sampling
--- Produces sharp, pixelated scaling - ideal for pixel art
---@param sourceImageData love.ImageData -- Source image data
---@param srcX number -- Source region X (0-based)
---@param srcY number -- Source region Y (0-based)
---@param srcW number -- Source region width
---@param srcH number -- Source region height
---@param destW number -- Destination width
---@param destH number -- Destination height
---@return love.ImageData -- Scaled image data
function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
if not sourceImageData then
ErrorHandler:error("ImageScaler", "VAL_001", {
parameter = "sourceImageData"
})
end
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
ErrorHandler:warn("ImageScaler", "VAL_002", {
srcW = srcW,
srcH = srcH,
destW = destW,
destH = destH,
fallback = "1x1 transparent image"
})
-- Return a minimal 1x1 transparent image as fallback
local fallbackImageData = love.image.newImageData(1, 1)
fallbackImageData:setPixel(0, 0, 0, 0, 0, 0)
return fallbackImageData
end
-- Create destination ImageData
local destImageData = love.image.newImageData(destW, destH)
-- Calculate scale ratios (cached outside loops for performance)
local scaleX = srcW / destW
local scaleY = srcH / destH
-- Nearest-neighbor sampling
for destY = 0, destH - 1 do
for destX = 0, destW - 1 do
-- Calculate source pixel coordinates using floor (nearest-neighbor)
local srcPixelX = math.floor(destX * scaleX) + srcX
local srcPixelY = math.floor(destY * scaleY) + srcY
-- Clamp to source bounds (safety check)
srcPixelX = math.min(srcPixelX, srcX + srcW - 1)
srcPixelY = math.min(srcPixelY, srcY + srcH - 1)
-- Sample source pixel
local r, g, b, a = sourceImageData:getPixel(srcPixelX, srcPixelY)
-- Write to destination
destImageData:setPixel(destX, destY, r, g, b, a)
end
end
return destImageData
end
--- Linear interpolation helper
--- Blends between two values based on interpolation factor
---@param a number -- Start value
---@param b number -- End value
---@param t number -- Interpolation factor [0, 1]
---@return number -- Interpolated value
local function lerp(a, b, t)
return a + (b - a) * t
end
--- Scale an ImageData region using bilinear interpolation
--- Produces smooth, filtered scaling - ideal for high-quality upscaling
---@param sourceImageData love.ImageData -- Source image data
---@param srcX number -- Source region X (0-based)
---@param srcY number -- Source region Y (0-based)
---@param srcW number -- Source region width
---@param srcH number -- Source region height
---@param destW number -- Destination width
---@param destH number -- Destination height
---@return love.ImageData -- Scaled image data
function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
if not sourceImageData then
ErrorHandler:error("ImageScaler", "VAL_001", {
parameter = "sourceImageData"
})
end
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
ErrorHandler:warn("ImageScaler", "VAL_002", {
srcW = srcW,
srcH = srcH,
destW = destW,
destH = destH,
fallback = "1x1 transparent image"
})
-- Return a minimal 1x1 transparent image as fallback
local fallbackImageData = love.image.newImageData(1, 1)
fallbackImageData:setPixel(0, 0, 0, 0, 0, 0)
return fallbackImageData
end
-- Create destination ImageData
local destImageData = love.image.newImageData(destW, destH)
-- Calculate scale ratios
local scaleX = srcW / destW
local scaleY = srcH / destH
-- Bilinear interpolation
for destY = 0, destH - 1 do
for destX = 0, destW - 1 do
-- Calculate fractional source position
local srcXf = destX * scaleX
local srcYf = destY * scaleY
-- Get integer coordinates for 2x2 sampling grid
local x0 = math.floor(srcXf)
local y0 = math.floor(srcYf)
local x1 = math.min(x0 + 1, srcW - 1)
local y1 = math.min(y0 + 1, srcH - 1)
-- Get fractional parts for interpolation
local fx = srcXf - x0
local fy = srcYf - y0
-- Sample 4 neighboring pixels (with source offset)
local r00, g00, b00, a00 = sourceImageData:getPixel(srcX + x0, srcY + y0)
local r10, g10, b10, a10 = sourceImageData:getPixel(srcX + x1, srcY + y0)
local r01, g01, b01, a01 = sourceImageData:getPixel(srcX + x0, srcY + y1)
local r11, g11, b11, a11 = sourceImageData:getPixel(srcX + x1, srcY + y1)
-- Interpolate horizontally (top and bottom rows)
local rTop = lerp(r00, r10, fx)
local gTop = lerp(g00, g10, fx)
local bTop = lerp(b00, b10, fx)
local aTop = lerp(a00, a10, fx)
local rBottom = lerp(r01, r11, fx)
local gBottom = lerp(g01, g11, fx)
local bBottom = lerp(b01, b11, fx)
local aBottom = lerp(a01, a11, fx)
-- Interpolate vertically (final result)
local r = lerp(rTop, rBottom, fy)
local g = lerp(gTop, gBottom, fy)
local b = lerp(bTop, bBottom, fy)
local a = lerp(aTop, aBottom, fy)
-- Write to destination
destImageData:setPixel(destX, destY, r, g, b, a)
end
end
return destImageData
end
return ImageScaler