luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

init.lua (11918B)


      1 -- LuaJIT OS Paint App
      2 -- Uses ImageBuffer userdata for true in-place drawing (no string copies)
      3 
      4 -- Dialog is pre-loaded in the sandbox, no require() needed
      5 
      6 local toolbarHeight = 60
      7 local initialWidth = 400
      8 local initialHeight = 250 + toolbarHeight
      9 
     10 local window = app:newWindow("Paint", initialWidth, initialHeight)
     11 window.resizable = true  -- Allow resizing
     12 
     13 -- Canvas buffer (ImageBuffer userdata - true mutable buffer)
     14 local canvasBuffer = nil
     15 local canvasBufferWidth = nil
     16 local canvasBufferHeight = nil
     17 
     18 -- Background image (native C image for display)
     19 local bgImage = nil
     20 
     21 -- Background image dimensions (for saving)
     22 local bgImageWidth = nil
     23 local bgImageHeight = nil
     24 
     25 -- Current file path
     26 local currentFile = nil
     27 
     28 -- Drawing state
     29 local isDrawing = false
     30 local lastX, lastY = nil, nil
     31 
     32 -- Colors
     33 local brushColor = 0x000000
     34 local selectedColor = 1
     35 local colors = {
     36     0x000000,  -- Black
     37     0xFFFFFF,  -- White
     38     0xFF0000,  -- Red
     39     0x00FF00,  -- Green
     40     0x0000FF,  -- Blue
     41     0xFFFF00,  -- Yellow
     42     0xFF00FF,  -- Magenta
     43     0x00FFFF,  -- Cyan
     44 }
     45 
     46 local brushRadius = 3
     47 
     48 local function isInside(x, y, rx, ry, rw, rh)
     49     return x >= rx and x < rx + rw and y >= ry and y < ry + rh
     50 end
     51 
     52 -- Ensure canvas buffer exists and matches window size
     53 local function ensureCanvasBuffer()
     54     local canvasW = window.width
     55     local canvasH = window.height - toolbarHeight
     56 
     57     if not canvasBuffer or canvasBufferWidth ~= canvasW or canvasBufferHeight ~= canvasH then
     58         -- Create new ImageBuffer userdata (true mutable buffer)
     59         local newBuffer = ImageBufferNew(canvasW, canvasH)
     60         newBuffer:fill(0xFFFFFF)
     61 
     62         -- If we had an old buffer, copy it over (for resize)
     63         if canvasBuffer then
     64             local oldW, oldH = canvasBuffer:getSize()
     65             for y = 0, math.min(oldH, canvasH) - 1 do
     66                 for x = 0, math.min(oldW, canvasW) - 1 do
     67                     local r, g, b = canvasBuffer:getPixel(x, y)
     68                     if r then
     69                         newBuffer:setPixel(x, y, r, g, b)
     70                     end
     71                 end
     72             end
     73         end
     74 
     75         canvasBuffer = newBuffer
     76         canvasBufferWidth = canvasW
     77         canvasBufferHeight = canvasH
     78     end
     79 end
     80 
     81 -- Draw a filled circle to the canvas buffer (in-place, no copies)
     82 local function drawCircleToBuffer(cx, cy, radius, color)
     83     if not canvasBuffer then return end
     84     canvasBuffer:fillCircle(cx, cy, radius, color)
     85 end
     86 
     87 -- Draw line between two points with thickness (in-place, no copies)
     88 local function addLine(x1, y1, x2, y2)
     89     if not canvasBuffer then return end
     90     canvasBuffer:drawLine(x1, y1, x2, y2, brushRadius * 2, brushColor)
     91 end
     92 
     93 -- Legacy addLine implementation for fallback (not used when C functions available)
     94 local function addLineLegacy(x1, y1, x2, y2)
     95     local dx = math.abs(x2 - x1)
     96     local dy = math.abs(y2 - y1)
     97     local sx = x1 < x2 and 1 or -1
     98     local sy = y1 < y2 and 1 or -1
     99     local err = dx - dy
    100 
    101     while true do
    102         -- Draw point directly to buffer
    103         drawCircleToBuffer(x1, y1, brushRadius, brushColor)
    104 
    105         if x1 == x2 and y1 == y2 then break end
    106         local e2 = 2 * err
    107         if e2 > -dy then
    108             err = err - dy
    109             x1 = x1 + sx
    110         end
    111         if e2 < dx then
    112             err = err + dx
    113             y1 = y1 + sy
    114         end
    115     end
    116 end
    117 
    118 -- Load image file (BMP, PNG, or JPEG)
    119 local function loadImage(path)
    120     if not path then return false end
    121 
    122     local data = fs:read(path)
    123     if not data then return false end
    124 
    125     local ext = path:lower():match("%.([^%.]+)$")
    126 
    127     -- Destroy old image if exists
    128     if bgImage and ImageDestroy then
    129         ImageDestroy(bgImage)
    130         bgImage = nil
    131     end
    132     bgImageWidth = nil
    133     bgImageHeight = nil
    134 
    135     -- Load native image for display
    136     local nativeImg = nil
    137     if ext == "bmp" and BMPLoad then
    138         nativeImg = BMPLoad(data)
    139     elseif ext == "png" and PNGLoad then
    140         nativeImg = PNGLoad(data)
    141     elseif (ext == "jpeg" or ext == "jpg") and JPEGLoad then
    142         nativeImg = JPEGLoad(data)
    143     end
    144 
    145     if nativeImg then
    146         bgImage = nativeImg
    147         -- Get dimensions from native image
    148         if ImageGetWidth and ImageGetHeight then
    149             bgImageWidth = ImageGetWidth(nativeImg)
    150             bgImageHeight = ImageGetHeight(nativeImg)
    151         end
    152         currentFile = path
    153 
    154         -- Load image into canvas buffer using Image.open
    155         Image.fsOverride = fs
    156         local luaImg, err = Image.open(path)
    157         Image.fsOverride = nil
    158 
    159         if luaImg then
    160             local imgW, imgH = luaImg:getSize()
    161             canvasBuffer = Image.new(imgW, imgH, false)
    162             canvasBuffer:fill(0xFFFFFF)
    163             -- Copy pixels
    164             for y = 0, imgH - 1 do
    165                 for x = 0, imgW - 1 do
    166                     local r, g, b = luaImg:getPixel(x, y)
    167                     if r then
    168                         canvasBuffer:setPixel(x, y, r, g, b)
    169                     end
    170                 end
    171             end
    172             canvasBufferWidth = imgW
    173             canvasBufferHeight = imgH
    174         end
    175 
    176         return true
    177     end
    178 
    179     return false
    180 end
    181 
    182 -- Button definitions
    183 local buttons = {
    184     {x = 5, y = 5, w = 40, h = 20, label = "New", action = function()
    185         -- Clear canvas buffer
    186         if canvasBuffer then
    187             canvasBuffer:fill(0xFFFFFF)
    188         end
    189         if bgImage and ImageDestroy then
    190             ImageDestroy(bgImage)
    191             bgImage = nil
    192         end
    193         bgImageWidth = nil
    194         bgImageHeight = nil
    195         currentFile = nil
    196         window:markDirty()
    197     end},
    198     {x = 50, y = 5, w = 40, h = 20, label = "Open", action = function()
    199         local dlg = Dialog.fileOpen("/", {
    200             app = app,
    201             fs = fs,
    202             title = "Open Image",
    203             filter = {"bmp", "png", "jpg", "jpeg"}
    204         })
    205         dlg:openDialog(function(path)
    206             if path then
    207                 loadImage(path)
    208                 window:markDirty()
    209             end
    210         end)
    211     end},
    212     {x = 95, y = 5, w = 40, h = 20, label = "Save", action = function()
    213         local defaultName = "painting.png"
    214         if currentFile then
    215             defaultName = currentFile:match("([^/]+)$") or defaultName
    216         end
    217         local dlg = Dialog.fileSave("/home", defaultName, {
    218             app = app,
    219             fs = fs,
    220             title = "Save Image"
    221         })
    222         dlg:openDialog(function(path)
    223             if path then
    224                 -- Ensure we have a canvas buffer
    225                 ensureCanvasBuffer()
    226 
    227                 if not canvasBuffer then
    228                     print("Paint: No canvas to save")
    229                     return
    230                 end
    231 
    232                 -- Detect format from extension
    233                 local ext = path:lower():match("%.([^%.]+)$")
    234 
    235                 -- If no extension, try to detect from original file's magic bytes
    236                 if not ext and currentFile then
    237                     local origData = fs:read(currentFile)
    238                     if origData and #origData >= 4 then
    239                         local b1, b2, b3, b4 = origData:byte(1, 4)
    240                         if b1 == 0x42 and b2 == 0x4D then
    241                             ext = "bmp"
    242                         elseif b1 == 0x89 and b2 == 0x50 and b3 == 0x4E and b4 == 0x47 then
    243                             ext = "png"
    244                         elseif b1 == 0xFF and b2 == 0xD8 then
    245                             ext = "jpg"
    246                         end
    247                     end
    248                 end
    249 
    250                 -- Default to PNG if still unknown
    251                 ext = ext or "png"
    252 
    253                 local success, err
    254                 if ext == "bmp" then
    255                     success, err = canvasBuffer:saveAsBMP(path, {fs = fs})
    256                 elseif ext == "jpg" or ext == "jpeg" then
    257                     success, err = canvasBuffer:saveAsJPEG(path, {fs = fs})
    258                 elseif ext == "png" then
    259                     success, err = canvasBuffer:saveAsPNG(path, {fs = fs})
    260                 else
    261                     -- Unknown extension, add .png and save
    262                     path = path .. ".png"
    263                     success, err = canvasBuffer:saveAsPNG(path, {fs = fs})
    264                 end
    265 
    266                 if success then
    267                     print("Saved to: " .. path)
    268                     currentFile = path
    269                 else
    270                     print("Save failed: " .. (err or "unknown error"))
    271                 end
    272             end
    273         end)
    274     end},
    275 }
    276 
    277 -- Mouse down - start drawing
    278 window.onMouseDown = function(mx, my)
    279     if my < toolbarHeight then
    280         -- Check file buttons
    281         for _, btn in ipairs(buttons) do
    282             if isInside(mx, my, btn.x, btn.y, btn.w, btn.h) then
    283                 btn.action()
    284                 return
    285             end
    286         end
    287 
    288         -- Check color palette
    289         local sz, margin = 20, 3
    290         local colorY = 30
    291         for i, color in ipairs(colors) do
    292             local cx = margin + (i-1)*(sz+margin)
    293             if isInside(mx, my, cx, colorY, sz, sz) then
    294                 selectedColor = i
    295                 brushColor = color
    296                 window:markDirty()
    297                 return
    298             end
    299         end
    300 
    301         -- Clear button
    302         local clearX = margin + #colors*(sz+margin) + 10
    303         if isInside(mx, my, clearX, colorY, 40, sz) then
    304             if canvasBuffer then
    305                 canvasBuffer:fill(0xFFFFFF)
    306             end
    307             window:markDirty()
    308             return
    309         end
    310     else
    311         -- Start drawing on canvas
    312         ensureCanvasBuffer()
    313         isDrawing = true
    314         local canvasY = my - toolbarHeight
    315         lastX, lastY = mx, canvasY
    316         -- Add initial point directly to buffer
    317         drawCircleToBuffer(mx, canvasY, brushRadius, brushColor)
    318         window:markDirty()
    319     end
    320 end
    321 
    322 -- Mouse move - continue drawing line (no throttling needed with ImageBuffer)
    323 window.onMouseMove = function(mx, my)
    324     if isDrawing and my >= toolbarHeight then
    325         local canvasY = my - toolbarHeight
    326         if lastX and lastY then
    327             addLine(lastX, lastY, mx, canvasY)
    328         end
    329         lastX, lastY = mx, canvasY
    330         window:markDirty()
    331     end
    332 end
    333 
    334 -- Mouse up - stop drawing
    335 window.onMouseUp = function(mx, my)
    336     isDrawing = false
    337     lastX, lastY = nil, nil
    338 end
    339 
    340 -- Keep onClick for toolbar compatibility
    341 window.onClick = function(mx, my)
    342     -- onClick is now handled by onMouseDown
    343 end
    344 
    345 window.onDraw = function(gfx)
    346     -- Get current window dimensions
    347     local winW = window.width
    348     local winH = window.height
    349     local canvasW = winW
    350     local canvasH = winH - toolbarHeight
    351 
    352     -- Toolbar background
    353     gfx:fillRect(0, 0, winW, toolbarHeight, 0x404040)
    354 
    355     -- Row 1: File buttons
    356     for _, btn in ipairs(buttons) do
    357         gfx:fillRect(btn.x, btn.y, btn.w, btn.h, 0x666666)
    358         gfx:drawRect(btn.x, btn.y, btn.w, btn.h, 0xAAAAAA)
    359         gfx:drawText(btn.x + 4, btn.y + 4, btn.label, 0xFFFFFF)
    360     end
    361 
    362     -- Row 2: Color palette
    363     local sz, margin = 20, 3
    364     local colorY = 30
    365     for i, color in ipairs(colors) do
    366         local cx = margin + (i-1)*(sz+margin)
    367         gfx:fillRect(cx, colorY, sz, sz, color)
    368         gfx:drawRect(cx, colorY, sz, sz, i == selectedColor and 0xFFFFFF or 0x808080)
    369     end
    370 
    371     -- Clear button
    372     local clearX = margin + #colors*(sz+margin) + 10
    373     gfx:fillRect(clearX, colorY, 40, sz, 0x666666)
    374     gfx:drawRect(clearX, colorY, 40, sz, 0xAAAAAA)
    375     gfx:drawText(clearX + 4, colorY + 4, "Clr", 0xFFFFFF)
    376 
    377     -- Draw canvas buffer (contains both background and strokes)
    378     ensureCanvasBuffer()
    379     if canvasBuffer then
    380         -- Draw the entire canvas buffer in one operation
    381         gfx:drawBuffer(canvasBuffer, 0, toolbarHeight)
    382     else
    383         gfx:fillRect(0, toolbarHeight, canvasW, canvasH, 0xFFFFFF)
    384     end
    385 
    386     gfx:drawRect(0, toolbarHeight, canvasW, canvasH, 0x000000)
    387 end
    388 
    389 print("Paint loaded")