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")