-- 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, stencil=true}
      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)

        drawPrintBuffer()

        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, stencil=true} 
  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

  
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, stencil=true} 
  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, stencil=true}
  
  -- 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 drawPrintBuffer()
    if not L5_env.showPrintBuffer or #L5_env.printBuffer == 0 then
        return
    end
    
    love.graphics.push()
    love.graphics.origin()
    
    -- Save user's current state
    local userFont = love.graphics.getFont()
    local pr, pg, pb, pa = love.graphics.getColor()
    
    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
        local textWidth = font:getWidth(line)
        love.graphics.setColor(0, 0, 0, 0.7)
        love.graphics.rectangle('fill', 5, y, textWidth + 4, L5_env.printLineHeight)
        love.graphics.setColor(1, 1, 1)
        love.graphics.print(line, 5, y)
        y = y + L5_env.printLineHeight
    end
    
    -- Restore user's state
    love.graphics.setFont(userFont)
    love.graphics.setColor(pr, pg, pb, pa)
    
    love.graphics.pop()
end

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()
  local _, _, flags = love.window.getMode()
  love.window.setMode(_w, _h, { display = flags.display })

  -- 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, stencil=true} 
  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, stencil=true}

  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, stencil=true} 
  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, stencil=true}
  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.currentFont = love.graphics.getFont()
  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 lineWidth = 0
      local buffer = {}
      for i = 1, #_msg do
        local char = _msg:sub(i, i)
        local charWidth = font:getWidth(char)
        if lineWidth + charWidth > _w then
          table.insert(buffer, "\n")
          table.insert(buffer, char)
          lineWidth = charWidth
        else
          table.insert(buffer, char)
          lineWidth = lineWidth + charWidth
        end
      end
      wrappedText = table.concat(buffer)
      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, stencil=true}
    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, stencil=true}
    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, stencil=true}
        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, stencil=true}
        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, stencil=true}
        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, stencil=true}
        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, stencil=true}
        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, stencil=true}
        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")
