3918 lines
114 KiB
Lua
3918 lines
114 KiB
Lua
-- L5 0.1.5 (c) Lee Tusman and Contributors GNU LGPL2.1
|
|
VERSION = '0.1.5'
|
|
|
|
-- Override love.run() - adds double buffering and custom events
|
|
function love.run()
|
|
defaults()
|
|
|
|
define_env_globals()
|
|
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
|
|
if love.timer then love.timer.step() end
|
|
local dt = 0
|
|
local setupComplete = false
|
|
|
|
-- Main loop
|
|
return function()
|
|
-- Process events
|
|
if love.event then
|
|
love.event.pump()
|
|
for name, a,b,c,d,e,f in love.event.poll() do
|
|
if name == "quit" then
|
|
if not love.quit or not love.quit() then
|
|
return a or 0
|
|
end
|
|
end
|
|
|
|
-- Handle mouse events - store them for drawing phase
|
|
if name == "mousepressed" then
|
|
-- a = x, b = y, c = button, d = istouch, e = presses
|
|
L5_env.pendingMouseClicked = {x = a, y = b, button = c}
|
|
elseif name == "mousereleased" then
|
|
-- a = x, b = y, c = button, d = istouch, e = presses
|
|
L5_env.pendingMouseReleased = {x = a, y = b, button = c}
|
|
end
|
|
|
|
-- Handle other events through the default handlers
|
|
if love.handlers[name] then
|
|
love.handlers[name](a,b,c,d,e,f)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Update dt
|
|
if love.timer then dt = love.timer.step() end
|
|
|
|
-- Update
|
|
if love.update then love.update(dt) end
|
|
|
|
-- Draw with double buffering
|
|
if love.graphics and love.graphics.isActive() then
|
|
love.graphics.origin()
|
|
|
|
-- Set render target to back buffer
|
|
if L5_env.backBuffer then
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
end
|
|
|
|
-- Only clear if background() was called this frame
|
|
if L5_env.clearscreen then
|
|
-- background() already cleared with the right color
|
|
L5_env.clearscreen = false
|
|
end
|
|
|
|
-- Draw current frame
|
|
-- Run setup() once in the drawing context
|
|
if not setupComplete and setup then
|
|
setup()
|
|
setupComplete = true
|
|
else
|
|
if love.draw then love.draw() end
|
|
end
|
|
|
|
-- Reset to screen and draw the back buffer
|
|
love.graphics.setCanvas()
|
|
if L5_env.backBuffer then
|
|
-- Save current color
|
|
local r, g, b, a = love.graphics.getColor()
|
|
|
|
-- Set to white (no tint) when drawing the canvas to screen
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
|
|
if L5_env.filterOn then
|
|
if L5_env.filter == "blur_twopass" then
|
|
-- Two-pass blur requires intermediate canvas
|
|
if not L5_env.blurTempCanvas or
|
|
L5_env.blurTempCanvas:getWidth() ~= love.graphics.getWidth() or
|
|
L5_env.blurTempCanvas:getHeight() ~= love.graphics.getHeight() then
|
|
L5_env.blurTempCanvas = love.graphics.newCanvas()
|
|
end
|
|
|
|
-- Pass 1: Horizontal blur to temp canvas
|
|
love.graphics.setCanvas(L5_env.blurTempCanvas)
|
|
love.graphics.clear()
|
|
love.graphics.setShader(L5_filter.blur_horizontal)
|
|
love.graphics.draw(L5_env.backBuffer, 0, 0)
|
|
|
|
-- Pass 2: Vertical blur to screen
|
|
love.graphics.setCanvas()
|
|
love.graphics.setShader(L5_filter.blur_vertical)
|
|
love.graphics.draw(L5_env.blurTempCanvas, 0, 0)
|
|
love.graphics.setShader()
|
|
else
|
|
-- Single-pass filter
|
|
love.graphics.setShader(L5_env.filter)
|
|
love.graphics.draw(L5_env.backBuffer, 0, 0)
|
|
love.graphics.setShader()
|
|
end
|
|
L5_env.filterOn = false
|
|
else
|
|
-- No filter, just draw normally
|
|
love.graphics.draw(L5_env.backBuffer, 0, 0)
|
|
end
|
|
|
|
-- Restore color (after drawing the canvas)
|
|
love.graphics.setColor(r, g, b, a)
|
|
love.graphics.present()
|
|
end
|
|
|
|
if love.timer then
|
|
if L5_env.framerate then --user-specified framerate
|
|
love.timer.sleep(1/L5_env.framerate)
|
|
else --default framerate
|
|
love.timer.sleep(0.001)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function love.load()
|
|
love.window.setVSync(1)
|
|
love.math.setRandomSeed(os.time())
|
|
|
|
displayWidth, displayHeight = love.window.getDesktopDimensions()
|
|
|
|
-- create default-size buffers. will be recreated again if size() or fullscreen(true) called
|
|
local w, h = love.graphics.getDimensions()
|
|
|
|
-- Create double buffers
|
|
L5_env.backBuffer = love.graphics.newCanvas(w, h)
|
|
L5_env.frontBuffer = love.graphics.newCanvas(w, h)
|
|
|
|
-- Clear both buffers initially
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
love.graphics.clear(0.5, 0.5, 0.5, 1) -- gray background
|
|
love.graphics.setCanvas(L5_env.frontBuffer)
|
|
love.graphics.clear(0.5, 0.5, 0.5, 1) -- gray background
|
|
love.graphics.setCanvas()
|
|
|
|
initShaderDefaults()
|
|
|
|
stroke(0)
|
|
fill(255)
|
|
end
|
|
|
|
function love.update(dt)
|
|
mouseX, mouseY = love.mouse.getPosition()
|
|
movedX=mouseX-pmouseX
|
|
movedY=mouseY-pmouseY
|
|
deltaTime = dt * 1000
|
|
key = updateLastKeyPressed()
|
|
|
|
-- Update looping videos
|
|
-- Note: Videos with audio tracks may experience sync issues when looping
|
|
-- This is a LÖVE limitation with video/audio stream synchronization
|
|
if L5_env.videos then
|
|
for _, v in ipairs(L5_env.videos) do
|
|
if v._shouldLoop and not v._manuallyPaused and not v._video:isPlaying() then
|
|
v._video:rewind()
|
|
v._video:play()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Optional update (not typically Processing-like but available)
|
|
if update ~= nil then update() end
|
|
end
|
|
|
|
function love.draw()
|
|
-- checking user events happens regardless of whether the user draw() function is currently looping
|
|
local isPressed = love.mouse.isDown(1) or love.mouse.isDown(2) or love.mouse.isDown(3)
|
|
|
|
if isPressed and not L5_env.wasPressed then
|
|
-- Mouse was just pressed this frame
|
|
if mousePressed ~= nil then mousePressed() end
|
|
mouseIsPressed = true
|
|
elseif not isPressed and L5_env.wasPressed then
|
|
-- Mouse was just released this frame
|
|
if mouseReleased ~= nil then mouseReleased() end
|
|
if mouseClicked ~= nil then mouseClicked() end -- Run immediately after mouseReleased
|
|
mouseIsPressed = false
|
|
elseif isPressed then
|
|
-- Still pressed - only call mouseDragged if mouse actually moved
|
|
if L5_env.mouseWasMoved then
|
|
if mouseDragged ~= nil then mouseDragged() end
|
|
L5_env.mouseWasMoved = false -- Clear the flag
|
|
end
|
|
mouseIsPressed = true
|
|
else
|
|
mouseIsPressed = false
|
|
end
|
|
|
|
L5_env.wasPressed = isPressed
|
|
|
|
-- Check for keyboard events in the draw cycle
|
|
if L5_env.keyWasPressed then
|
|
if keyPressed ~= nil then keyPressed() end
|
|
L5_env.keyWasPressed = false
|
|
end
|
|
|
|
if L5_env.keyWasReleased then
|
|
if keyReleased ~= nil then keyReleased() end
|
|
L5_env.keyWasReleased = false
|
|
end
|
|
|
|
if L5_env.keyWasTyped then
|
|
local savedKey = key
|
|
key = L5_env.typedKey -- Temporarily use the typed character
|
|
if keyTyped ~= nil then keyTyped() end
|
|
key = savedKey -- Restore
|
|
L5_env.keyWasTyped = false
|
|
L5_env.typedKey = nil
|
|
end
|
|
|
|
-- Check for mouse events in draw cycle
|
|
-- Only call mouseMoved if mouse button is NOT pressed
|
|
if L5_env.mouseWasMoved and not isPressed then
|
|
if mouseMoved ~= nil then mouseMoved() end
|
|
L5_env.mouseWasMoved = false
|
|
elseif L5_env.mouseWasMoved and isPressed then
|
|
-- Clear the flag even if we don't call mouseMoved
|
|
-- (mouseDragged already handled above)
|
|
L5_env.mouseWasMoved = false
|
|
end
|
|
|
|
if L5_env.wheelWasMoved then
|
|
if mouseWheel ~= nil then
|
|
mouseWheel(L5_env.wheelY or 0)
|
|
end
|
|
L5_env.wheelWasMoved = false
|
|
L5_env.wheelX = nil
|
|
L5_env.wheelY = nil
|
|
end
|
|
|
|
-- only run if user draw() function is looping
|
|
if L5_env.drawing then
|
|
frameCount = frameCount + 1
|
|
|
|
-- Reset transformation matrix to identity at start of each frame
|
|
love.graphics.origin()
|
|
love.graphics.push()
|
|
|
|
-- Call user draw function
|
|
if draw ~= nil then draw() end
|
|
|
|
pmouseX, pmouseY = mouseX,mouseY
|
|
|
|
love.graphics.pop()
|
|
end
|
|
|
|
-- Draw print buffer on top of window, if on
|
|
if L5_env.showPrintBuffer and #L5_env.printBuffer > 0 then
|
|
love.graphics.push()
|
|
love.graphics.origin()
|
|
|
|
-- Save user's current font and switch to default
|
|
local userFont = love.graphics.getFont()
|
|
love.graphics.setFont(L5_env.printFont or L5_env.defaultFont)
|
|
|
|
-- Calculate max lines that fit on screen
|
|
local maxLines = math.floor((height - 10) / L5_env.printLineHeight)
|
|
|
|
-- Trim buffer to only show lines that fit
|
|
local displayBuffer = {}
|
|
local startIdx = math.max(1, #L5_env.printBuffer - maxLines + 1)
|
|
for i = startIdx, #L5_env.printBuffer do
|
|
table.insert(displayBuffer, L5_env.printBuffer[i])
|
|
end
|
|
|
|
-- Get the font to measure text width
|
|
local font = love.graphics.getFont()
|
|
|
|
-- Draw each line with its own background
|
|
local y = 5
|
|
for _, line in ipairs(displayBuffer) do
|
|
-- Measure the actual width of this line of text
|
|
local textWidth = font:getWidth(line)
|
|
|
|
-- Draw background rectangle just for this line
|
|
love.graphics.setColor(0, 0, 0, 0.7)
|
|
love.graphics.rectangle('fill', 3, y, textWidth + 6, L5_env.printLineHeight)
|
|
|
|
-- Draw the text on top
|
|
love.graphics.setColor(1, 1, 1)
|
|
love.graphics.print(line, 5, y)
|
|
|
|
y = y + L5_env.printLineHeight
|
|
end
|
|
|
|
-- Restore user's font
|
|
love.graphics.setFont(userFont)
|
|
|
|
love.graphics.pop()
|
|
end
|
|
end
|
|
|
|
function love.mousepressed(_x, _y, button, istouch, presses)
|
|
--turned off so as not to duplicate event handling running twice
|
|
--if mousePressed ~= nil then mousePressed() end
|
|
if button==1 then
|
|
mouseButton=LEFT
|
|
elseif button==2 then
|
|
mouseButton=RIGHT
|
|
elseif button==3 then
|
|
mouseButton=CENTER
|
|
end
|
|
end
|
|
|
|
function love.mousereleased( x, y, button, istouch, presses )
|
|
--if mouseClicked ~= nil then mouseClicked() end
|
|
--if focused and mouseReleased ~= nil then mouseReleased() end
|
|
end
|
|
|
|
function love.wheelmoved(_x,_y)
|
|
L5_env.wheelWasMoved = true
|
|
L5_env.wheelX = _x
|
|
L5_env.wheelY = _y
|
|
return _x, _y
|
|
end
|
|
|
|
function love.mousemoved(x,y,dx,dy,istouch)
|
|
L5_env.mouseWasMoved = true
|
|
end
|
|
|
|
function love.keypressed(k, scancode, isrepeat)
|
|
-- Add key to pressed keys table
|
|
L5_env.pressedKeys[k] = true
|
|
|
|
key = k
|
|
keyCode = love.keyboard.getScancodeFromKey(k)
|
|
L5_env.keyWasPressed = true
|
|
keyIsPressed = true
|
|
end
|
|
|
|
function love.keyreleased(k)
|
|
-- Remove key from pressed keys table
|
|
L5_env.pressedKeys[k] = nil
|
|
|
|
key = k
|
|
keyCode = love.keyboard.getScancodeFromKey(k)
|
|
L5_env.keyWasReleased = true
|
|
|
|
-- Only set keyIsPressed to false if no keys are pressed
|
|
local anyKeyPressed = false
|
|
for _ in pairs(L5_env.pressedKeys) do
|
|
anyKeyPressed = true
|
|
break
|
|
end
|
|
keyIsPressed = anyKeyPressed
|
|
end
|
|
|
|
function love.textinput(_text)
|
|
key = _text
|
|
L5_env.typedKey = _text
|
|
L5_env.keyWasTyped = true
|
|
end
|
|
|
|
function love.resize(w, h)
|
|
-- Recreate buffers when window is resized at density-scaled resolution
|
|
if L5_env.backBuffer then L5_env.backBuffer:release() end
|
|
if L5_env.frontBuffer then L5_env.frontBuffer:release() end
|
|
|
|
L5_env.backBuffer = love.graphics.newCanvas(w, h)
|
|
L5_env.frontBuffer = love.graphics.newCanvas(w, h )
|
|
|
|
-- Clear new buffers and apply scaling
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
love.graphics.clear(0.5, 0.5, 0.5, 1)
|
|
|
|
love.graphics.setCanvas(L5_env.frontBuffer)
|
|
love.graphics.clear(0.5, 0.5, 0.5, 1)
|
|
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
|
|
-- Update global width/height to logical size
|
|
width, height = w, h
|
|
|
|
-- Call user's windowResized function if it exists
|
|
if windowResized then
|
|
windowResized()
|
|
end
|
|
end
|
|
|
|
function love.focus(_focused)
|
|
focused = _focused
|
|
end
|
|
|
|
------------------- CUSTOM FUNCTIONS -----------------
|
|
function printToScreen(textSize)
|
|
L5_env.showPrintBuffer = true
|
|
|
|
textSize = textSize or 16
|
|
|
|
L5_env.printFont = love.graphics.newFont(textSize)
|
|
L5_env.printLineHeight = L5_env.printFont:getHeight()
|
|
|
|
end
|
|
|
|
function size(_w, _h)
|
|
-- must clear canvas before setMode
|
|
love.graphics.setCanvas()
|
|
|
|
love.window.setMode(_w, _h)
|
|
|
|
-- Recreate buffers for new size
|
|
if L5_env.backBuffer then L5_env.backBuffer:release() end
|
|
if L5_env.frontBuffer then L5_env.frontBuffer:release() end
|
|
|
|
L5_env.backBuffer = love.graphics.newCanvas(_w, _h)
|
|
L5_env.frontBuffer = love.graphics.newCanvas(_w, _h)
|
|
|
|
-- Clear new buffers
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
love.graphics.clear(0.5, 0.5, 0.5, 1)
|
|
love.graphics.setCanvas(L5_env.frontBuffer)
|
|
love.graphics.clear(0.5, 0.5, 0.5, 1)
|
|
|
|
-- Set back to back buffer for continued drawing
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
|
|
width, height = love.graphics.getDimensions()
|
|
end
|
|
|
|
function fullscreen(display)
|
|
display = display or 1
|
|
|
|
love.graphics.setCanvas()
|
|
|
|
local displays = love.window.getDisplayCount()
|
|
if display > displays then
|
|
display = 1
|
|
end
|
|
|
|
-- Get dimensions for the specified display
|
|
local w, h = love.window.getDesktopDimensions(display)
|
|
|
|
-- First, create a windowed mode on that display
|
|
love.window.setMode(w, h, {fullscreen = false})
|
|
|
|
-- Position the window on the target display
|
|
local xPos = 0
|
|
for i = 1, display - 1 do
|
|
local dw, _ = love.window.getDesktopDimensions(i)
|
|
xPos = xPos + dw
|
|
end
|
|
love.window.setPosition(xPos, 0)
|
|
|
|
-- Small delay for Windows to process window positioning
|
|
if love.timer then love.timer.sleep(0.1) end
|
|
|
|
-- Now go fullscreen
|
|
local success, err = pcall(function()
|
|
love.window.setFullscreen(true, "desktop")
|
|
end)
|
|
|
|
if not success then
|
|
print("Fullscreen error:", err)
|
|
return
|
|
end
|
|
|
|
-- Release old canvases
|
|
if L5_env.backBuffer then
|
|
pcall(function() L5_env.backBuffer:release() end)
|
|
end
|
|
if L5_env.frontBuffer then
|
|
pcall(function() L5_env.frontBuffer:release() end)
|
|
end
|
|
|
|
-- Create new canvases
|
|
L5_env.backBuffer = love.graphics.newCanvas(w, h)
|
|
L5_env.frontBuffer = love.graphics.newCanvas(w, h)
|
|
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
love.graphics.clear(0.5, 0.5, 0.5, 1)
|
|
love.graphics.setCanvas(L5_env.frontBuffer)
|
|
love.graphics.clear(0.5, 0.5, 0.5, 1)
|
|
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
width, height = love.graphics.getDimensions()
|
|
|
|
if windowResized then
|
|
windowResized()
|
|
end
|
|
end
|
|
|
|
function toColor(_a, _b, _c, _d)
|
|
-- If _a is a table, return it (assuming it's already in RGBA format)
|
|
if type(_a) == "table" and _b == nil and #_a == 4 then
|
|
return _a
|
|
end
|
|
|
|
local r, g, b, a
|
|
|
|
-- Handle different argument patterns
|
|
if _b == nil then
|
|
-- One argument = grayscale or color name
|
|
if type(_a) == "number" then
|
|
if L5_env.color_mode == RGB then
|
|
r, g, b, a = _a, _a, _a, L5_env.color_max[4]
|
|
elseif L5_env.color_mode == HSB then
|
|
-- Grayscale in HSB: hue=0, saturation=0, brightness=value
|
|
r, g, b = HSVtoRGB(0, 0, _a / L5_env.color_max[3])
|
|
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
|
|
a = L5_env.color_max[4]
|
|
elseif L5_env.color_mode == HSL then
|
|
-- Grayscale in HSL: hue=0, saturation=0, lightness=value
|
|
r, g, b = HSLtoRGB(0, 0, _a / L5_env.color_max[3])
|
|
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
|
|
a = L5_env.color_max[4]
|
|
end
|
|
elseif type(_a) == "string" then
|
|
if _a:sub(1, 1) == "#" then -- Hex color
|
|
r, g, b = hexToRGB(_a)
|
|
a = L5_env.color_max[4]
|
|
else -- HTML color name
|
|
if htmlColors[_a] then
|
|
r, g, b = unpack(htmlColors[_a])
|
|
a = L5_env.color_max[4]
|
|
else
|
|
error("Color '" .. _a .. "' not found in htmlColors table")
|
|
end
|
|
end
|
|
else
|
|
error("Invalid color argument")
|
|
end
|
|
elseif _c == nil then
|
|
-- Two arguments = grayscale with alpha
|
|
if L5_env.color_mode == RGB then
|
|
r, g, b, a = _a, _a, _a, _b
|
|
elseif L5_env.color_mode == HSB then
|
|
r, g, b = HSVtoRGB(0, 0, _a / L5_env.color_max[3])
|
|
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
|
|
a = _b
|
|
elseif L5_env.color_mode == HSL then
|
|
r, g, b = HSLtoRGB(0, 0, _a / L5_env.color_max[3])
|
|
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
|
|
a = _b
|
|
end
|
|
elseif _d == nil then
|
|
-- Three arguments = color components without alpha
|
|
if L5_env.color_mode == RGB then
|
|
r, g, b, a = _a, _b, _c, L5_env.color_max[4]
|
|
elseif L5_env.color_mode == HSB then
|
|
r, g, b = HSVtoRGB(_a / L5_env.color_max[1], _b / L5_env.color_max[2], _c / L5_env.color_max[3])
|
|
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
|
|
a = L5_env.color_max[4]
|
|
elseif L5_env.color_mode == HSL then
|
|
r, g, b = HSLtoRGB(_a / L5_env.color_max[1], _b / L5_env.color_max[2], _c / L5_env.color_max[3])
|
|
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
|
|
a = L5_env.color_max[4]
|
|
end
|
|
else
|
|
-- Four arguments = color components with alpha
|
|
if L5_env.color_mode == RGB then
|
|
r, g, b, a = _a, _b, _c, _d
|
|
elseif L5_env.color_mode == HSB then
|
|
r, g, b = HSVtoRGB(_a / L5_env.color_max[1], _b / L5_env.color_max[2], _c / L5_env.color_max[3])
|
|
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
|
|
a = _d
|
|
elseif L5_env.color_mode == HSL then
|
|
r, g, b = HSLtoRGB(_a / L5_env.color_max[1], _b / L5_env.color_max[2], _c / L5_env.color_max[3])
|
|
r, g, b = r * L5_env.color_max[1], g * L5_env.color_max[2], b * L5_env.color_max[3]
|
|
a = _d
|
|
end
|
|
end
|
|
|
|
-- Return normalized RGBA values (0-1 range)
|
|
return {r/L5_env.color_max[1], g/L5_env.color_max[2], b/L5_env.color_max[3], a/L5_env.color_max[4]}
|
|
end
|
|
|
|
function hexToRGB(hex)
|
|
hex = hex:gsub("#", "") -- Remove # if present
|
|
|
|
-- Check valid length
|
|
if #hex == 3 then
|
|
hex = hex:gsub("(.)", "%1%1") -- Convert 3 to 6-digit
|
|
elseif #hex ~= 6 then
|
|
return nil, "Invalid hex color format. Expected 3 or 6 characters."
|
|
end
|
|
|
|
-- Extract RGB components
|
|
local r = tonumber(hex:sub(1, 2), 16)
|
|
local g = tonumber(hex:sub(3, 4), 16)
|
|
local b = tonumber(hex:sub(5, 6), 16)
|
|
|
|
-- Check if conversion was successful
|
|
if not r or not g or not b then
|
|
return nil, "Invalid hex color format. Contains non-hex characters."
|
|
end
|
|
|
|
return r, g, b
|
|
end
|
|
|
|
function HSVtoRGB(h, s, v)
|
|
if s <= 0 then
|
|
return v, v, v
|
|
end
|
|
h = h * 6
|
|
local c = v * s
|
|
local x = c * (1 - math.abs((h % 2) - 1))
|
|
local m = v - c
|
|
local r, g, b = 0, 0, 0
|
|
if h < 1 then
|
|
r, g, b = c, x, 0
|
|
elseif h < 2 then
|
|
r, g, b = x, c, 0
|
|
elseif h < 3 then
|
|
r, g, b = 0, c, x
|
|
elseif h < 4 then
|
|
r, g, b = 0, x, c
|
|
elseif h < 5 then
|
|
r, g, b = x, 0, c
|
|
else
|
|
r, g, b = c, 0, x
|
|
end
|
|
return r + m, g + m, b + m
|
|
end
|
|
|
|
function HSLtoRGB(h, s, l)
|
|
if s <= 0 then
|
|
return l, l, l
|
|
end
|
|
h = h * 6
|
|
local c = (1 - math.abs(2 * l - 1)) * s
|
|
local x = c * (1 - math.abs((h % 2) - 1))
|
|
local m = l - c / 2
|
|
local r, g, b = 0, 0, 0
|
|
if h < 1 then
|
|
r, g, b = c, x, 0
|
|
elseif h < 2 then
|
|
r, g, b = x, c, 0
|
|
elseif h < 3 then
|
|
r, g, b = 0, c, x
|
|
elseif h < 4 then
|
|
r, g, b = 0, x, c
|
|
elseif h < 5 then
|
|
r, g, b = x, 0, c
|
|
else
|
|
r, g, b = c, 0, x
|
|
end
|
|
return r + m, g + m, b + m
|
|
end
|
|
|
|
function RGBtoHSL(r, g, b)
|
|
-- Normalize RGB values to 0-1 range
|
|
r = r / 255
|
|
g = g / 255
|
|
b = b / 255
|
|
|
|
local max = math.max(r, g, b)
|
|
local min = math.min(r, g, b)
|
|
local h, s, l
|
|
|
|
-- Calculate lightness
|
|
l = (max + min) / 2
|
|
|
|
if max == min then
|
|
-- Achromatic (no color)
|
|
h = 0
|
|
s = 0
|
|
else
|
|
local d = max - min
|
|
|
|
-- Calculate saturation
|
|
if l > 0.5 then
|
|
s = d / (2 - max - min)
|
|
else
|
|
s = d / (max + min)
|
|
end
|
|
|
|
-- Calculate hue
|
|
if max == r then
|
|
h = (g - b) / d + (g < b and 6 or 0)
|
|
elseif max == g then
|
|
h = (b - r) / d + 2
|
|
elseif max == b then
|
|
h = (r - g) / d + 4
|
|
end
|
|
|
|
h = h / 6
|
|
end
|
|
|
|
-- Convert to 0-360 for hue, 0-100 for saturation and lightness
|
|
return h * L5_env.color_max[1], s * L5_env.color_max[2], l * L5_env.color_max[3]
|
|
end
|
|
|
|
function save(filename)
|
|
love.graphics.captureScreenshot(function(imageData)
|
|
-- Generate filename
|
|
local finalFilename
|
|
|
|
if filename then
|
|
-- Check if filename ends with .png
|
|
if filename:match("%.png$") then
|
|
finalFilename = filename
|
|
else
|
|
-- Add .png extension
|
|
finalFilename = filename .. ".png"
|
|
end
|
|
else
|
|
-- Use default timestamp-based name
|
|
local timestamp = os.date("%Y%m%d_%H%M%S")
|
|
finalFilename = "screenshot_" .. timestamp .. ".png"
|
|
end
|
|
|
|
-- Encode to PNG
|
|
local pngData = imageData:encode("png")
|
|
|
|
-- Try to write to current directory first
|
|
local programDir = love.filesystem.getSource()
|
|
local targetPath = programDir .. "/" .. finalFilename
|
|
|
|
local file = io.open(targetPath, "wb")
|
|
if file then
|
|
file:write(pngData:getString())
|
|
file:close()
|
|
print("Screenshot saved to: " .. targetPath)
|
|
else
|
|
-- Fallback: use Love2d's save directory
|
|
print("Warning: Could not write to current directory, using save directory instead")
|
|
local success = love.filesystem.write(finalFilename, pngData)
|
|
|
|
if success then
|
|
local saveDir = love.filesystem.getSaveDirectory()
|
|
print("Screenshot saved to: " .. saveDir .. "/" .. finalFilename)
|
|
else
|
|
print("Error: Could not save screenshot")
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
function describe(sceneDescription)
|
|
if not L5_env.described then
|
|
L5_env.originalPrint("CANVAS_DESCRIPTION: " .. sceneDescription)
|
|
io.flush() -- Ensure immediate output for screen readers
|
|
L5_env.described = true
|
|
end
|
|
end
|
|
|
|
function defaults()
|
|
-- constants
|
|
-- shapes
|
|
CORNER = "CORNER"
|
|
RADIUS = "RADIUS"
|
|
CORNERS = "CORNERS"
|
|
CENTER = "CENTER"
|
|
RADIANS = "RADIANS"
|
|
DEGREES = "DEGREES"
|
|
ROUND = "smooth"
|
|
SQUARE = "rough"
|
|
PROJECT = "project"
|
|
MITER = "miter"
|
|
BEVEL = "bevel"
|
|
NONE = "none"
|
|
-- typography
|
|
LEFT = "left"
|
|
RIGHT = "right"
|
|
CENTER = "center"
|
|
TOP = "top"
|
|
BOTTOM = "bottom"
|
|
BASELINE = "baseline"
|
|
WORD = "word"
|
|
CHAR = "char"
|
|
-- color
|
|
RGB = "rgb"
|
|
HSB = "hsb"
|
|
HSL = "hsl"
|
|
-- math
|
|
PI = math.pi
|
|
HALF_PI = math.pi/2
|
|
QUARTER_PI=math.pi/4
|
|
TWO_PI = 2 * math.pi
|
|
TAU = TWO_PI
|
|
PIE = "pie"
|
|
OPEN = "open"
|
|
CHORD = "closed"
|
|
-- filters (shaders)
|
|
GRAY = "gray"
|
|
THRESHOLD = "threshold"
|
|
INVERT = "invert"
|
|
POSTERIZE = "posterize"
|
|
BLUR = "blur"
|
|
ERODE = "erode"
|
|
DILATE = "dilate"
|
|
-- for applying texture wrapping
|
|
NORMAL = "NORMAL"
|
|
IMAGE = "IMAGE"
|
|
CLAMP = "clamp"
|
|
REPEAT = "repeat"
|
|
-- blend modes
|
|
BLEND = "blend"
|
|
ADD = "add"
|
|
MULTIPLY = "multiply"
|
|
SCREEN = "screen"
|
|
LIGHTEST = "lightest"
|
|
DARKEST = "darkest"
|
|
REPLACE = "replace"
|
|
-- system cursors
|
|
ARROW = "arrow"
|
|
IBEAM = "ibeam"
|
|
WAIT = "wait"
|
|
WAITARROW = "waitarrow"
|
|
CROSSHAIR = "crosshair"
|
|
SIZENWSE = "sizenwse"
|
|
SIZENESW = "sizenesw"
|
|
SIZEWE = "sizewe"
|
|
SIZENS = "sizens"
|
|
SIZEALL = "sizeall"
|
|
NO = "no"
|
|
HAND = "hand"
|
|
|
|
-- global user vars - can be read by user but shouldn't be altered by user
|
|
key = "" --default, overriden with key presses detected in love.update(dt)
|
|
width = 800 --default, overridden with size() or fullscreen()
|
|
height = 600 --ditto
|
|
frameCount = 0
|
|
mouseIsPressed = false
|
|
mouseX=0
|
|
mouseY=0
|
|
keyIsPressed = false
|
|
pmouseX,pmouseY,movedX,movedY=0,0
|
|
mouseButton = nil
|
|
focused = true
|
|
pixels = {}
|
|
end
|
|
|
|
-- environment global variables not user-facing
|
|
function define_env_globals()
|
|
L5_env = L5_env or {} -- Initialize L5_env if it doesn't exist
|
|
L5_env.drawing = true
|
|
-- drawing mode state
|
|
L5_env.degree_mode = RADIANS --also: DEGREES
|
|
L5_env.rect_mode = CORNER --also: CORNERS, CENTER, RADIUS
|
|
L5_env.ellipse_mode = CENTER --also: CORNER, CORNERS, RADIUS
|
|
L5_env.image_mode = CORNER --also: CENTER, CORNERS
|
|
-- global color state
|
|
L5_env.fill_mode="fill" --also: "line"
|
|
L5_env.stroke_color = {0,0,0}
|
|
L5_env.currentTint = {1, 1, 1, 1} -- Default: no tint white
|
|
L5_env.color_max = {255,255,255,255}
|
|
L5_env.color_mode = RGB --also: HSB, HSL
|
|
-- global key state
|
|
L5_env.pressedKeys = {}
|
|
L5_env.keyWasPressed = false
|
|
L5_env.keyWasReleased = false
|
|
L5_env.keyWasTyped = false
|
|
L5_env.typedKey = nil
|
|
-- mouse state
|
|
L5_env.mouseWasMoved = false
|
|
L5_env.wasPressed = false
|
|
L5_env.wheelWasMoved = false
|
|
L5_env.wheelX = nil
|
|
L5_env.wheelY = nil
|
|
L5_env.pendingMouseClicked = nil
|
|
L5_env.pendingMouseReleased = nil
|
|
-- screen buffer state
|
|
L5_env.framerate = nil
|
|
L5_env.backBuffer = nil
|
|
L5_env.frontBuffer = nil
|
|
L5_env.clearscreen = false
|
|
L5_env.described = false
|
|
-- global video tracking for looping
|
|
L5_env.videos = {}
|
|
-- global font state
|
|
L5_env.fontPaths = {}
|
|
L5_env.currentFontPath = nil
|
|
L5_env.currentFontSize = 12
|
|
L5_env.textAlignX = LEFT
|
|
L5_env.textAlignY = BASELINE
|
|
L5_env.textWrap = WORD
|
|
-- filters (shaders)
|
|
L5_env.filterOn = false
|
|
L5_env.filter = nil
|
|
-- pixel array
|
|
L5_env.pixels = {}
|
|
L5_env.imageData = nil
|
|
L5_env.pixelsLoaded = false
|
|
-- custom shape drawing
|
|
L5_env.vertices = {}
|
|
-- custom texture mesh
|
|
L5_env.currentTexture = nil
|
|
L5_env.useTexture = false
|
|
L5_env.textureMode=IMAGE -- NORMAL or IMAGE
|
|
L5_env.textureWrap=CLAMP -- wrap mode CLAMP or REPEAT
|
|
-- custom print output on screen
|
|
L5_env.printBuffer = {}
|
|
L5_env.defaultFont = love.graphics.getFont()
|
|
L5_env.printFont = L5_env.defaultFont
|
|
L5_env.showPrintBuffer = false
|
|
L5_env.printY = 5
|
|
L5_env.printLineHeight = L5_env.defaultFont:getHeight() + 2
|
|
|
|
-- Override print to also draw to screen
|
|
local originalPrint = print
|
|
L5_env.originalPrint = originalPrint
|
|
function print(...)
|
|
originalPrint(...) -- Still print to console
|
|
|
|
local text = ""
|
|
local args = {...}
|
|
for i = 1, #args do
|
|
if i > 1 then text = text .. "\t" end
|
|
text = text .. tostring(args[i])
|
|
end
|
|
|
|
table.insert(L5_env.printBuffer, text)
|
|
end
|
|
end
|
|
|
|
------------------ INIT SHADERS ---------------------
|
|
-- initialize shader default values
|
|
function initShaderDefaults()
|
|
-- Set default values for threshold shader
|
|
L5_filter.threshold:send("soft", 0.5)
|
|
L5_filter.threshold:send("threshold", 0.5)
|
|
|
|
-- Set default value for posterize
|
|
L5_filter.posterize:send("levels", 4.0)
|
|
-- Set default values for blur
|
|
if L5_filter.blurSupportsParameter then
|
|
L5_filter.blur_horizontal:send("blurRadius", 4.0)
|
|
L5_filter.blur_horizontal:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
|
|
L5_filter.blur_vertical:send("blurRadius", 4.0)
|
|
L5_filter.blur_vertical:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
|
|
elseif L5_filter.blur then
|
|
L5_filter.blur:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
|
|
end
|
|
-- Set default values for erode
|
|
L5_filter.erode:send("strength", 0.5)
|
|
L5_filter.erode:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
|
|
|
|
-- Set default values for dilate
|
|
L5_filter.dilate:send("strength", 1.0)
|
|
L5_filter.dilate:send("threshold", 0.1)
|
|
L5_filter.dilate:send("textureSize", {love.graphics.getWidth(), love.graphics.getHeight()})
|
|
end
|
|
|
|
----------------------- INPUT -----------------------
|
|
|
|
function loadStrings(_file)
|
|
local lines = {}
|
|
for line in love.filesystem.lines(_file) do
|
|
table.insert(lines, line)
|
|
end
|
|
return lines
|
|
end
|
|
|
|
function loadTable(_file, _header)
|
|
local extension = _file:match("%.([^%.]+)$")
|
|
|
|
if extension == "csv" or extension == "tsv" then
|
|
local separator = (extension == "csv") and "," or "\t"
|
|
local pattern = (extension == "csv") and "[^,]+" or "[^\t]+"
|
|
|
|
local function splitLine(line)
|
|
local values = {}
|
|
for value in line:gmatch(pattern) do
|
|
if tonumber(value) then table.insert(values, tonumber(value))
|
|
elseif value == "true" then table.insert(values, true)
|
|
elseif value == "false" then table.insert(values, false)
|
|
else table.insert(values, value)
|
|
end
|
|
end
|
|
return values
|
|
end
|
|
|
|
local function loadDelimitedFile(filename)
|
|
local data = {}
|
|
local headers = {}
|
|
local first_line = true
|
|
|
|
for line in love.filesystem.lines(filename) do
|
|
local row = splitLine(line)
|
|
|
|
if _header == "header" and first_line then
|
|
for value in line:gmatch(pattern) do
|
|
table.insert(headers, value)
|
|
end
|
|
first_line = false
|
|
else
|
|
if _header == "header" then
|
|
local record = {}
|
|
for i, value in ipairs(row) do
|
|
if headers[i] then
|
|
record[headers[i]] = value
|
|
end
|
|
end
|
|
table.insert(data, record)
|
|
else
|
|
table.insert(data, row)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- If no headers were loaded, create numbered column identifiers
|
|
if #headers == 0 and #data > 0 then
|
|
for i = 1, #data[1] do
|
|
table.insert(headers, i)
|
|
end
|
|
end
|
|
|
|
data.columns = headers
|
|
return data
|
|
end
|
|
|
|
return loadDelimitedFile(_file)
|
|
|
|
elseif extension == "lua" then
|
|
local chunk = love.filesystem.load(_file)
|
|
if chunk then
|
|
return chunk()
|
|
else
|
|
error("Could not load Lua file: " .. _file)
|
|
end
|
|
else
|
|
error("Unsupported file type: " .. (extension or "no extension") .. " for file: " .. _file)
|
|
end
|
|
end
|
|
|
|
function saveStrings(data, filename)
|
|
local lines = {}
|
|
for i, value in ipairs(data) do
|
|
table.insert(lines, tostring(value))
|
|
end
|
|
local content = table.concat(lines, "\n")
|
|
|
|
-- Use io.open to write directly to current directory
|
|
local file = io.open(filename, "w")
|
|
if file then
|
|
file:write(content)
|
|
file:close()
|
|
return true
|
|
else
|
|
print("Error: Could not open file for writing: " .. filename)
|
|
return false
|
|
end
|
|
end
|
|
|
|
function saveTable(data, filename, format)
|
|
-- Auto-detect format from filename if not specified
|
|
if not format then
|
|
local extension = filename:match("%.([^%.]+)$")
|
|
format = extension or "lua"
|
|
end
|
|
|
|
if format == "lua" then
|
|
-- Save as Lua file with return
|
|
local function serializeValue(val)
|
|
if type(val) == "string" then
|
|
return string.format("%q", val)
|
|
elseif type(val) == "number" or type(val) == "boolean" then
|
|
return tostring(val)
|
|
elseif val == nil then
|
|
return "nil"
|
|
else
|
|
return tostring(val)
|
|
end
|
|
end
|
|
|
|
local function serializeTable(tbl, indent)
|
|
indent = indent or ""
|
|
local lines = {}
|
|
table.insert(lines, "{")
|
|
|
|
for i, value in ipairs(tbl) do
|
|
if type(value) == "table" then
|
|
table.insert(lines, indent .. " " .. serializeTable(value, indent .. " ") .. ",")
|
|
else
|
|
table.insert(lines, indent .. " " .. serializeValue(value) .. ",")
|
|
end
|
|
end
|
|
|
|
-- Handle named keys
|
|
for key, value in pairs(tbl) do
|
|
if type(key) ~= "number" or key > #tbl then
|
|
local keyStr = type(key) == "string" and key or "[" .. serializeValue(key) .. "]"
|
|
if type(value) == "table" then
|
|
table.insert(lines, indent .. " " .. keyStr .. " = " .. serializeTable(value, indent .. " ") .. ",")
|
|
else
|
|
table.insert(lines, indent .. " " .. keyStr .. " = " .. serializeValue(value) .. ",")
|
|
end
|
|
end
|
|
end
|
|
|
|
table.insert(lines, indent .. "}")
|
|
return table.concat(lines, "\n")
|
|
end
|
|
|
|
local content = "return " .. serializeTable(data)
|
|
|
|
local file = io.open(filename, "w")
|
|
if file then
|
|
file:write(content)
|
|
file:close()
|
|
return true
|
|
end
|
|
|
|
elseif format == "csv" or format == "tsv" then
|
|
local separator = (format == "csv") and "," or "\t"
|
|
local lines = {}
|
|
|
|
-- Check if data is a single record (has named keys but no array elements)
|
|
local isSingleRecord = (#data == 0)
|
|
for k, v in pairs(data) do
|
|
if type(k) == "string" then
|
|
isSingleRecord = true
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Convert single record to array of one record
|
|
local records = data
|
|
if isSingleRecord and #data == 0 then
|
|
records = {data}
|
|
end
|
|
|
|
-- Get headers from first row if it's a table with named keys
|
|
local headers = {}
|
|
if #records > 0 and type(records[1]) == "table" then -- Fixed: use records
|
|
for key, _ in pairs(records[1]) do -- Fixed: use records
|
|
if type(key) == "string" then
|
|
table.insert(headers, key)
|
|
end
|
|
end
|
|
|
|
if #headers > 0 then
|
|
-- Add header row
|
|
table.insert(lines, table.concat(headers, separator))
|
|
|
|
-- Add data rows using headers
|
|
for i, row in ipairs(records) do -- Fixed: use records
|
|
local values = {}
|
|
for _, header in ipairs(headers) do
|
|
table.insert(values, tostring(row[header] or ""))
|
|
end
|
|
table.insert(lines, table.concat(values, separator))
|
|
end
|
|
else
|
|
-- Array-style table, just use indices
|
|
for i, row in ipairs(records) do -- Fixed: use records
|
|
if type(row) == "table" then
|
|
local values = {}
|
|
for _, value in ipairs(row) do
|
|
table.insert(values, tostring(value))
|
|
end
|
|
table.insert(lines, table.concat(values, separator))
|
|
else
|
|
table.insert(lines, tostring(row))
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- Simple array
|
|
for i, value in ipairs(records) do -- Fixed: use records
|
|
table.insert(lines, tostring(value))
|
|
end
|
|
end
|
|
|
|
local content = table.concat(lines, "\n")
|
|
|
|
local file = io.open(filename, "w")
|
|
if file then
|
|
file:write(content)
|
|
file:close()
|
|
return true
|
|
end
|
|
|
|
else
|
|
print("Error: Unsupported format '" .. format .. "'. Use 'lua', 'csv', or 'tsv'")
|
|
return false
|
|
end
|
|
|
|
print("Error: Could not open file for writing: " .. filename)
|
|
return false
|
|
end
|
|
|
|
----------------------- EVENTS ----------------------
|
|
|
|
---------------------- KEYBOARD ---------------------
|
|
|
|
function updateLastKeyPressed()
|
|
local commonKeys = {
|
|
-- Letters
|
|
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
|
|
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
|
-- Numbers
|
|
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
|
|
-- Function keys
|
|
"f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12",
|
|
-- Special keys
|
|
"space", "return", "escape", "backspace", "delete", "tab",
|
|
-- Arrow keys
|
|
"up", "down", "left", "right",
|
|
-- Navigation
|
|
"home", "end", "pageup", "pagedown", "insert",
|
|
-- Modifiers
|
|
"lshift", "rshift", "lctrl", "rctrl", "lalt", "ralt",
|
|
"capslock", "numlock", "scrolllock",
|
|
-- Punctuation
|
|
".", ",", ";", "'", "/", "\\", "[", "]", "-", "=", "`",
|
|
-- Numpad
|
|
"kp0", "kp1", "kp2", "kp3", "kp4", "kp5", "kp6", "kp7", "kp8", "kp9",
|
|
"kp.", "kp/", "kp*", "kp-", "kp+", "kpenter",
|
|
-- Other
|
|
"pause", "printscreen"
|
|
}
|
|
|
|
-- reset keyIsPressed to false initially
|
|
keyIsPressed = false
|
|
-- Check each key and update vars
|
|
for _, k in ipairs(commonKeys) do
|
|
if love.keyboard.isDown(k) then
|
|
key = k
|
|
keyIsPressed = true
|
|
break -- Take the first key found
|
|
end
|
|
end
|
|
return key
|
|
end
|
|
|
|
function keyIsDown(k)
|
|
return L5_env.pressedKeys[k] == true
|
|
end
|
|
|
|
---------------------- TRANSFORM ---------------------
|
|
|
|
function push()
|
|
love.graphics.push()
|
|
end
|
|
|
|
function pop()
|
|
love.graphics.pop()
|
|
end
|
|
|
|
function translate(_x,_y)
|
|
love.graphics.translate(_x,_y )
|
|
end
|
|
|
|
function rotate(_angle)
|
|
if L5_env.degree_mode == RADIANS then
|
|
love.graphics.rotate(_angle)
|
|
else
|
|
love.graphics.rotate(radians(_angle))
|
|
end
|
|
end
|
|
|
|
function scale(_sx,_sy)
|
|
if _sy ~= nil then --2 args, 2 dif scales
|
|
love.graphics.scale(_sx,_sy)
|
|
else --only 1 arg, scale same both directions
|
|
love.graphics.scale(_sx,_sx)
|
|
end
|
|
end
|
|
|
|
function applyMatrix(...)
|
|
local args = {...}
|
|
local a, b, c, d, e, f
|
|
|
|
-- Check if first argument is a table
|
|
if #args == 1 and type(args[1]) == "table" then
|
|
local t = args[1]
|
|
if #t ~= 6 then
|
|
error("applyMatrix() table must contain exactly 6 values")
|
|
end
|
|
a, b, c, d, e, f = t[1], t[2], t[3], t[4], t[5], t[6]
|
|
elseif #args == 6 then
|
|
a, b, c, d, e, f = args[1], args[2], args[3], args[4], args[5], args[6]
|
|
else
|
|
error("applyMatrix() requires either 6 arguments or a table with 6 values")
|
|
end
|
|
|
|
-- Validate that all values are numbers
|
|
if type(a) ~= "number" or type(b) ~= "number" or type(c) ~= "number" or
|
|
type(d) ~= "number" or type(e) ~= "number" or type(f) ~= "number" then
|
|
error("applyMatrix() requires all values to be numbers")
|
|
end
|
|
|
|
-- p5.js matrix format:
|
|
-- | a c e |
|
|
-- | b d f |
|
|
-- | 0 0 1 |
|
|
|
|
-- Extract translation
|
|
local tx, ty = e, f
|
|
|
|
-- Check if it's a pure shear matrix (no rotation/scale, just shear)
|
|
-- Pure x-shear: a=1, b=0, d=1, c=shear
|
|
if a == 1 and b == 0 and d == 1 then
|
|
local transform = love.math.newTransform(tx, ty, 0, 1, 1, 0, 0, c, 0)
|
|
love.graphics.applyTransform(transform)
|
|
return
|
|
end
|
|
|
|
-- Pure y-shear: a=1, c=0, d=1, b=shear
|
|
if a == 1 and c == 0 and d == 1 then
|
|
local transform = love.math.newTransform(tx, ty, 0, 1, 1, 0, 0, 0, b)
|
|
love.graphics.applyTransform(transform)
|
|
return
|
|
end
|
|
|
|
-- General case: decompose into scale, rotation, and shear
|
|
local sx = math.sqrt(a * a + b * b)
|
|
local sy = math.sqrt(c * c + d * d)
|
|
local angle = math.atan2(b, a)
|
|
|
|
-- Calculate shear
|
|
local kx = (a * c + b * d) / (sx * sx)
|
|
local ky = 0
|
|
|
|
local transform = love.math.newTransform(tx, ty, angle, sx, sy, 0, 0, kx, ky)
|
|
love.graphics.applyTransform(transform)
|
|
end
|
|
|
|
function resetMatrix()
|
|
love.graphics.origin()
|
|
end
|
|
|
|
-------------------- TIME and DATE -------------------
|
|
|
|
function millis()
|
|
return 1000*love.timer.getTime()
|
|
end
|
|
|
|
function day()
|
|
return tonumber(os.date("%d"))
|
|
end
|
|
|
|
function month()
|
|
return tonumber(os.date("%m"))
|
|
end
|
|
|
|
function year()
|
|
return tonumber(os.date("%Y"))
|
|
end
|
|
|
|
function hour()
|
|
return tonumber(os.date("%H"))
|
|
end
|
|
|
|
function minute()
|
|
return tonumber(os.date("%M"))
|
|
end
|
|
|
|
function second()
|
|
return tonumber(os.date("%S"))
|
|
end
|
|
|
|
------------------------ SHAPE -----------------------
|
|
|
|
-------------------- 2D Primitives -------------------
|
|
|
|
function rect(_a,_b,_c,_d,_e)
|
|
if L5_env.rect_mode==CORNERS then --x1,y1,x2,y2
|
|
love.graphics.rectangle(L5_env.fill_mode,_a,_b,_c-_a,_d-_b,_e,_e)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.rectangle("line",_a,_b,_c-_a,_d-_b,_e,_e)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.rect_mode==CENTER then --x-w/2,y-h/2,w,h
|
|
love.graphics.rectangle(L5_env.fill_mode, _a-_c/2,_b-_d/2,_c,_d,_e,_e)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.rectangle("line", _a-_c/2,_b-_d/2,_c,_d,_e,_e)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.rect_mode==RADIUS then --x-w/2,y-h/2,r1*2,r2*2
|
|
love.graphics.rectangle(L5_env.fill_mode, _a-_c,_b-_d,_c*2,_d*2,_e,_e)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.rectangle("line", _a-_c,_b-_d,_c*2,_d*2,_e,_e)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.rect_mode==CORNER then --CORNER default x,y,w,h
|
|
love.graphics.rectangle(L5_env.fill_mode,_a,_b,_c,_d,_e,_e)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.rectangle("line",_a,_b,_c,_d,_e,_e)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
end
|
|
|
|
function square(_a,_b,_c, _d)
|
|
--note: _d is not height! it is radius of rounded corners!
|
|
--CORNERS mode doesn't exist for squares
|
|
if L5_env.rect_mode==CENTER then --x-w/2,y-h/2,w,h
|
|
love.graphics.rectangle(L5_env.fill_mode, _a-_c/2,_b-_c/2,_c,_c,_d,_d)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.rectangle("line", _a-_c/2,_b-_c/2,_c,_c,_d,_d)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.rect_mode==RADIUS then --x-w/2,y-h/2,r*2,r*2
|
|
love.graphics.rectangle(L5_env.fill_mode, _a-_c,_b-_c,_c*2,_c*2,_d,_d)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.rectangle("line", _a-_c,_b-_c,_c*2,_c*2,_d,_d)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.rect_mode==CORNER then -- CORNER default x,y,w,h
|
|
love.graphics.rectangle(L5_env.fill_mode,_a,_b,_c,_c,_d,_d)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.rectangle("line",_a,_b,_c,_c,_d,_d)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
end
|
|
|
|
function ellipse(_a,_b,_c,_d)
|
|
--love.graphics.ellipse( mode, x, y, radiusx, radiusy, segments )
|
|
if not _d then
|
|
_d = _c
|
|
end
|
|
if L5_env.ellipse_mode==RADIUS then
|
|
love.graphics.ellipse(L5_env.fill_mode,_a,_b,_c,_d)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.ellipse("line",_a,_b,_c,_d)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.ellipse_mode==CORNER then
|
|
love.graphics.ellipse(L5_env.fill_mode,_a+_c/2,_b+_d/2,_c/2,_d/2)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.ellipse("line",_a+_c/2,_b+_d/2,_c/2,_d/2)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.ellipse_mode==CORNERS then
|
|
love.graphics.ellipse(L5_env.fill_mode,_a+(_c-_a)/2,_b+(_d-_a)/2,(_c-_a)/2,(_d-_b)/2)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.ellipse("line",_a+(_c-_a)/2,_b+(_d-_a)/2,(_c-_a)/2,(_d-_b)/2)
|
|
love.graphics.setColor(r, g, b, a)
|
|
else --default CENTER x,y,w/2,h/2
|
|
love.graphics.ellipse(L5_env.fill_mode,_a,_b,_c/2,_d/2)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.ellipse("line",_a,_b,_c/2,_d/2)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
end
|
|
|
|
function circle(_a,_b,_c)
|
|
if L5_env.ellipse_mode==RADIUS then
|
|
love.graphics.ellipse(L5_env.fill_mode,_a,_b,_c,_c)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.ellipse("line",_a,_b,_c,_d)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.ellipse_mode==CORNER then
|
|
love.graphics.ellipse(L5_env.fill_mode,_a+_c/2,_b+_c/2,_c/2,_c/2)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.ellipse("line",_a+_c/2,_b+_c/2,_c/2,_c/2)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.ellipse_mode==CORNERS then
|
|
love.graphics.ellipse(L5_env.fill_mode,_a+(_c-_a)/2,_b+(_c-_a)/2,(_c-_a)/2,(_c-_b)/2)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.ellipse("line",_a+(_c-_a)/2,_b+(_c-_a)/2,(_c-_a)/2,(_c-_b)/2)
|
|
love.graphics.setColor(r, g, b, a)
|
|
elseif L5_env.ellipse_mode==CENTER then --default CENTER x,y,w/2,h/2
|
|
love.graphics.ellipse(L5_env.fill_mode,_a,_b,_c/2,_c/2)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.ellipse("line",_a,_b,_c/2,_c/2)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
end
|
|
|
|
|
|
|
|
function quad(_x1,_y1,_x2,_y2,_x3,_y3,_x4,_y4) --this is a 4-sided love2d polygon! a quad implies an applied texture
|
|
--for other # of sides, use processing api call createShape
|
|
love.graphics.polygon(L5_env.fill_mode,_x1,_y1,_x2,_y2,_x3,_y3,_x4,_y4)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.polygon("line",_x1,_y1,_x2,_y2,_x3,_y3,_x4,_y4)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
|
|
function triangle(_x1,_y1,_x2,_y2,_x3,_y3) --this is a 3-sided love2d polygon
|
|
love.graphics.polygon(L5_env.fill_mode,_x1,_y1,_x2,_y2,_x3,_y3)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.polygon("line",_x1,_y1,_x2,_y2,_x3,_y3)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
|
|
--p5 calls arctype parameter "mode"
|
|
function arc(_x, _y, _w, _h, _start, _stop, _arctype)
|
|
local arctype = _arctype or PIE
|
|
|
|
-- Convert angles to radians if in DEGREES mode
|
|
local start_angle = _start
|
|
local stop_angle = _stop
|
|
|
|
if L5_env.degree_mode == DEGREES then
|
|
start_angle = math.rad(_start)
|
|
stop_angle = math.rad(_stop)
|
|
end
|
|
|
|
local radius_x = _w / 2
|
|
local radius_y = _h / 2
|
|
|
|
-- Calculate center based on ellipseMode
|
|
local center_x = _x
|
|
local center_y = _y
|
|
|
|
if L5_env.ellipse_mode == CENTER then
|
|
center_x = _x
|
|
center_y = _y
|
|
elseif L5_env.ellipse_mode == RADIUS then
|
|
center_x = _x
|
|
center_y = _y
|
|
radius_x = _w -- In RADIUS mode, w and h are the radii directly
|
|
radius_y = _h
|
|
elseif L5_env.ellipse_mode == CORNER then
|
|
center_x = _x + radius_x
|
|
center_y = _y + radius_y
|
|
elseif L5_env.ellipse_mode == CORNERS then
|
|
center_x = (_x + _w) / 2
|
|
center_y = (_y + _h) / 2
|
|
radius_x = (_w - _x) / 2
|
|
radius_y = (_h - _y) / 2
|
|
end
|
|
|
|
-- Normalize angles to [0, 2π) range
|
|
local function normalize_angle(angle)
|
|
local TWO_PI = 2 * math.pi
|
|
angle = angle % TWO_PI
|
|
if angle < 0 then
|
|
angle = angle + TWO_PI
|
|
end
|
|
return angle
|
|
end
|
|
|
|
local start_norm = normalize_angle(start_angle)
|
|
local stop_norm = normalize_angle(stop_angle)
|
|
|
|
-- Processing always draws clockwise from start to stop
|
|
local arc_span
|
|
if stop_norm <= start_norm then
|
|
-- Arc crosses the 0° boundary - go the long way around
|
|
arc_span = (2 * math.pi - start_norm) + stop_norm
|
|
else
|
|
-- Normal case - direct clockwise arc
|
|
arc_span = stop_norm - start_norm
|
|
end
|
|
|
|
-- Check if this should be a full circle
|
|
local epsilon = 1e-6
|
|
local is_full_circle = arc_span >= (2 * math.pi - epsilon)
|
|
|
|
if is_full_circle then
|
|
-- Draw a full ellipse
|
|
if L5_env.fill_mode and L5_env.fill_mode ~= "line" then
|
|
love.graphics.ellipse("fill", center_x, center_y, radius_x, radius_y)
|
|
end
|
|
|
|
if L5_env.stroke_color then
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.ellipse("line", center_x, center_y, radius_x, radius_y)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
else
|
|
-- Handle elliptical arcs (when _w != _h)
|
|
if math.abs(radius_x - radius_y) < epsilon then
|
|
-- Circular arc - use Love2D's built-in arc function
|
|
local radius = radius_x
|
|
|
|
if L5_env.fill_mode and L5_env.fill_mode ~= "line" then
|
|
love.graphics.arc("fill", arctype, center_x, center_y, radius, start_norm, start_norm + arc_span)
|
|
end
|
|
|
|
if L5_env.stroke_color then
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.arc("line", arctype, center_x, center_y, radius, start_norm, start_norm + arc_span)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
else
|
|
-- Elliptical arc - need to draw manually with vertices
|
|
draw_elliptical_arc(center_x, center_y, radius_x, radius_y, start_norm, arc_span, arctype)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Helper function to draw elliptical arcs
|
|
function draw_elliptical_arc(cx, cy, rx, ry, start_angle, arc_span, arctype)
|
|
local segments = math.max(8, math.floor(math.abs(arc_span) * 12)) -- Adaptive segments
|
|
local vertices = {}
|
|
|
|
-- Generate arc vertices
|
|
for i = 0, segments do
|
|
local angle = start_angle + (arc_span * i / segments)
|
|
local x = cx + rx * math.cos(angle)
|
|
local y = cy + ry * math.sin(angle)
|
|
table.insert(vertices, x)
|
|
table.insert(vertices, y)
|
|
end
|
|
|
|
if arctype == PIE then
|
|
-- Add center point for pie
|
|
table.insert(vertices, 1, cy) -- Insert at position 2 (after first vertex)
|
|
table.insert(vertices, 1, cx) -- Insert at position 1
|
|
elseif arctype == CHORD then
|
|
-- Close the arc by connecting endpoints
|
|
-- vertices already has the right points
|
|
end
|
|
-- "open" type doesn't need modification
|
|
|
|
-- Draw filled arc
|
|
if L5_env.fill_mode and L5_env.fill_mode ~= "line" and #vertices >= 6 then
|
|
if arctype == "pie" then
|
|
love.graphics.polygon("fill", vertices)
|
|
elseif arctype == CHORD then
|
|
love.graphics.polygon("fill", vertices)
|
|
end
|
|
-- "open" type doesn't get filled
|
|
end
|
|
|
|
-- Draw stroke
|
|
if L5_env.stroke_color then
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
|
|
if arctype == OPEN then
|
|
-- Just draw the arc line
|
|
for i = 1, #vertices - 2, 2 do
|
|
love.graphics.line(vertices[i], vertices[i+1], vertices[i+2], vertices[i+3])
|
|
end
|
|
elseif arctype == CHORD then
|
|
-- Draw the arc and the closing line
|
|
love.graphics.polygon("line", vertices)
|
|
elseif arctype == PIE then
|
|
-- Draw the arc and lines to center
|
|
love.graphics.polygon("line", vertices)
|
|
end
|
|
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
end
|
|
|
|
function point(_x,_y)
|
|
--Points unaffected by love.graphics.scale - size is always in pixels
|
|
--a line is drawn in the stroke color
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.points(_x,_y)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
|
|
function line(_x1,_y1,_x2,_y2)
|
|
--a line is drawn in the stroke color
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.line(_x1,_y1,_x2,_y2)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
|
|
function background(_r,_g,_b,_a)
|
|
if type(_r) == "userdata" and _r:type() == "Image" then
|
|
image(_r,0,0,width,height)
|
|
else
|
|
local prevR, prevG, prevB, prevA = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(toColor(_r,_g,_b,_a)))
|
|
love.graphics.rectangle("fill", 0, 0, width, height)
|
|
love.graphics.setColor(prevR, prevG, prevB, prevA)
|
|
L5_env.clearscreen = true
|
|
end
|
|
end
|
|
|
|
function colorMode(_mode, _max1, _max2, _max3, _maxA)
|
|
--handles 4 colorMode variations
|
|
-- Set the color mode
|
|
if _mode == RGB or _mode == HSB or _mode == HSL then
|
|
L5_env.color_mode = _mode
|
|
else
|
|
error("Invalid color mode. Use RGB, HSB, or HSL")
|
|
end
|
|
|
|
-- Handle different argument patterns
|
|
if _max1 == nil then
|
|
-- No max specified - use defaults
|
|
if _mode == RGB then
|
|
L5_env.color_max = {255, 255, 255, 255}
|
|
elseif _mode == HSB or _mode == HSL then
|
|
L5_env.color_max = {360, 100, 100, 100}
|
|
end
|
|
elseif _max2 == nil then
|
|
-- One max specified - apply to all channels
|
|
L5_env.color_max = {_max1, _max1, _max1, _max1}
|
|
elseif _max3 == nil then
|
|
error("colorMode requires either 1, 3, or 4 max values")
|
|
elseif _maxA == nil then
|
|
-- Three max values specified (no alpha)
|
|
if _mode == RGB then
|
|
L5_env.color_max = {_max1, _max2, _max3, 255} -- Default alpha
|
|
elseif _mode == HSB or _mode == HSL then
|
|
L5_env.color_max = {_max1, _max2, _max3, 100} -- Default alpha
|
|
end
|
|
else
|
|
-- Four max values specified (including alpha)
|
|
L5_env.color_max = {_max1, _max2, _max3, _maxA}
|
|
end
|
|
end
|
|
|
|
function fill(...)
|
|
L5_env.fill_mode = "fill"
|
|
local args = {...}
|
|
|
|
-- If single argument is a table
|
|
if #args == 1 and type(args[1]) == "table" then
|
|
local t = args[1]
|
|
-- Check if it's normalized (all values <= 1.0) or raw array
|
|
if t[1] <= 1.0 and t[2] <= 1.0 and t[3] <= 1.0 and (not t[4] or t[4] <= 1.0) then
|
|
-- Already normalized, use directly
|
|
love.graphics.setColor(unpack(t))
|
|
else
|
|
-- Raw array, needs conversion
|
|
love.graphics.setColor(unpack(toColor(unpack(t))))
|
|
end
|
|
else
|
|
love.graphics.setColor(unpack(toColor(...)))
|
|
end
|
|
end
|
|
|
|
--------------- CREATING and READING ----------------
|
|
|
|
function color(...)
|
|
local args = {...}
|
|
|
|
-- Check if first argument is a table
|
|
if #args == 1 and type(args[1]) == "table" then
|
|
local t = args[1]
|
|
if #t == 3 then
|
|
return toColor(t[1], t[2], t[3], L5_env.color_max[4])
|
|
elseif #t == 4 then
|
|
return toColor(t[1], t[2], t[3], t[4])
|
|
else
|
|
error("color() table argument requires 3 or 4 values")
|
|
end
|
|
end
|
|
|
|
-- Regular argument handling
|
|
if #args == 3 then
|
|
return toColor(args[1], args[2], args[3], L5_env.color_max[4])
|
|
elseif #args == 4 then
|
|
return toColor(args[1], args[2], args[3], args[4])
|
|
elseif #args == 2 then
|
|
return toColor(args[1], args[1], args[1], args[2])
|
|
elseif #args == 1 then
|
|
return toColor(args[1])
|
|
else
|
|
error("color() requires 1-4 arguments or a table with 3-4 values")
|
|
end
|
|
end
|
|
|
|
function red(_color)
|
|
if type(_color) == "string" then
|
|
-- Convert CSS color string to color object first
|
|
_color = toColor(_color)
|
|
elseif type(_color) ~= "table" then
|
|
error("red() requires a color table or CSS string")
|
|
end
|
|
|
|
-- Check if it's a normalized color object (values 0-1) from color() function
|
|
-- versus a raw array with values in the current color mode range
|
|
if _color[1] <= 1.0 and _color[2] <= 1.0 and _color[3] <= 1.0 then
|
|
-- It's normalized - scale to current color mode range
|
|
return _color[1] * L5_env.color_max[1]
|
|
else
|
|
-- It's a raw array - already in color mode range, return as-is
|
|
return _color[1]
|
|
end
|
|
end
|
|
|
|
function green(_color)
|
|
if type(_color) == "string" then
|
|
-- Convert CSS color string to color object first
|
|
_color = toColor(_color)
|
|
elseif type(_color) ~= "table" then
|
|
error("green() requires a color table or CSS string")
|
|
end
|
|
|
|
-- Check if it's a normalized color object (values 0-1) from color() function
|
|
-- versus a raw array with values in the current color mode range
|
|
if _color[1] <= 1.0 and _color[2] <= 1.0 and _color[3] <= 1.0 then
|
|
-- It's normalized - scale to current color mode range
|
|
return _color[2] * L5_env.color_max[2]
|
|
else
|
|
-- It's a raw array - already in color mode range, return as-is
|
|
return _color[2]
|
|
end
|
|
end
|
|
|
|
function blue(_color)
|
|
if type(_color) == "string" then
|
|
-- Convert CSS color string to color object first
|
|
_color = toColor(_color)
|
|
elseif type(_color) ~= "table" then
|
|
error("blue() requires a color table or CSS string")
|
|
end
|
|
|
|
-- Check if it's a normalized color object (values 0-1) from color() function
|
|
-- versus a raw array with values in the current color mode range
|
|
if _color[1] <= 1.0 and _color[2] <= 1.0 and _color[3] <= 1.0 then
|
|
-- It's normalized - scale to current color mode range
|
|
return _color[3] * L5_env.color_max[3]
|
|
else
|
|
-- It's a raw array - already in color mode range, return as-is
|
|
return _color[3]
|
|
end
|
|
end
|
|
|
|
function alpha(_color)
|
|
if type(_color) == "string" then
|
|
-- Convert CSS color string to color object first
|
|
_color = toColor(_color)
|
|
elseif type(_color) ~= "table" then
|
|
error("alpha() requires a color table or CSS string")
|
|
end
|
|
|
|
-- Check if it's a normalized color object (values 0-1) from color() function
|
|
-- versus a raw array with values in the current color mode range
|
|
if _color[1] <= 1.0 and _color[2] <= 1.0 and _color[3] <= 1.0 then
|
|
-- It's normalized - scale to current color mode range
|
|
return _color[4] * L5_env.color_max[4]
|
|
else
|
|
-- It's a raw array - already in color mode range, return as-is
|
|
return _color[4]
|
|
end
|
|
end
|
|
|
|
function brightness(_color)
|
|
if type(_color) == "string" then
|
|
-- Convert CSS color string to color object first
|
|
_color = toColor(_color)
|
|
elseif type(_color) ~= "table" then
|
|
error("brightness() requires a color table or CSS string")
|
|
end
|
|
|
|
-- Check if it's a normalized color object (values 0-1) or raw array
|
|
local isNormalized = _color[1] <= 1.0 and _color[2] <= 1.0 and _color[3] <= 1.0
|
|
|
|
local r, g, b
|
|
if isNormalized then
|
|
-- Already normalized (0-1)
|
|
r, g, b = _color[1], _color[2], _color[3]
|
|
else
|
|
-- Raw array - normalize it
|
|
r = _color[1] / L5_env.color_max[1]
|
|
g = _color[2] / L5_env.color_max[2]
|
|
b = _color[3] / L5_env.color_max[3]
|
|
end
|
|
|
|
-- Convert RGB to HSB and extract brightness (which is the V in HSV)
|
|
local max = math.max(r, g, b)
|
|
local min = math.min(r, g, b)
|
|
local brightness = max -- Brightness is the max of RGB values
|
|
|
|
-- Return brightness in the current color mode range
|
|
if L5_env.color_mode == HSB then
|
|
return brightness * L5_env.color_max[3]
|
|
else
|
|
-- Default: return in 0-100 range
|
|
return brightness * 100
|
|
end
|
|
end
|
|
|
|
function lightness(_color)
|
|
if type(_color) == "string" then
|
|
-- Convert CSS color string to color object first
|
|
_color = toColor(_color)
|
|
elseif type(_color) ~= "table" then
|
|
error("lightness() requires a color table or CSS string")
|
|
end
|
|
|
|
-- Check if it's a normalized color object (values 0-1) or raw array
|
|
local isNormalized = _color[1] <= 1.0 and _color[2] <= 1.0 and _color[3] <= 1.0
|
|
|
|
local r, g, b
|
|
if isNormalized then
|
|
-- Already normalized (0-1) from toColor()
|
|
r, g, b = _color[1], _color[2], _color[3]
|
|
else
|
|
-- Raw array - normalize based on current color mode
|
|
if L5_env.color_mode == RGB then
|
|
r = _color[1] / L5_env.color_max[1]
|
|
g = _color[2] / L5_env.color_max[2]
|
|
b = _color[3] / L5_env.color_max[3]
|
|
elseif L5_env.color_mode == HSL then
|
|
-- Raw HSL array - convert to RGB first
|
|
r, g, b = HSLtoRGB(_color[1] / L5_env.color_max[1], _color[2] / L5_env.color_max[2], _color[3] / L5_env.color_max[3])
|
|
elseif L5_env.color_mode == HSB then
|
|
-- Raw HSB array - convert to RGB first
|
|
r, g, b = HSVtoRGB(_color[1] / L5_env.color_max[1], _color[2] / L5_env.color_max[2], _color[3] / L5_env.color_max[3])
|
|
end
|
|
end
|
|
|
|
-- Convert RGB to HSL lightness
|
|
local max = math.max(r, g, b)
|
|
local min = math.min(r, g, b)
|
|
local lightness = (max + min) / 2
|
|
|
|
-- Return lightness in the current color mode range
|
|
if L5_env.color_mode == HSL then
|
|
return lightness * L5_env.color_max[3]
|
|
else
|
|
-- Default: return in 0-100 range
|
|
return lightness * 100
|
|
end
|
|
end
|
|
|
|
function hue(_color)
|
|
if type(_color) == "string" then
|
|
_color = toColor(_color)
|
|
elseif type(_color) ~= "table" then
|
|
error("hue() requires a color table or CSS string")
|
|
end
|
|
|
|
-- toColor() always returns normalized 0-1 values
|
|
-- Raw arrays have values in the color_max range
|
|
-- If all values are <= 1, it's normalized; otherwise it's raw
|
|
local isNormalized = _color[1] <= 1.0 and _color[2] <= 1.0 and _color[3] <= 1.0
|
|
|
|
local r, g, b
|
|
if isNormalized then
|
|
-- Already normalized (0-1) from toColor()
|
|
r, g, b = _color[1], _color[2], _color[3]
|
|
else
|
|
-- Raw array - normalize based on current color mode
|
|
if L5_env.color_mode == RGB then
|
|
r = _color[1] / L5_env.color_max[1]
|
|
g = _color[2] / L5_env.color_max[2]
|
|
b = _color[3] / L5_env.color_max[3]
|
|
elseif L5_env.color_mode == HSL then
|
|
-- Raw HSL array - convert to RGB first
|
|
r, g, b = HSLtoRGB(_color[1] / L5_env.color_max[1], _color[2] / L5_env.color_max[2], _color[3] / L5_env.color_max[3])
|
|
elseif L5_env.color_mode == HSB then
|
|
-- Raw HSB array - convert to RGB first
|
|
r, g, b = HSVtoRGB(_color[1] / L5_env.color_max[1], _color[2] / L5_env.color_max[2], _color[3] / L5_env.color_max[3])
|
|
end
|
|
end
|
|
|
|
-- Convert RGB to hue
|
|
local max = math.max(r, g, b)
|
|
local min = math.min(r, g, b)
|
|
local delta = max - min
|
|
|
|
local h = 0
|
|
if delta ~= 0 then
|
|
if max == r then
|
|
h = ((g - b) / delta) % 6
|
|
elseif max == g then
|
|
h = (b - r) / delta + 2
|
|
else
|
|
h = (r - g) / delta + 4
|
|
end
|
|
h = h * 60
|
|
if h < 0 then h = h + 360 end
|
|
end
|
|
|
|
-- Return hue in the current color mode range
|
|
if L5_env.color_mode == HSB or L5_env.color_mode == HSL then
|
|
return (h / 360) * L5_env.color_max[1]
|
|
else
|
|
return h
|
|
end
|
|
end
|
|
|
|
function lerpColor(_c1, _c2, _amt)
|
|
-- Clamp amt to [0, 1]
|
|
_amt = math.max(0, math.min(1, _amt))
|
|
|
|
-- Convert string colors if needed
|
|
if type(_c1) == "string" then
|
|
_c1 = toColor(_c1)
|
|
end
|
|
if type(_c2) == "string" then
|
|
_c2 = toColor(_c2)
|
|
end
|
|
|
|
-- Check if colors are normalized or raw arrays
|
|
local c1_normalized = _c1[1] <= 1.0 and _c1[2] <= 1.0 and _c1[3] <= 1.0
|
|
local c2_normalized = _c2[1] <= 1.0 and _c2[2] <= 1.0 and _c2[3] <= 1.0
|
|
|
|
-- Normalize colors if needed
|
|
local c1, c2
|
|
if c1_normalized then
|
|
c1 = {_c1[1] * L5_env.color_max[1], _c1[2] * L5_env.color_max[2], _c1[3] * L5_env.color_max[3], _c1[4] * L5_env.color_max[4]}
|
|
else
|
|
c1 = {_c1[1], _c1[2], _c1[3], _c1[4] or L5_env.color_max[4]}
|
|
end
|
|
|
|
if c2_normalized then
|
|
c2 = {_c2[1] * L5_env.color_max[1], _c2[2] * L5_env.color_max[2], _c2[3] * L5_env.color_max[3], _c2[4] * L5_env.color_max[4]}
|
|
else
|
|
c2 = {_c2[1], _c2[2], _c2[3], _c2[4] or L5_env.color_max[4]}
|
|
end
|
|
|
|
-- Interpolate in the current color mode
|
|
local result = {}
|
|
for i = 1, 4 do
|
|
result[i] = c1[i] + (c2[i] - c1[i]) * _amt
|
|
end
|
|
|
|
-- Convert back to normalized format (what toColor returns)
|
|
return {
|
|
result[1] / L5_env.color_max[1],
|
|
result[2] / L5_env.color_max[2],
|
|
result[3] / L5_env.color_max[3],
|
|
result[4] / L5_env.color_max[4]
|
|
}
|
|
end
|
|
|
|
----------------------- COLOR ------------------------
|
|
htmlColors = {
|
|
["aliceblue"] = {240, 248, 255},
|
|
["antiquewhite"] = {250, 235, 215},
|
|
["aqua"] = {0, 255, 255},
|
|
["aquamarine"] = {127, 255, 212},
|
|
["azure"] = {240, 255, 255},
|
|
["beige"] = {245, 245, 220},
|
|
["bisque"] = {255, 228, 196},
|
|
["black"] = {0, 0, 0},
|
|
["blanchedalmond"] = {255, 235, 205},
|
|
["blue"] = {0, 0, 255},
|
|
["blueviolet"] = {138, 43, 226},
|
|
["brown"] = {165, 42, 42},
|
|
["burlywood"] = {222, 184, 135},
|
|
["cadetblue"] = {95, 158, 160},
|
|
["chartreuse"] = {127, 255, 0},
|
|
["chocolate"] = {210, 105, 30},
|
|
["coral"] = {255, 127, 80},
|
|
["cornflowerblue"] = {100, 149, 237},
|
|
["cornsilk"] = {255, 248, 220},
|
|
["crimson"] = {220, 20, 60},
|
|
["cyan"] = {0, 255, 255},
|
|
["darkblue"] = {0, 0, 139},
|
|
["darkcyan"] = {0, 139, 139},
|
|
["darkgoldenrod"] = {184, 134, 11},
|
|
["darkgray"] = {169, 169, 169},
|
|
["darkgreen"] = {0, 100, 0},
|
|
["darkgrey"] = {169, 169, 169},
|
|
["darkkhaki"] = {189, 183, 107},
|
|
["darkmagenta"] = {139, 0, 139},
|
|
["darkolivegreen"] = {85, 107, 47},
|
|
["darkorange"] = {255, 140, 0},
|
|
["darkorchid"] = {153, 50, 204},
|
|
["darkred"] = {139, 0, 0},
|
|
["darksalmon"] = {233, 150, 122},
|
|
["darkseagreen"] = {143, 188, 139},
|
|
["darkslateblue"] = {72, 61, 139},
|
|
["darkslategray"] = {47, 79, 79},
|
|
["darkslategrey"] = {47, 79, 79},
|
|
["darkturquoise"] = {0, 206, 209},
|
|
["darkviolet"] = {148, 0, 211},
|
|
["deeppink"] = {255, 20, 147},
|
|
["deepskyblue"] = {0, 191, 255},
|
|
["dimgray"] = {105, 105, 105},
|
|
["dimgrey"] = {105, 105, 105},
|
|
["dodgerblue"] = {30, 144, 255},
|
|
["firebrick"] = {178, 34, 34},
|
|
["floralwhite"] = {255, 250, 240},
|
|
["forestgreen"] = {34, 139, 34},
|
|
["fuchsia"] = {255, 0, 255},
|
|
["gainsboro"] = {220, 220, 220},
|
|
["ghostwhite"] = {248, 248, 255},
|
|
["gold"] = {255, 215, 0},
|
|
["goldenrod"] = {218, 165, 32},
|
|
["gray"] = {128, 128, 128},
|
|
["green"] = {0, 128, 0},
|
|
["greenyellow"] = {173, 255, 47},
|
|
["grey"] = {128, 128, 128},
|
|
["honeydew"] = {240, 255, 240},
|
|
["hotpink"] = {255, 105, 180},
|
|
["indianred"] = {205, 92, 92},
|
|
["indigo"] = {75, 0, 130},
|
|
["ivory"] = {255, 255, 240},
|
|
["khaki"] = {240, 230, 140},
|
|
["lavender"] = {230, 230, 250},
|
|
["lavenderblush"] = {255, 240, 245},
|
|
["lawngreen"] = {124, 252, 0},
|
|
["lemonchiffon"] = {255, 250, 205},
|
|
["lightblue"] = {173, 216, 230},
|
|
["lightcoral"] = {240, 128, 128},
|
|
["lightcyan"] = {224, 255, 255},
|
|
["lightgoldenrodyellow"] = {250, 250, 210},
|
|
["lightgray"] = {211, 211, 211},
|
|
["lightgreen"] = {144, 238, 144},
|
|
["lightgrey"] = {211, 211, 211},
|
|
["lightpink"] = {255, 182, 193},
|
|
["lightsalmon"] = {255, 160, 122},
|
|
["lightseagreen"] = {32, 178, 170},
|
|
["lightskyblue"] = {135, 206, 250},
|
|
["lightslategray"] = {119, 136, 153},
|
|
["lightslategrey"] = {119, 136, 153},
|
|
["lightsteelblue"] = {176, 196, 222},
|
|
["lightyellow"] = {255, 255, 224},
|
|
["lime"] = {0, 255, 0},
|
|
["limegreen"] = {50, 205, 50},
|
|
["linen"] = {250, 240, 230},
|
|
["magenta"] = {255, 0, 255},
|
|
["maroon"] = {128, 0, 0},
|
|
["mediumaquamarine"] = {102, 205, 170},
|
|
["mediumblue"] = {0, 0, 205},
|
|
["mediumorchid"] = {186, 85, 211},
|
|
["mediumpurple"] = {147, 112, 219},
|
|
["mediumseagreen"] = {60, 179, 113},
|
|
["mediumslateblue"] = {123, 104, 238},
|
|
["mediumspringgreen"] = {0, 250, 154},
|
|
["mediumturquoise"] = {72, 209, 204},
|
|
["mediumvioletred"] = {199, 21, 133},
|
|
["midnightblue"] = {25, 25, 112},
|
|
["mintcream"] = {245, 255, 250},
|
|
["mistyrose"] = {255, 228, 225},
|
|
["moccasin"] = {255, 228, 181},
|
|
["navajowhite"] = {255, 222, 173},
|
|
["navy"] = {0, 0, 128},
|
|
["oldlace"] = {253, 245, 230},
|
|
["olive"] = {128, 128, 0},
|
|
["olivedrab"] = {107, 142, 35},
|
|
["orange"] = {255, 165, 0},
|
|
["orangered"] = {255, 69, 0},
|
|
["orchid"] = {218, 112, 214},
|
|
["palegoldenrod"] = {238, 232, 170},
|
|
["palegreen"] = {152, 251, 152},
|
|
["paleturquoise"] = {175, 238, 238},
|
|
["palevioletred"] = {219, 112, 147},
|
|
["papayawhip"] = {255, 239, 213},
|
|
["peachpuff"] = {255, 218, 185},
|
|
["peru"] = {205, 133, 63},
|
|
["pink"] = {255, 192, 203},
|
|
["plum"] = {221, 160, 221},
|
|
["powderblue"] = {176, 224, 230},
|
|
["purple"] = {128, 0, 128},
|
|
["rebeccapurple"] = {102, 51, 153},
|
|
["red"] = {255, 0, 0},
|
|
["rosybrown"] = {188, 143, 143},
|
|
["royalblue"] = {65, 105, 225},
|
|
["saddlebrown"] = {139, 69, 19},
|
|
["salmon"] = {250, 128, 114},
|
|
["sandybrown"] = {244, 164, 96},
|
|
["seagreen"] = {46, 139, 87},
|
|
["seashell"] = {255, 245, 238},
|
|
["sienna"] = {160, 82, 45},
|
|
["silver"] = {192, 192, 192},
|
|
["skyblue"] = {135, 206, 235},
|
|
["slateblue"] = {106, 90, 205},
|
|
["slategray"] = {112, 128, 144},
|
|
["slategrey"] = {112, 128, 144},
|
|
["snow"] = {255, 250, 250},
|
|
["springgreen"] = {0, 255, 127},
|
|
["steelblue"] = {70, 130, 180},
|
|
["tan"] = {210, 180, 140},
|
|
["teal"] = {0, 128, 128},
|
|
["thistle"] = {216, 191, 216},
|
|
["tomato"] = {255, 99, 71},
|
|
["turquoise"] = {64, 224, 208},
|
|
["violet"] = {238, 130, 238},
|
|
["wheat"] = {245, 222, 179},
|
|
["white"] = {255, 255, 255},
|
|
["whitesmoke"] = {245, 245, 245},
|
|
["yellow"] = {255, 255, 0},
|
|
["yellowgreen"] = {154, 205, 50}
|
|
}
|
|
|
|
function rectMode(_mode)
|
|
if _mode == CORNER or _mode == CORNERS or _mode == CENTER or _mode == RADIUS then
|
|
L5_env.rect_mode = _mode
|
|
else
|
|
error("rectMode() must be CORNER, CORNERS, CENTER, or RADIUS")
|
|
end
|
|
end
|
|
|
|
function ellipseMode(_mode)
|
|
if _mode == CENTER or _mode == CORNER or _mode == CORNERS or _mode == RADIUS then
|
|
L5_env.ellipse_mode = _mode
|
|
else
|
|
error("ellipseMode() must be CENTER, CORNER, CORNERS, or RADIUS")
|
|
end
|
|
end
|
|
|
|
function imageMode(_mode)
|
|
if _mode == CORNER or _mode == CENTER or _mode == CORNERS then
|
|
L5_env.image_mode = _mode
|
|
else
|
|
error("imageMode() must be CORNER, CENTER, or CORNERS")
|
|
end
|
|
end
|
|
|
|
function noFill()
|
|
L5_env.fill_mode="line"
|
|
end
|
|
|
|
function strokeWeight(_w)
|
|
love.graphics.setLineWidth(_w)
|
|
love.graphics.setPointSize(_w) --also sets sizing on points
|
|
end
|
|
|
|
function strokeJoin(_style)
|
|
love.graphics.setLineJoin(_style)
|
|
end
|
|
|
|
function noSmooth()
|
|
love.graphics.setDefaultFilter("nearest", "nearest", 1)
|
|
love.graphics.setLineStyle('rough')
|
|
|
|
end
|
|
|
|
function smooth()
|
|
love.graphics.setDefaultFilter("linear", "linear", 1)
|
|
love.graphics.setLineStyle('smooth')
|
|
end
|
|
|
|
function stroke(_r,_g,_b,_a)
|
|
L5_env.stroke_color = toColor(_r,_g,_b,_a)
|
|
end
|
|
|
|
function noStroke()
|
|
L5_env.stroke_color={0,0,0,0}
|
|
end
|
|
|
|
------------------ RENDERING ------------------------
|
|
function createGraphics(_width, _height)
|
|
local pg = {}
|
|
|
|
-- Create the offscreen buffer
|
|
pg._canvas = love.graphics.newCanvas(_width, _height)
|
|
pg.width = _width or width
|
|
pg.height = _height or height
|
|
pg._previousCanvas = nil
|
|
pg._drawing = false
|
|
|
|
-- Begin drawing to this graphics buffer
|
|
function pg:beginDraw()
|
|
if self._drawing then
|
|
error("beginDraw() called while already drawing to this buffer")
|
|
end
|
|
self._previousCanvas = love.graphics.getCanvas()
|
|
love.graphics.setCanvas(self._canvas)
|
|
self._drawing = true
|
|
end
|
|
|
|
-- End drawing to this graphics buffer
|
|
function pg:endDraw()
|
|
if not self._drawing then
|
|
error("endDraw() called without beginDraw()")
|
|
end
|
|
love.graphics.setCanvas(self._previousCanvas)
|
|
self._previousCanvas = nil
|
|
self._drawing = false
|
|
end
|
|
|
|
-- Get the canvas for drawing to screen
|
|
function pg:getCanvas()
|
|
return self._canvas
|
|
end
|
|
|
|
return pg
|
|
end
|
|
|
|
-------------------- VERTEX -------------------------
|
|
|
|
function texture(_img)
|
|
-- to be applied to vertices
|
|
L5_env.currentTexture = _img
|
|
L5_env.useTexture = true
|
|
end
|
|
|
|
function textureMode(_mode)
|
|
-- Set how texture coordinates are interpreted
|
|
-- NORMAL - coordinates are 0 to 1 (default)
|
|
-- IMAGE - coordinates are in pixel dimensions
|
|
if _mode == NORMAL or _mode == IMAGE then
|
|
L5_env.textureMode = _mode
|
|
else
|
|
error("textureMode must be NORMAL or IMAGE")
|
|
end
|
|
end
|
|
|
|
function textureWrap(_mode)
|
|
-- Set texture wrapping mode
|
|
-- Valid modes: CLAMP or REPEAT
|
|
if _mode == CLAMP or _mode == REPEAT then
|
|
L5_env.textureWrap = _mode
|
|
else
|
|
error("textureWrap must be CLAMP or REPEAT")
|
|
end
|
|
end
|
|
|
|
function beginShape()
|
|
-- reset custom shape vertices table
|
|
L5_env.vertices = {}
|
|
L5_env.useTexture = false
|
|
end
|
|
|
|
function vertex(_x, _y, _u, _v)
|
|
-- add vertex (x, y) to the custom shape vertices table
|
|
if _u ~= nil and _v ~= nil then
|
|
local texU, texV = _u, _v
|
|
|
|
if L5_env.textureMode == IMAGE and L5_env.currentTexture then
|
|
-- Convert from pixel coordinates to normalized 0-1 range
|
|
texU = _u / L5_env.currentTexture:getWidth()
|
|
texV = _v / L5_env.currentTexture:getHeight()
|
|
end
|
|
table.insert(L5_env.vertices, {_x, _y, texU, texV})
|
|
else
|
|
table.insert(L5_env.vertices, _x)
|
|
table.insert(L5_env.vertices, _y)
|
|
end
|
|
end
|
|
|
|
function endShape()
|
|
-- draw the custom shape
|
|
if #L5_env.vertices > 0 then
|
|
if L5_env.useTexture and L5_env.currentTexture then
|
|
-- Use mesh for textured polygon
|
|
local mesh = love.graphics.newMesh(L5_env.vertices, "fan")
|
|
mesh:setTexture(L5_env.currentTexture)
|
|
|
|
-- Apply texture wrap mode
|
|
L5_env.currentTexture:setWrap(L5_env.textureWrap, L5_env.textureWrap)
|
|
|
|
love.graphics.draw(mesh)
|
|
else
|
|
-- Use regular polygon for non-textured shapes
|
|
love.graphics.polygon("fill", L5_env.vertices)
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.polygon("line", L5_env.vertices)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
end
|
|
end
|
|
|
|
function bezier(x1,y1,x2,y2,x3,y3,x4,y4)
|
|
local curve = love.math.newBezierCurve({x1,y1,x2,y2,x3,y3,x4,y4})
|
|
local points = curve:render()
|
|
|
|
-- Draw fill if fill mode is set
|
|
if L5_env.fill_mode == "fill" then
|
|
-- Close the shape by connecting end point back to start
|
|
local closedPoints = {}
|
|
for i, v in ipairs(points) do
|
|
table.insert(closedPoints, v)
|
|
end
|
|
-- Add line back to start to close the shape
|
|
table.insert(closedPoints, x1)
|
|
table.insert(closedPoints, y1)
|
|
|
|
love.graphics.polygon("fill", closedPoints)
|
|
end
|
|
|
|
-- Draw stroke
|
|
local r, g, b, a = love.graphics.getColor()
|
|
love.graphics.setColor(unpack(L5_env.stroke_color))
|
|
love.graphics.line(points)
|
|
love.graphics.setColor(r, g, b, a)
|
|
end
|
|
|
|
--catmull-rom spline - generated
|
|
-- curve(x1,y1,x2,y2,x3,y3,x4,y4)
|
|
-- x1,y1: first control point (not drawn)
|
|
-- x2,y2: first anchor point (curve starts here)
|
|
-- x3,y3: second anchor point (curve ends here)
|
|
-- x4,y4: last control point (not drawn)
|
|
function curve(x1, y1, x2, y2, x3, y3, x4, y4)
|
|
local points = {}
|
|
local segments = 20 -- Number of line segments to approximate the curve
|
|
|
|
-- Generate points along the curve
|
|
for i = 0, segments do
|
|
local t = i / segments
|
|
|
|
-- Catmull-Rom spline formula
|
|
local t2 = t * t
|
|
local t3 = t2 * t
|
|
|
|
-- Basis functions for Catmull-Rom spline
|
|
local b1 = -0.5 * t3 + t2 - 0.5 * t
|
|
local b2 = 1.5 * t3 - 2.5 * t2 + 1
|
|
local b3 = -1.5 * t3 + 2 * t2 + 0.5 * t
|
|
local b4 = 0.5 * t3 - 0.5 * t2
|
|
|
|
-- Calculate point coordinates
|
|
local x = b1 * x1 + b2 * x2 + b3 * x3 + b4 * x4
|
|
local y = b1 * y1 + b2 * y2 + b3 * y3 + b4 * y4
|
|
|
|
table.insert(points, x)
|
|
table.insert(points, y)
|
|
end
|
|
|
|
-- Draw the curve using love.graphics.line
|
|
if #points >= 4 then
|
|
love.graphics.line(points)
|
|
end
|
|
end
|
|
|
|
--------------------- MATH --------------------------
|
|
function fract(_n)
|
|
return _n - int(_n)
|
|
end
|
|
|
|
function log(_n)
|
|
return math.log(_n)
|
|
end
|
|
|
|
function pow(n, e)
|
|
return n ^ e
|
|
end
|
|
|
|
function exp(n)
|
|
return math.exp(n)
|
|
end
|
|
|
|
function norm(val, start, stop)
|
|
-- normalize the value to 0-1 range
|
|
return (val - start) / (stop - start)
|
|
end
|
|
|
|
function lerp(start, stop, amt)
|
|
return start + (stop - start) * amt
|
|
end
|
|
|
|
function sq(n)
|
|
return n * n
|
|
end
|
|
|
|
function sqrt(n)
|
|
return math.sqrt(n)
|
|
end
|
|
|
|
function random(_a,_b)
|
|
if _b then
|
|
return love.math.random()*(_b-_a)+_a
|
|
elseif _a then
|
|
if type(_a) == 'table' then
|
|
-- more robust in case a table isn't ordered by integers
|
|
local keyset = {}
|
|
for k in pairs(_a) do
|
|
table.insert(keyset, k)
|
|
end
|
|
return _a[keyset[math.floor(love.math.random() * #keyset) + 1]]
|
|
elseif type(_a) == 'number' then
|
|
return love.math.random()*_a
|
|
end
|
|
else
|
|
return love.math.random()
|
|
end
|
|
end
|
|
|
|
function randomSeed(seed)
|
|
love.math.setRandomSeed(seed)
|
|
end
|
|
|
|
function noise(_x,_y,_z)
|
|
return love.math.noise(_x,_y,_z)
|
|
end
|
|
|
|
--self-contained, optional params
|
|
randomGaussian = (function()
|
|
local hasSpare = false
|
|
local spare = 0
|
|
|
|
return function(mean, sd)
|
|
mean = mean or 0
|
|
sd = sd or 1
|
|
|
|
local val
|
|
|
|
if hasSpare then
|
|
val = spare
|
|
hasSpare = false
|
|
else
|
|
local u, v, s
|
|
repeat
|
|
u = math.random() * 2 - 1
|
|
v = math.random() * 2 - 1
|
|
s = u * u + v * v
|
|
until s > 0 and s < 1
|
|
|
|
s = math.sqrt(-2 * math.log(s) / s)
|
|
val = u * s
|
|
spare = v * s
|
|
hasSpare = true
|
|
end
|
|
|
|
return val * sd + mean
|
|
end
|
|
end)()
|
|
|
|
function abs(_a)
|
|
return math.abs(_a)
|
|
end
|
|
|
|
function round(n, decimals)
|
|
decimals = decimals or 0
|
|
local mult = 10 ^ decimals
|
|
return math.floor(n * mult + 0.5 * (n >= 0 and 1 or -1)) / mult
|
|
end
|
|
|
|
function int(_a)
|
|
-- Handle table input
|
|
if type(_a) == "table" then
|
|
local result = {}
|
|
for i, v in ipairs(_a) do
|
|
result[i] = int(v) -- Recursively convert each element
|
|
end
|
|
return result
|
|
end
|
|
|
|
local num
|
|
|
|
if type(_a) == "string" then
|
|
num = tonumber(_a)
|
|
if num == nil then return nil end
|
|
elseif type(_a) == "boolean" then
|
|
num = _a and 1 or 0
|
|
elseif type(_a) == "number" then
|
|
num = _a
|
|
else
|
|
return nil
|
|
end
|
|
|
|
-- check for invalid numbers
|
|
if num ~= num or num == math.huge or num == -math.huge then
|
|
return nil
|
|
end
|
|
|
|
-- strip decimal via floor
|
|
return math.floor(num)
|
|
end
|
|
|
|
function ceil(_a)
|
|
return math.ceil(_a)
|
|
end
|
|
|
|
function floor(_a)
|
|
return math.floor(_a)
|
|
end
|
|
|
|
function max(...)
|
|
local args = {...}
|
|
-- If single table argument, unpack it
|
|
if #args == 1 and type(args[1]) == "table" then
|
|
return math.max(unpack(args[1]))
|
|
else
|
|
return math.max(unpack(args))
|
|
end
|
|
end
|
|
|
|
function min(...)
|
|
local args = {...}
|
|
-- If single table argument, unpack it
|
|
if #args == 1 and type(args[1]) == "table" then
|
|
return math.min(unpack(args[1]))
|
|
else
|
|
return math.min(unpack(args))
|
|
end
|
|
end
|
|
|
|
function constrain(_val,_min,_max)
|
|
return math.max(_min, math.min(_val,_max));
|
|
end
|
|
|
|
function map(_val, inputMin, inputMax, outputMin, outputMax, withinBounds)
|
|
local mapped = outputMin + (outputMax - outputMin) * ((_val - inputMin) / (inputMax - inputMin))
|
|
|
|
if withinBounds then
|
|
if outputMin < outputMax then
|
|
mapped = math.max(outputMin, math.min(outputMax, mapped))
|
|
else
|
|
mapped = math.max(outputMax, math.min(outputMin, mapped))
|
|
end
|
|
end
|
|
|
|
return mapped
|
|
end
|
|
|
|
function dist(x1,y1,x2,y2)
|
|
return ((x2-x1)^2+(y2-y1)^2)^0.5
|
|
end
|
|
|
|
-------------------- TRIGONOMETRY --------------------
|
|
|
|
function angleMode(_mode)
|
|
if not _mode then
|
|
return L5_env.degree_mode
|
|
elseif _mode == RADIANS or _mode == DEGREES then
|
|
L5_env.degree_mode = _mode
|
|
else
|
|
error("angleMode() must be RADIANS or DEGREES")
|
|
end
|
|
end
|
|
|
|
function degrees(_angle)
|
|
return math.deg(_angle)
|
|
end
|
|
|
|
function radians(_angle)
|
|
return math.rad(_angle)
|
|
end
|
|
|
|
function sin(_angle)
|
|
if L5_env.degree_mode == RADIANS then
|
|
return math.sin(_angle)
|
|
else
|
|
return math.sin(radians(_angle))
|
|
end
|
|
end
|
|
|
|
function asin(_angle)
|
|
if L5_env.degree_mode == RADIANS then
|
|
return math.asin(_angle)
|
|
else
|
|
return math.asin(radians(_angle))
|
|
end
|
|
end
|
|
|
|
function cos(_angle)
|
|
if L5_env.degree_mode == RADIANS then
|
|
return math.cos(_angle)
|
|
else
|
|
return math.cos(radians(_angle))
|
|
end
|
|
end
|
|
|
|
function acos(_angle)
|
|
if L5_env.degree_mode == RADIANS then
|
|
return math.acos(_angle)
|
|
else
|
|
return math.acos(radians(_angle))
|
|
end
|
|
end
|
|
|
|
function tan(_angle)
|
|
if L5_env.degree_mode == RADIANS then
|
|
return math.tan(_angle)
|
|
else
|
|
return math.tan(radians(_angle))
|
|
end
|
|
end
|
|
|
|
function atan(_angle)
|
|
if L5_env.degree_mode == RADIANS then
|
|
return math.atan(_angle)
|
|
else
|
|
return math.atan(radians(_angle))
|
|
end
|
|
end
|
|
|
|
function atan2(y, x)
|
|
local angle = math.atan2(y, x) -- This returns radians
|
|
|
|
if L5_env.degree_mode == DEGREES then
|
|
return math.deg(angle) -- convert to degrees
|
|
else
|
|
return angle -- or keep in default radians
|
|
end
|
|
end
|
|
|
|
---------------------- DATA ------------------------
|
|
|
|
function boolean(n)
|
|
if type(n) == "table" then
|
|
local result = {}
|
|
for i, v in ipairs(n) do
|
|
result[i] = boolean(v) -- Recursively convert each element
|
|
end
|
|
return result
|
|
end
|
|
|
|
if type(n) == "string" then
|
|
return n == "true"
|
|
end
|
|
|
|
if type(n) == "number" then
|
|
return n ~= 0
|
|
end
|
|
|
|
if type(n) == "boolean" then
|
|
return n
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function byte(n)
|
|
if type(n) == "table" then
|
|
local result = {}
|
|
for i, v in ipairs(n) do
|
|
result[i] = byte(v)
|
|
end
|
|
return result
|
|
end
|
|
|
|
if type(n) == "boolean" then
|
|
return n and 1 or 0
|
|
end
|
|
|
|
-- Handle strings by converting to number first, or get first character's byte value
|
|
if type(n) == "string" then
|
|
-- Try to convert to number
|
|
local num = tonumber(n)
|
|
if num then
|
|
n = num
|
|
else
|
|
-- Get first character's byte value using string library
|
|
n = string.byte(n, 1) or 0
|
|
end
|
|
end
|
|
|
|
if type(n) == "number" then
|
|
-- Convert to integer
|
|
local int_val = math.floor(n)
|
|
|
|
-- Wrap to byte range (-128 to 127)
|
|
local wrapped = int_val % 256
|
|
|
|
-- Convert to signed byte range
|
|
if wrapped > 127 then
|
|
wrapped = wrapped - 256
|
|
end
|
|
|
|
return wrapped
|
|
end
|
|
|
|
-- Default case
|
|
return 0
|
|
end
|
|
|
|
function char(n)
|
|
if type(n) == "table" then
|
|
local result = {}
|
|
for i, v in ipairs(n) do
|
|
result[i] = char(v)
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- handle strings by converting to number first
|
|
if type(n) == "string" then
|
|
local num = tonumber(n)
|
|
if num then
|
|
n = math.floor(num)
|
|
else
|
|
-- if not a valid number, return first character or empty string
|
|
return n:sub(1, 1)
|
|
end
|
|
end
|
|
|
|
if type(n) == "number" then
|
|
local int_val = math.floor(n)
|
|
-- Convert to character using string.char
|
|
-- handle out of range values gracefully
|
|
if int_val >= 0 and int_val <= 1114111 then -- Valid Unicode range
|
|
local success, result = pcall(string.char, int_val)
|
|
if success then
|
|
return result
|
|
end
|
|
end
|
|
return ""
|
|
end
|
|
|
|
-- handle booleans via converting to string
|
|
if type(n) == "boolean" then
|
|
return n and "1" or "0"
|
|
end
|
|
|
|
-- default case
|
|
return ""
|
|
end
|
|
|
|
function float(str)
|
|
if type(str) == "table" then
|
|
local result = {}
|
|
for i, v in ipairs(str) do
|
|
result[i] = float(v)
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- pass through numbers
|
|
if type(str) == "number" then
|
|
return str
|
|
end
|
|
|
|
if type(str) == "boolean" then
|
|
return str and 1.0 or 0.0
|
|
end
|
|
|
|
if type(str) == "string" then
|
|
-- Trim whitespace
|
|
str = str:match("^%s*(.-)%s*$")
|
|
|
|
-- try to convert to number (returns nil on failure)
|
|
return tonumber(str)
|
|
end
|
|
|
|
-- Default case for anything else (including nil)
|
|
return nil
|
|
end
|
|
|
|
function hex(n, digits)
|
|
if type(n) == "table" then
|
|
local result = {}
|
|
for i, v in ipairs(n) do
|
|
result[i] = hex(v, digits)
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- Default to 8 digits if not specified (matches p5.js)
|
|
digits = digits or 8
|
|
|
|
-- convert to int
|
|
local int_val = math.floor(tonumber(n) or 0)
|
|
|
|
-- convert to hex string uppercase
|
|
local hex_str = string.format("%X", int_val)
|
|
|
|
-- pad with zeros if needed
|
|
if #hex_str < digits then
|
|
hex_str = string.rep("0", digits - #hex_str) .. hex_str
|
|
end
|
|
|
|
return hex_str
|
|
end
|
|
|
|
function str(n)
|
|
if type(n) == "table" then
|
|
local result = {}
|
|
for i, v in ipairs(n) do
|
|
result[i] = str(v)
|
|
end
|
|
return result
|
|
end
|
|
|
|
if type(n) == "boolean" then
|
|
return n and "true" or "false"
|
|
end
|
|
|
|
if type(n) == "number" then
|
|
return tostring(n)
|
|
end
|
|
|
|
-- pass through strings
|
|
if type(n) == "string" then
|
|
return n
|
|
end
|
|
|
|
return tostring(n)
|
|
end
|
|
|
|
function unchar(n)
|
|
if type(n) == "table" then
|
|
local result = {}
|
|
for i, v in ipairs(n) do
|
|
result[i] = unchar(v)
|
|
end
|
|
return result
|
|
end
|
|
|
|
if type(n) == "string" then
|
|
-- get byte value of the first character
|
|
if #n > 0 then
|
|
return string.byte(n, 1)
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
-- pass through numbers
|
|
if type(n) == "number" then
|
|
return n
|
|
end
|
|
|
|
-- default
|
|
return nil
|
|
end
|
|
|
|
function unhex(n)
|
|
if type(n) == "table" then
|
|
local result = {}
|
|
for i, v in ipairs(n) do
|
|
result[i] = unhex(v)
|
|
end
|
|
return result
|
|
end
|
|
|
|
if type(n) == "string" then
|
|
-- trim whitespace
|
|
n = n:match("^%s*(.-)%s*$")
|
|
|
|
-- convert hex string to number
|
|
return tonumber(n, 16) -- base 16
|
|
end
|
|
|
|
-- pass through any numbers
|
|
if type(n) == "number" then
|
|
return n
|
|
end
|
|
|
|
-- default
|
|
return nil
|
|
end
|
|
|
|
------------------- TYPOGRAPHY ---------------------
|
|
|
|
function loadFont(fontPath)
|
|
local font = love.graphics.newFont(fontPath)
|
|
-- Store the path so we can recreate the font at different sizes
|
|
L5_env.fontPaths[font] = fontPath
|
|
return font
|
|
end
|
|
|
|
function textFont(font, size)
|
|
-- Update size if provided
|
|
if size then
|
|
L5_env.currentFontSize = size
|
|
end
|
|
|
|
-- Font object - look up its stored path
|
|
L5_env.currentFontPath = L5_env.fontPaths[font]
|
|
if L5_env.currentFontPath then
|
|
-- Recreate font with current size using stored path
|
|
L5_env.currentFont = love.graphics.newFont(L5_env.currentFontPath, L5_env.currentFontSize)
|
|
else
|
|
-- No path found, use font as-is (won't be resizable)
|
|
L5_env.currentFont = font
|
|
end
|
|
love.graphics.setFont(L5_env.currentFont)
|
|
end
|
|
|
|
function textSize(size)
|
|
L5_env.currentFontSize = size
|
|
if L5_env.currentFontPath then
|
|
-- We have a path, recreate with new size
|
|
L5_env.currentFont = love.graphics.newFont(L5_env.currentFontPath, size)
|
|
else
|
|
-- No path stored, use default font
|
|
L5_env.currentFont = love.graphics.newFont(size)
|
|
end
|
|
love.graphics.setFont(L5_env.currentFont)
|
|
end
|
|
|
|
function textWidth(text)
|
|
if L5_env.currentFont then
|
|
return L5_env.currentFont:getWidth(text)
|
|
end
|
|
return 0
|
|
end
|
|
|
|
function textHeight()
|
|
if L5_env.currentFont then
|
|
return L5_env.currentFont:getHeight()
|
|
end
|
|
return 0
|
|
end
|
|
|
|
--------------------- SYSTEM -----------------------
|
|
function exit()
|
|
os.exit()
|
|
end
|
|
|
|
function windowTitle(_title)
|
|
if _title ~= nil then
|
|
love.window.setTitle(_title)
|
|
else
|
|
return love.window.getTitle()
|
|
end
|
|
end
|
|
|
|
function resizeWindow(_w, _h)
|
|
if _w == nil or _h == nil then --check for 2 args
|
|
error("resizeWindow() requires two arguments: width and height")
|
|
end
|
|
if type(_w) ~= "number" or type(_h) ~= "number" then -- Check if args are numbers
|
|
error("resizeWindow() requires width and height to be numbers")
|
|
end
|
|
if _w <= 0 or _h <= 0 then -- Check for reasonable values
|
|
error("resizeWindow() requires positive width and height values")
|
|
end
|
|
|
|
-- clear active canvas first
|
|
love.graphics.setCanvas()
|
|
-- then resize
|
|
love.window.setMode(_w, _h)
|
|
|
|
-- manually resize window
|
|
love.resize(_w, _h)
|
|
end
|
|
|
|
function clear()
|
|
love.graphics.clear()
|
|
end
|
|
|
|
function displayDensity()
|
|
return love.graphics.getDPIScale()
|
|
end
|
|
|
|
function frameRate(_inp)
|
|
if _inp then --change frameRate
|
|
L5_env.framerate = _inp
|
|
else --get frameRate
|
|
return love.timer.getFPS( )
|
|
end
|
|
end
|
|
|
|
function noLoop()
|
|
L5_env.drawing = false
|
|
end
|
|
|
|
function loop()
|
|
L5_env.drawing = true
|
|
end
|
|
|
|
function isLooping()
|
|
if L5_env.drawing then
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
function redraw()
|
|
draw()
|
|
noLoop()
|
|
end
|
|
|
|
--------------------- TYPOGRAPHY ---------------------
|
|
|
|
function text(_msg,_x,_y,_w)
|
|
if _msg == nil then
|
|
return -- Don't draw anything if message is nil
|
|
end
|
|
_msg = tostring(_msg) -- Convert to string in case it's a number, boolean, etc.
|
|
|
|
local x_offset=0
|
|
local y_offset=0
|
|
local font = love.graphics.getFont()
|
|
|
|
-- set x-offset
|
|
if L5_env.textAlignX==LEFT then
|
|
x_offset = 0
|
|
elseif L5_env.textAlignX == RIGHT then
|
|
x_offset = font:getWidth(_msg)
|
|
elseif L5_env.textAlignX == CENTER then
|
|
x_offset = font:getWidth(_msg)/2
|
|
end
|
|
|
|
-- set y-offset
|
|
-- For wrapped text (when _w is specified), treat BASELINE as TOP
|
|
local effectiveAlignY = L5_env.textAlignY
|
|
if _w ~= nil and effectiveAlignY == BASELINE then
|
|
effectiveAlignY = TOP
|
|
end
|
|
|
|
if effectiveAlignY == BASELINE then
|
|
y_offset = font:getAscent()
|
|
elseif effectiveAlignY == TOP then
|
|
y_offset = 0
|
|
elseif effectiveAlignY == CENTER then
|
|
y_offset = font:getHeight()/2
|
|
elseif effectiveAlignY == BOTTOM then
|
|
y_offset = font:getHeight()
|
|
end
|
|
|
|
if _w ~= nil then
|
|
local wrapStyle = L5_env.textWrap
|
|
|
|
if wrapStyle == CHAR then
|
|
-- Manual character wrapping (ASCII only)
|
|
local wrappedText = ""
|
|
local currentLine = ""
|
|
local lineWidth = 0
|
|
|
|
for i = 1, #_msg do
|
|
local char = _msg:sub(i, i)
|
|
local charWidth = font:getWidth(char)
|
|
|
|
if lineWidth + charWidth > _w then
|
|
wrappedText = wrappedText .. currentLine .. "\n"
|
|
currentLine = char
|
|
lineWidth = charWidth
|
|
else
|
|
currentLine = currentLine .. char
|
|
lineWidth = lineWidth + charWidth
|
|
end
|
|
end
|
|
wrappedText = wrappedText .. currentLine
|
|
|
|
love.graphics.printf(wrappedText, _x - x_offset, _y - y_offset, _w, L5_env.textAlignX)
|
|
else
|
|
-- Default WORD wrapping (LÖVE's default behavior)
|
|
love.graphics.printf(_msg, _x - x_offset, _y - y_offset, _w, L5_env.textAlignX)
|
|
end
|
|
else
|
|
-- No specified max width/wrap
|
|
love.graphics.print(_msg, _x - x_offset, _y - y_offset)
|
|
end
|
|
end
|
|
|
|
function textAlign(x_alignment,y_alignment)
|
|
if x_alignment == LEFT or x_alignment == RIGHT or x_alignment == CENTER then
|
|
L5_env.textAlignX=x_alignment
|
|
end
|
|
if y_alignment and (y_alignment == TOP or y_alignment == CENTER or y_alignment == BOTTOM or y_alignment == BASELINE) then
|
|
L5_env.textAlignY=y_alignment
|
|
else
|
|
L5_env.textAlignY=BASELINE
|
|
end
|
|
end
|
|
|
|
function textWrap(_style)
|
|
-- If no argument, return current style
|
|
if _style == nil then
|
|
return L5_env.textWrap
|
|
end
|
|
|
|
-- Set the wrap style
|
|
if _style == WORD or _style == CHAR then
|
|
L5_env.textWrap = _style
|
|
else
|
|
error("textWrap() style must be WORD or CHAR")
|
|
end
|
|
end
|
|
|
|
---------------- LOADING & DISPLAYING ----------------
|
|
|
|
function loadImage(_filename)
|
|
local success, result = pcall(love.graphics.newImage, _filename)
|
|
|
|
if success then
|
|
return result
|
|
else
|
|
error("Failed to load image '" .. _filename .. "': " .. tostring(result))
|
|
end
|
|
end
|
|
|
|
function loadVideo(_filename)
|
|
local success, result = pcall(love.graphics.newVideo, _filename)
|
|
|
|
if not success then
|
|
error("Failed to load video '" .. _filename .. "': " .. tostring(result))
|
|
end
|
|
|
|
-- Create a wrapper with additional methods
|
|
local videoWrapper = {
|
|
_video = result,
|
|
_shouldLoop = false, -- Add loop flag
|
|
|
|
-- pause override
|
|
pause = function(self)
|
|
self._manuallyPaused = true
|
|
self._video:pause()
|
|
end,
|
|
|
|
-- stop method - pause and rewind
|
|
stop = function(self)
|
|
self._manuallyPaused = true
|
|
self._video:pause()
|
|
self._video:rewind()
|
|
end,
|
|
|
|
-- play override
|
|
play = function(self)
|
|
self._manuallyPaused = false
|
|
self._video:play()
|
|
end,
|
|
|
|
-- loop() method
|
|
loop = function(self)
|
|
self._shouldLoop = true
|
|
self._manuallyPaused = false
|
|
self._video:play()
|
|
end,
|
|
|
|
-- noLoop() method
|
|
noLoop = function(self)
|
|
self._shouldLoop = false
|
|
end,
|
|
|
|
-- time() method
|
|
time = function(self, t)
|
|
if t == nil then
|
|
return self._video:tell()
|
|
else
|
|
self._video:seek(t)
|
|
end
|
|
end,
|
|
|
|
-- volume() method
|
|
volume = function(self, val)
|
|
if val == nil then
|
|
local source = self._video:getSource()
|
|
return source and source:getVolume() or 1
|
|
else
|
|
local source = self._video:getSource()
|
|
if source then
|
|
source:setVolume(val)
|
|
end
|
|
end
|
|
end
|
|
}
|
|
|
|
-- Create metatable
|
|
setmetatable(videoWrapper, {
|
|
__index = function(t, key)
|
|
if rawget(t, key) then
|
|
return rawget(t, key)
|
|
end
|
|
local value = t._video[key]
|
|
if type(value) == "function" then
|
|
return function(_, ...) return value(t._video, ...) end
|
|
end
|
|
return value
|
|
end
|
|
})
|
|
|
|
-- Register video for loop tracking
|
|
L5_env.videos = L5_env.videos or {}
|
|
table.insert(L5_env.videos, videoWrapper)
|
|
|
|
return videoWrapper
|
|
end
|
|
|
|
function image(_img,_x,_y,_w,_h)
|
|
local originalWidth = _img:getWidth()
|
|
local originalHeight = _img:getHeight()
|
|
local xscale, yscale, ox, oy
|
|
|
|
if L5_env.image_mode == CENTER then
|
|
-- CENTER mode: _x,_y is center, _w,_h are width and height
|
|
xscale = _w and (_w/originalWidth) or 1
|
|
yscale = _h and (_h/originalHeight) or xscale
|
|
ox = originalWidth/2
|
|
oy = originalHeight/2
|
|
elseif L5_env.image_mode == CORNERS then
|
|
-- CORNERS mode: (_x,_y) is top-left corner, (_w,_h) is bottom-right corner
|
|
local width = _w - _x
|
|
local height = _h - _y
|
|
xscale = width / originalWidth
|
|
yscale = height / originalHeight
|
|
ox, oy = 0, 0
|
|
else -- CORNER mode (default)
|
|
-- CORNER mode: _x,_y is top-left, _w,_h are width and height
|
|
xscale = _w and (_w/originalWidth) or 1
|
|
yscale = _h and (_h/originalHeight) or xscale
|
|
ox, oy = 0, 0
|
|
end
|
|
|
|
love.graphics.draw(_img,_x,_y,0,xscale,yscale,ox,oy)
|
|
end
|
|
|
|
function tint(...)
|
|
local args = {...}
|
|
if #args == 1 and type(args[1]) == "table" then
|
|
L5_env.currentTint = toColor(unpack(args[1]))
|
|
else
|
|
L5_env.currentTint = toColor(...)
|
|
end
|
|
end
|
|
|
|
function noTint()
|
|
L5_env.currentTint = {1, 1, 1, 1}
|
|
end
|
|
|
|
-- Override love.graphics.draw to automatically apply tint
|
|
local originalDraw = love.graphics.draw
|
|
function love.graphics.draw(drawable, x, y, r, sx, sy, ox, oy, kx, ky)
|
|
local prevR, prevG, prevB, prevA = love.graphics.getColor()
|
|
|
|
-- Check if it's a video wrapper (our custom table)
|
|
local actualDrawable = drawable
|
|
if type(drawable) == "table" and drawable._video then
|
|
actualDrawable = drawable._video -- Unwrap to get the real video
|
|
end
|
|
|
|
-- Handle Image and Video objects
|
|
if type(actualDrawable) == "userdata" and
|
|
(actualDrawable:type() == "Image" or actualDrawable:type() == "Video") then
|
|
if L5_env.currentTint then
|
|
love.graphics.setColor(unpack(L5_env.currentTint))
|
|
else
|
|
love.graphics.setColor(1, 1, 1, 1) -- No tint = white
|
|
end
|
|
end
|
|
|
|
originalDraw(actualDrawable, x, y, r, sx, sy, ox, oy, kx, ky)
|
|
love.graphics.setColor(prevR, prevG, prevB, prevA)
|
|
end
|
|
|
|
function cursor(_cursor_icon, hotX, hotY)
|
|
love.mouse.setVisible(true)
|
|
local _cursor_icon = _cursor_icon or "arrow"
|
|
local hotX = hotX or 0
|
|
local hotY = hotY or 0
|
|
|
|
-- Check if it's a system cursor type
|
|
local systemCursors = {
|
|
"arrow", "ibeam", "wait", "crosshair", "waitarrow",
|
|
"sizenwse", "sizenesw", "sizewe", "sizens", "sizeall",
|
|
"no", "hand"
|
|
}
|
|
|
|
local isSystemCursor = false
|
|
for _, cursorType in ipairs(systemCursors) do
|
|
if _cursor_icon == cursorType then
|
|
isSystemCursor = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if isSystemCursor then
|
|
-- Use system cursor
|
|
local _cursor = love.mouse.getSystemCursor(_cursor_icon)
|
|
love.mouse.setCursor(_cursor)
|
|
elseif type(_cursor_icon) == "userdata" and _cursor_icon:type() == "ImageData" then
|
|
-- Use ImageData directly
|
|
local _cursor = love.mouse.newCursor(_cursor_icon, hotX, hotY)
|
|
love.mouse.setCursor(_cursor)
|
|
elseif type(_cursor_icon) == "string" then
|
|
-- Treat as file path to custom cursor image
|
|
local cursorImage = love.image.newImageData(_cursor_icon)
|
|
local _cursor = love.mouse.newCursor(cursorImage, hotX, hotY)
|
|
love.mouse.setCursor(_cursor)
|
|
end
|
|
end
|
|
|
|
function noCursor()
|
|
love.mouse.setVisible(false)
|
|
end
|
|
|
|
---------------------- Pixels ----------------------
|
|
|
|
function copy(source, sx, sy, sw, sh, dx, dy, dw, dh)
|
|
-- If source is nil, try to use the current canvas
|
|
if source == nil then
|
|
source = love.graphics.getCanvas()
|
|
|
|
-- If still nil, we can't copy from the screen
|
|
if source == nil then
|
|
error("copy() requires a source image or an active canvas")
|
|
end
|
|
end
|
|
|
|
local quad = love.graphics.newQuad(sx, sy, sw, sh,
|
|
source:getDimensions())
|
|
|
|
local scaleX = dw / sw
|
|
local scaleY = dh / sh
|
|
love.graphics.draw(source, quad, dx, dy, 0, scaleX, scaleY)
|
|
end
|
|
|
|
function blend(source, sx, sy, sw, sh, dx, dy, dw, dh, blendMode)
|
|
-- allows blend, normal, add, multiply, screen, lightest, darkest, replace
|
|
-- would need to be implemented with shaders: DIFFERENCE, EXCLUSION, OVERLAY, HARD_LIGHT, SOFT_LIGHT, DODGE, BURN
|
|
if source == nil then
|
|
source = love.graphics.getCanvas()
|
|
|
|
if source == nil then
|
|
error("blend() requires a source image or an active canvas")
|
|
end
|
|
end
|
|
|
|
local quad = love.graphics.newQuad(sx, sy, sw, sh,
|
|
source:getDimensions())
|
|
|
|
-- Save previous blend mode
|
|
local previousMode, previousAlphaMode = love.graphics.getBlendMode()
|
|
|
|
-- Map p5.js blend modes to LÖVE2D
|
|
local mode, alphaMode = "alpha", "alphamultiply"
|
|
|
|
if blendMode == BLEND or blendMode == NORMAL then
|
|
mode, alphaMode = "alpha", "alphamultiply"
|
|
elseif blendMode == ADD then
|
|
mode, alphaMode = "add", "alphamultiply"
|
|
elseif blendMode == MULTIPLY then
|
|
mode, alphaMode = "multiply", "premultiplied"
|
|
elseif blendMode == SCREEN then
|
|
mode, alphaMode = "screen", "premultiplied"
|
|
elseif blendMode == LIGHTEST then
|
|
mode, alphaMode = "lighten", "premultiplied"
|
|
elseif blendMode == DARKEST then
|
|
mode, alphaMode = "darken", "premultiplied"
|
|
elseif blendMode == REPLACE then
|
|
mode, alphaMode = "replace", "alphamultiply"
|
|
else
|
|
error("Unknown blend mode "..tostring(blendMode)..". Must be of type: BLEND, NORMAL, ADD, MULTIPLY, SCREEN, LIGHTEST, DARKEST, REPLACE.")
|
|
end
|
|
|
|
love.graphics.setBlendMode(mode, alphaMode)
|
|
|
|
local scaleX = dw / sw
|
|
local scaleY = dh / sh
|
|
love.graphics.draw(source, quad, dx, dy, 0, scaleX, scaleY)
|
|
|
|
love.graphics.setBlendMode(previousMode, previousAlphaMode)
|
|
end
|
|
|
|
function filter(_name, _param)
|
|
if _name == GRAY then
|
|
L5_env.filterOn = true
|
|
L5_env.filter = L5_filter.grayscale
|
|
elseif _name == THRESHOLD then
|
|
if _param then
|
|
L5_filter.threshold:send("threshold", _param)
|
|
end
|
|
L5_env.filterOn = true
|
|
L5_env.filter = L5_filter.threshold
|
|
elseif _name == INVERT then
|
|
L5_env.filterOn = true
|
|
L5_env.filter = L5_filter.invert
|
|
elseif _name == POSTERIZE then
|
|
if _param then
|
|
L5_filter.posterize:send("levels", _param)
|
|
end
|
|
L5_env.filterOn = true
|
|
L5_env.filter = L5_filter.posterize
|
|
elseif _name == BLUR then
|
|
if L5_filter.blurSupportsParameter then
|
|
-- Scale to match p5.js: their radius 4 = our radius 15
|
|
local radius = (_param or 4.0) * 5.5
|
|
L5_filter.blur_horizontal:send("blurRadius", radius)
|
|
L5_filter.blur_vertical:send("blurRadius", radius)
|
|
L5_env.filterOn = true
|
|
L5_env.filter = "blur_twopass"
|
|
elseif L5_filter.blur then
|
|
L5_env.filterOn = true
|
|
L5_env.filter = L5_filter.blur
|
|
else
|
|
print("Blur filter not available on this system")
|
|
end
|
|
elseif _name == ERODE then
|
|
if _param then
|
|
L5_filter.erode:send("strength", _param)
|
|
end
|
|
L5_env.filterOn = true
|
|
L5_env.filter = L5_filter.erode
|
|
elseif _name == DILATE then
|
|
if _param then
|
|
L5_filter.dilate:send("strength", _param)
|
|
end
|
|
L5_env.filterOn = true
|
|
L5_env.filter = L5_filter.dilate
|
|
else
|
|
error("Error: not a filter name.")
|
|
end
|
|
end
|
|
|
|
-- Load pixels from the back buffer into the pixels array
|
|
function loadPixels()
|
|
if not L5_env.backBuffer then
|
|
error("L5_env.backBuffer not initialized. Make sure L5 is loaded properly.")
|
|
end
|
|
|
|
-- Must unbind canvas to call newImageData() on it
|
|
local wasActive = love.graphics.getCanvas() == L5_env.backBuffer
|
|
love.graphics.setCanvas()
|
|
L5_env.imageData = L5_env.backBuffer:newImageData()
|
|
if wasActive then
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
end
|
|
|
|
local w = L5_env.imageData:getWidth()
|
|
local h = L5_env.imageData:getHeight()
|
|
|
|
-- Clear the pixels array
|
|
pixels = {}
|
|
|
|
-- Fill pixels array with RGBA values (0-255 like p5.js)
|
|
-- Index: (x + y * width) * 4
|
|
for y = 0, h - 1 do
|
|
for x = 0, w - 1 do
|
|
local r, g, b, a = L5_env.imageData:getPixel(x, y)
|
|
local idx = (x + y * w) * 4
|
|
pixels[idx] = r * 255
|
|
pixels[idx + 1] = g * 255
|
|
pixels[idx + 2] = b * 255
|
|
pixels[idx + 3] = a * 255
|
|
end
|
|
end
|
|
|
|
L5_env.pixelsLoaded = true -- Changed from pixelsLoaded to L5_env.pixelsLoaded
|
|
end
|
|
|
|
-- Update the back buffer with modified pixel data
|
|
function updatePixels()
|
|
if not L5_env.pixelsLoaded then
|
|
return
|
|
end
|
|
|
|
local w = L5_env.imageData:getWidth()
|
|
local h = L5_env.imageData:getHeight()
|
|
|
|
-- Write pixels array back to imageData
|
|
for y = 0, h - 1 do
|
|
for x = 0, w - 1 do
|
|
local idx = (x + y * w) * 4
|
|
local r = (pixels[idx] or 0) / 255 -- Changed from L5_env.pixels to pixels
|
|
local g = (pixels[idx + 1] or 0) / 255
|
|
local b = (pixels[idx + 2] or 0) / 255
|
|
local a = (pixels[idx + 3] or 255) / 255
|
|
L5_env.imageData:setPixel(x, y, r, g, b, a)
|
|
end
|
|
end
|
|
|
|
-- Create a new image from the modified imageData and draw it to the backBuffer
|
|
local tempImage = love.graphics.newImage(L5_env.imageData)
|
|
local wasActive = love.graphics.getCanvas() == L5_env.backBuffer
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
love.graphics.draw(tempImage, 0, 0)
|
|
if not wasActive then
|
|
love.graphics.setCanvas()
|
|
end
|
|
|
|
L5_env.pixelsLoaded = false
|
|
end
|
|
|
|
-- Helper function to get pixel index
|
|
function getPixelIndex(x, y)
|
|
local w = L5_env.imageData:getWidth()
|
|
return (x + y * w) * 4
|
|
end
|
|
|
|
-- Helper to set a pixel color (optional convenience function)
|
|
function setPixel(x, y, r, g, b, a)
|
|
local idx = getPixelIndex(x, y)
|
|
pixels[idx] = r
|
|
pixels[idx + 1] = g
|
|
pixels[idx + 2] = b
|
|
pixels[idx + 3] = a or 255
|
|
end
|
|
|
|
function get(x, y, w, h)
|
|
if not x then
|
|
-- No parameters: return entire window as image
|
|
local wasActive = love.graphics.getCanvas() == L5_env.backBuffer
|
|
love.graphics.setCanvas()
|
|
local imageData = L5_env.backBuffer:newImageData()
|
|
if wasActive then
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
end
|
|
return love.graphics.newImage(imageData)
|
|
elseif not w then
|
|
-- Two parameters: return pixel RGBA (0-255 range)
|
|
local wasActive = love.graphics.getCanvas() == L5_env.backBuffer
|
|
love.graphics.setCanvas()
|
|
local imageData = L5_env.backBuffer:newImageData()
|
|
local r, g, b, a = imageData:getPixel(x, y)
|
|
if wasActive then
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
end
|
|
return r * 255, g * 255, b * 255, a * 255
|
|
else
|
|
-- Four parameters: return sub-region as image
|
|
local wasActive = love.graphics.getCanvas() == L5_env.backBuffer
|
|
love.graphics.setCanvas()
|
|
local fullImageData = L5_env.backBuffer:newImageData()
|
|
|
|
-- Create a new ImageData for the sub-region
|
|
local subImageData = love.image.newImageData(w, h)
|
|
subImageData:paste(fullImageData, 0, 0, x, y, w, h)
|
|
|
|
if wasActive then
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
end
|
|
return love.graphics.newImage(subImageData)
|
|
end
|
|
end
|
|
|
|
function set(x, y, c)
|
|
if type(c) == "userdata" and c.type and c:type() == "Image" then
|
|
-- c is an image, draw it at x,y
|
|
local wasActive = love.graphics.getCanvas() == L5_env.backBuffer
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
love.graphics.draw(c, x, y)
|
|
if not wasActive then
|
|
love.graphics.setCanvas()
|
|
end
|
|
elseif type(c) == "table" then
|
|
-- c is a color table {r, g, b, a} (in 0-1 range)
|
|
-- Draw a 1x1 point at x,y with this color
|
|
local wasActive = love.graphics.getCanvas() == L5_env.backBuffer
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
local prevColor = {love.graphics.getColor()}
|
|
love.graphics.setColor(c[1], c[2], c[3], c[4] or 1)
|
|
love.graphics.points(x, y)
|
|
love.graphics.setColor(unpack(prevColor))
|
|
if not wasActive then
|
|
love.graphics.setCanvas()
|
|
end
|
|
elseif type(c) == "number" then
|
|
-- c is a grayscale value (0-255)
|
|
local wasActive = love.graphics.getCanvas() == L5_env.backBuffer
|
|
love.graphics.setCanvas(L5_env.backBuffer)
|
|
local prevColor = {love.graphics.getColor()}
|
|
local normalized = c / 255
|
|
love.graphics.setColor(normalized, normalized, normalized, 1)
|
|
love.graphics.points(x, y)
|
|
love.graphics.setColor(unpack(prevColor))
|
|
if not wasActive then
|
|
love.graphics.setCanvas()
|
|
end
|
|
end
|
|
end
|
|
|
|
--- shaders
|
|
local function createShaderSafe(shaderCode, fallbackMessage)
|
|
local success, shader = pcall(love.graphics.newShader, shaderCode)
|
|
if success then
|
|
return shader
|
|
else
|
|
print("Warning: " .. fallbackMessage)
|
|
return nil
|
|
end
|
|
end
|
|
|
|
L5_filter = {}
|
|
|
|
|
|
L5_filter.grayscale = createShaderSafe([[
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords)
|
|
{
|
|
vec4 pixel = Texel(texture, texture_coords);
|
|
float gray = dot(pixel.rgb, vec3(0.299, 0.587, 0.114));
|
|
return vec4(gray, gray, gray, pixel.a) * color;
|
|
}
|
|
]], "Grayscale shader failed to compile - filter unavailable")
|
|
|
|
--from https://www.love2d.org/forums/viewtopic.php?t=3733&start=300, modified to work on Mac
|
|
L5_filter.threshold = createShaderSafe([[
|
|
extern float soft;
|
|
extern float threshold;
|
|
vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords )
|
|
{
|
|
float f = soft * 0.5;
|
|
float a = threshold - f;
|
|
float b = threshold + f;
|
|
|
|
vec4 tx = Texel( texture, texture_coords );
|
|
float l = (tx.r + tx.g + tx.b) * 0.333333;
|
|
vec3 col = vec3( smoothstep(a, b, l) );
|
|
|
|
return vec4( col, 1.0 ) * color;
|
|
}
|
|
]], "Threshold shader failed to compile - filter unavailable")
|
|
|
|
-- from https://www.reddit.com/r/love2d/comments/ee8n0j/how_to_make_inverted_colornegative_shader/fcaouw5/
|
|
L5_filter.invert = createShaderSafe([[
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 pixel_coords)
|
|
{
|
|
vec4 col = Texel( texture, texture_coords );
|
|
return vec4(1.0-col.r, 1.0-col.g, 1.0-col.b, col.a) * color;
|
|
}
|
|
]], "Invert shader failed to compile - filter unavailable")
|
|
|
|
L5_filter.posterize = createShaderSafe([[
|
|
uniform float levels;
|
|
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
|
vec4 pixel = Texel(texture, texture_coords);
|
|
|
|
pixel.r = floor(pixel.r * levels) / levels;
|
|
pixel.g = floor(pixel.g * levels) / levels;
|
|
pixel.b = floor(pixel.b * levels) / levels;
|
|
|
|
return pixel * color;
|
|
}
|
|
]], "Posterize shader failed to compile - filter unavailable")
|
|
|
|
-- Two-pass blur matching p5.js 2D implementation
|
|
L5_filter.blur_horizontal = createShaderSafe([[
|
|
uniform float blurRadius;
|
|
uniform vec2 textureSize;
|
|
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
|
vec2 pixelSize = 1.0 / textureSize;
|
|
|
|
// Clamp to minimum radius to avoid divide by zero
|
|
float safeRadius = max(blurRadius, 0.01);
|
|
|
|
vec4 sum = vec4(0.0);
|
|
float totalWeight = 0.0;
|
|
|
|
const int maxSamples = 32;
|
|
|
|
// Horizontal pass only
|
|
for(int x = -maxSamples; x <= maxSamples; x++) {
|
|
float fx = float(x);
|
|
float distance = abs(fx);
|
|
|
|
if (distance > safeRadius) continue;
|
|
|
|
float radiusi = safeRadius - distance;
|
|
float weight = radiusi * radiusi;
|
|
|
|
vec2 offset = vec2(fx, 0.0) * pixelSize;
|
|
sum += Texel(texture, texture_coords + offset) * weight;
|
|
totalWeight += weight;
|
|
}
|
|
|
|
return (sum / totalWeight) * color;
|
|
}
|
|
]], "Horizontal blur pass failed to compile")
|
|
|
|
L5_filter.blur_vertical = createShaderSafe([[
|
|
uniform float blurRadius;
|
|
uniform vec2 textureSize;
|
|
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
|
vec2 pixelSize = 1.0 / textureSize;
|
|
|
|
// Clamp to minimum radius to avoid divide by zero
|
|
float safeRadius = max(blurRadius, 0.01);
|
|
|
|
vec4 sum = vec4(0.0);
|
|
float totalWeight = 0.0;
|
|
|
|
const int maxSamples = 32;
|
|
|
|
// Vertical pass only
|
|
for(int y = -maxSamples; y <= maxSamples; y++) {
|
|
float fy = float(y);
|
|
float distance = abs(fy);
|
|
|
|
if (distance > safeRadius) continue;
|
|
float radiusi = safeRadius - distance;
|
|
float weight = radiusi * radiusi;
|
|
|
|
vec2 offset = vec2(0.0, fy) * pixelSize;
|
|
sum += Texel(texture, texture_coords + offset) * weight;
|
|
totalWeight += weight;
|
|
}
|
|
|
|
return (sum / totalWeight) * color;
|
|
}
|
|
]], "Vertical blur pass failed to compile")
|
|
|
|
-- Track if two-pass blur is available
|
|
L5_filter.blurSupportsParameter = (L5_filter.blur_horizontal ~= nil and L5_filter.blur_vertical ~= nil)
|
|
|
|
-- If two-pass failed, create simple 3x3 Gaussian fallback
|
|
if not L5_filter.blurSupportsParameter then
|
|
L5_filter.blur = createShaderSafe([[
|
|
uniform vec2 textureSize;
|
|
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
|
vec2 pixelSize = 1.0 / textureSize;
|
|
vec4 sum = vec4(0.0);
|
|
|
|
// 3x3 Gaussian kernel (radius 1)
|
|
sum += Texel(texture, texture_coords + vec2(-1.0, -1.0) * pixelSize) * 1.0;
|
|
sum += Texel(texture, texture_coords + vec2( 0.0, -1.0) * pixelSize) * 2.0;
|
|
sum += Texel(texture, texture_coords + vec2( 1.0, -1.0) * pixelSize) * 1.0;
|
|
sum += Texel(texture, texture_coords + vec2(-1.0, 0.0) * pixelSize) * 2.0;
|
|
sum += Texel(texture, texture_coords + vec2( 0.0, 0.0) * pixelSize) * 4.0;
|
|
sum += Texel(texture, texture_coords + vec2( 1.0, 0.0) * pixelSize) * 2.0;
|
|
sum += Texel(texture, texture_coords + vec2(-1.0, 1.0) * pixelSize) * 1.0;
|
|
sum += Texel(texture, texture_coords + vec2( 0.0, 1.0) * pixelSize) * 2.0;
|
|
sum += Texel(texture, texture_coords + vec2( 1.0, 1.0) * pixelSize) * 1.0;
|
|
|
|
return (sum / 16.0) * color;
|
|
}
|
|
]], "Blur shader completely unavailable")
|
|
end
|
|
|
|
L5_filter.erode = createShaderSafe([[
|
|
uniform float strength;
|
|
uniform vec2 textureSize;
|
|
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
|
vec2 pixelSize = 1.0 / textureSize;
|
|
|
|
vec4 centerColor = Texel(texture, texture_coords);
|
|
vec4 result = centerColor;
|
|
|
|
// 3x3 erosion - unrolled for compatibility
|
|
vec2 offset;
|
|
vec4 neighborColor;
|
|
|
|
// Manually unroll the 3x3 kernel (excluding center)
|
|
offset = vec2(-1.0, -1.0) * pixelSize * strength;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
result = mix(result, min(result, neighborColor), 0.3);
|
|
|
|
offset = vec2(0.0, -1.0) * pixelSize * strength;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
result = mix(result, min(result, neighborColor), 0.3);
|
|
|
|
offset = vec2(1.0, -1.0) * pixelSize * strength;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
result = mix(result, min(result, neighborColor), 0.3);
|
|
|
|
offset = vec2(-1.0, 0.0) * pixelSize * strength;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
result = mix(result, min(result, neighborColor), 0.3);
|
|
|
|
offset = vec2(1.0, 0.0) * pixelSize * strength;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
result = mix(result, min(result, neighborColor), 0.3);
|
|
|
|
offset = vec2(-1.0, 1.0) * pixelSize * strength;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
result = mix(result, min(result, neighborColor), 0.3);
|
|
|
|
offset = vec2(0.0, 1.0) * pixelSize * strength;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
result = mix(result, min(result, neighborColor), 0.3);
|
|
|
|
offset = vec2(1.0, 1.0) * pixelSize * strength;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
result = mix(result, min(result, neighborColor), 0.3);
|
|
|
|
return result * color;
|
|
}
|
|
]], "Erode shader failed to compile - filter unavailable")
|
|
|
|
L5_filter.dilate = createShaderSafe([[
|
|
uniform float strength;
|
|
uniform float threshold;
|
|
uniform vec2 textureSize;
|
|
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
|
vec2 pixelSize = 1.0 / textureSize;
|
|
|
|
vec4 centerColor = Texel(texture, texture_coords);
|
|
vec4 maxColor = centerColor;
|
|
|
|
float centerBrightness = dot(centerColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
|
|
// Only dilate if center pixel is bright enough
|
|
if (centerBrightness > threshold) {
|
|
// Simplified 3x3 dilation
|
|
vec2 offset;
|
|
vec4 neighborColor;
|
|
float neighborBrightness;
|
|
float weight;
|
|
|
|
// Unroll 3x3 kernel (excluding center)
|
|
offset = vec2(-1.0, -1.0) * pixelSize;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
neighborBrightness = dot(neighborColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
if (neighborBrightness > threshold) {
|
|
weight = 1.0 - 1.414 / (strength + 1.0);
|
|
maxColor = max(maxColor, neighborColor * weight);
|
|
}
|
|
|
|
offset = vec2(0.0, -1.0) * pixelSize;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
neighborBrightness = dot(neighborColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
if (neighborBrightness > threshold) {
|
|
weight = 1.0 - 1.0 / (strength + 1.0);
|
|
maxColor = max(maxColor, neighborColor * weight);
|
|
}
|
|
|
|
offset = vec2(1.0, -1.0) * pixelSize;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
neighborBrightness = dot(neighborColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
if (neighborBrightness > threshold) {
|
|
weight = 1.0 - 1.414 / (strength + 1.0);
|
|
maxColor = max(maxColor, neighborColor * weight);
|
|
}
|
|
|
|
offset = vec2(-1.0, 0.0) * pixelSize;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
neighborBrightness = dot(neighborColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
if (neighborBrightness > threshold) {
|
|
weight = 1.0 - 1.0 / (strength + 1.0);
|
|
maxColor = max(maxColor, neighborColor * weight);
|
|
}
|
|
|
|
offset = vec2(1.0, 0.0) * pixelSize;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
neighborBrightness = dot(neighborColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
if (neighborBrightness > threshold) {
|
|
weight = 1.0 - 1.0 / (strength + 1.0);
|
|
maxColor = max(maxColor, neighborColor * weight);
|
|
}
|
|
|
|
offset = vec2(-1.0, 1.0) * pixelSize;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
neighborBrightness = dot(neighborColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
if (neighborBrightness > threshold) {
|
|
weight = 1.0 - 1.414 / (strength + 1.0);
|
|
maxColor = max(maxColor, neighborColor * weight);
|
|
}
|
|
|
|
offset = vec2(0.0, 1.0) * pixelSize;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
neighborBrightness = dot(neighborColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
if (neighborBrightness > threshold) {
|
|
weight = 1.0 - 1.0 / (strength + 1.0);
|
|
maxColor = max(maxColor, neighborColor * weight);
|
|
}
|
|
|
|
offset = vec2(1.0, 1.0) * pixelSize;
|
|
neighborColor = Texel(texture, texture_coords + offset);
|
|
neighborBrightness = dot(neighborColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
if (neighborBrightness > threshold) {
|
|
weight = 1.0 - 1.414 / (strength + 1.0);
|
|
maxColor = max(maxColor, neighborColor * weight);
|
|
}
|
|
}
|
|
|
|
return maxColor * color;
|
|
}
|
|
]], "Dilate shader failed to compile - filter unavailable")
|