diff --git a/aniphallow.lua b/aniphallow.lua index 134ce58..c817b77 100644 --- a/aniphallow.lua +++ b/aniphallow.lua @@ -23,7 +23,7 @@ local SOURCE_VIEWPORT_W = 300 local SOURCE_VIEWPORT_H = 400 local MAX_ANIMS = 20 -- max number of dynamic animations -local TABS = { "Animations", "GB" } +local TABS = { "Animations", "Optimize" } local DEFAULT_GB_TILE_W = 8 -- Default Game Boy tile width local DEFAULT_GB_TILE_H = 8 -- Default Game Boy tile height local GB_COLS = 16 -- tiles per row in optimized image (128px = GB standard) @@ -119,7 +119,7 @@ local SETUP_IDS = { "sepAnims", "cmbAnimList", "btnNewAnim", "btnDelAnim", "btnRenameAnim", "btnCloneAnim", "sepSource", "canvasSource", "canvasStrips", - "lblFrames", "btnFlipX", "btnFlipY", "btnMoveLeft", "btnMoveRight", "btnRemoveFrame", + "btnFlipX", "btnFlipY", "btnMoveLeft", "btnMoveRight", "btnRemoveFrame", "btnFrameUp", "btnFrameDown", "btnFrameLeft", "btnFrameRight", "btnFrameReset", } @@ -129,13 +129,13 @@ local GB_IDS = { "gbAnalyzeMode", "gbSimilarThreshold", "btnFindSimilar", "sepGbSource", "canvasGbSource", - "canvasGbOpt", "lblGbOpt", + "canvasGbOpt", "btnGbSaveLayer", "btnGbCopyClipboard", "btnGbSave", } -- GB canvas heights (taller to match Animations tab and prevent resize on tab switch) -local GB_SRC_H = 220 -local GB_OPT_H = 220 +local GB_SRC_H = 240 +local GB_OPT_H = 240 ---------------------------------------------------------------------- -- Module-level window/timer variables @@ -209,7 +209,9 @@ local function loadPrefs() -- Migrate old tab names if v == "Setup" then v = "Animations" elseif v == "Render" then v = "Animations" - elseif v == "Preview" then v = "Animations" end + elseif v == "Preview" then v = "Animations" + elseif v == "GB" then v = "Optimize" + end S.currentTab = v end if k == "animNames" and v ~= "" then @@ -967,6 +969,21 @@ local function buildOptimizedImage(img, tiles, compress, silhouette, silhouetteC end end +---------------------------------------------------------------------- +-- Helper: get preview title based on current mode/animation +---------------------------------------------------------------------- +local function getPreviewTitle() + if S.previewMode == "all" then + return "AniPhallow: All" + else + local name = "" + if S.previewSingleIdx >= 1 and S.previewSingleIdx <= #S.animNames then + name = S.animNames[S.previewSingleIdx] + end + return "AniPhallow: " .. name + end +end + ---------------------------------------------------------------------- -- Preview Window (primary window) ---------------------------------------------------------------------- @@ -1017,7 +1034,7 @@ openPreviewWindow = function() pvHeight = math.max(pvHeight, 40) previewDlg = Dialog{ - title = "AniPhallow Preview", + title = getPreviewTitle(), onclose = function() if previewTimer then pcall(function() previewTimer:stop() end) end if pvRefreshTimer then pcall(function() pvRefreshTimer:stop() end) end @@ -1026,12 +1043,10 @@ openPreviewWindow = function() previewDlg = nil S.previewWindowOpen = false savePrefs() - if mainDlg then - pcall(function() mainDlg:modify{ id = "tabPreview", text = " Preview " } end) - end end } + -- All buttons on the same row: Setup, toggle mode, Fit previewDlg:button{ id = "pvSetup", text = "Setup", @@ -1042,7 +1057,7 @@ openPreviewWindow = function() previewDlg:button{ id = "pvToggleMode", - text = S.previewMode == "all" and "Show One" or "Show All", + text = S.previewMode == "all" and "All" or "One", onclick = function() if S.previewMode == "all" then S.previewMode = "single" @@ -1050,52 +1065,22 @@ openPreviewWindow = function() if S.previewSingleIdx < 1 then S.previewSingleIdx = 1 end if S.previewSingleIdx > #S.animNames then S.previewSingleIdx = #S.animNames end end - previewDlg:modify{ id = "pvToggleMode", text = "Show All" } - previewDlg:modify{ id = "pvPrev", visible = true } - previewDlg:modify{ id = "pvNext", visible = true } - previewDlg:modify{ id = "pvAnimName", visible = true } else S.previewMode = "all" - previewDlg:modify{ id = "pvToggleMode", text = "Show One" } - previewDlg:modify{ id = "pvPrev", visible = false } - previewDlg:modify{ id = "pvNext", visible = false } - previewDlg:modify{ id = "pvAnimName", visible = false } end - previewDlg:repaint() + -- Close and reopen to update title + pcall(function() previewDlg:close() end) + openPreviewWindow() end } - -- Navigation buttons on a new row, only in single mode - previewDlg:newrow() previewDlg:button{ - id = "pvPrev", - text = "<-", - visible = (S.previewMode == "single"), + id = "pvFit", + text = "Fit", onclick = function() - if #S.animNames > 0 then - S.previewSingleIdx = S.previewSingleIdx - 1 - if S.previewSingleIdx < 1 then S.previewSingleIdx = #S.animNames end - -- Update name label - pcall(function() - previewDlg:modify{ id = "pvAnimName", text = S.animNames[S.previewSingleIdx] or "" } - end) - previewDlg:repaint() - end - end - } - previewDlg:button{ - id = "pvNext", - text = "->", - visible = (S.previewMode == "single"), - onclick = function() - if #S.animNames > 0 then - S.previewSingleIdx = S.previewSingleIdx + 1 - if S.previewSingleIdx > #S.animNames then S.previewSingleIdx = 1 end - pcall(function() - previewDlg:modify{ id = "pvAnimName", text = S.animNames[S.previewSingleIdx] or "" } - end) - previewDlg:repaint() - end + -- Close and reopen to refit + pcall(function() previewDlg:close() end) + openPreviewWindow() end } @@ -1171,6 +1156,28 @@ openPreviewWindow = function() end end end, + onmousedown = function(ev) + -- In single mode: L-click=prev anim, R-click=next anim + if S.previewMode == "single" then + if ev.button == MouseButton.LEFT then + if #S.animNames > 0 then + S.previewSingleIdx = S.previewSingleIdx - 1 + if S.previewSingleIdx < 1 then S.previewSingleIdx = #S.animNames end + -- Close and reopen to update title + pcall(function() previewDlg:close() end) + openPreviewWindow() + end + elseif ev.button == MouseButton.RIGHT then + if #S.animNames > 0 then + S.previewSingleIdx = S.previewSingleIdx + 1 + if S.previewSingleIdx > #S.animNames then S.previewSingleIdx = 1 end + -- Close and reopen to update title + pcall(function() previewDlg:close() end) + openPreviewWindow() + end + end + end + end, onwheel = function(ev) local dz = ev.deltaY < 0 and 1 or -1 if S.previewMode == "single" then @@ -1182,29 +1189,6 @@ openPreviewWindow = function() end } - -- Animation name label (only in single mode) - local singleName = "" - if S.previewSingleIdx >= 1 and S.previewSingleIdx <= #S.animNames then - singleName = S.animNames[S.previewSingleIdx] - end - previewDlg:label{ - id = "pvAnimName", - text = singleName, - visible = (S.previewMode == "single") - } - - -- Adjust button: re-fits window to content by closing and reopening - previewDlg:newrow() - previewDlg:button{ - id = "pvAdjust", - text = "Adjust", - onclick = function() - -- Close and reopen to refit - pcall(function() previewDlg:close() end) - openPreviewWindow() - end - } - previewTimer = Timer{ interval = S.animSpeed / 1000.0, ontick = function() @@ -1267,18 +1251,18 @@ openMainDialog = function() dlg:repaint() end - -- Switch visible tab (only Animations and GB) + -- Switch visible tab (only Animations and Optimize) local function switchTab(tab) S.currentTab = tab local isSetup = (tab == "Animations") - local isGB = (tab == "GB") + local isOpt = (tab == "Optimize") for _, id in ipairs(SETUP_IDS) do pcall(function() dlg:modify{ id = id, visible = isSetup } end) end for _, id in ipairs(GB_IDS) do - pcall(function() dlg:modify{ id = id, visible = isGB } end) + pcall(function() dlg:modify{ id = id, visible = isOpt } end) end - -- Update tab button labels for Animations and GB only + -- Update tab button labels for _, t in ipairs(TABS) do local label = (t == tab) and ("[" .. t .. "]") or (" " .. t .. " ") pcall(function() dlg:modify{ id = "tab" .. t, text = label } end) @@ -1316,7 +1300,7 @@ openMainDialog = function() mainDlg = dlg ---------------------------------------------------------------- - -- TAB BUTTONS (Animations, GB, Preview toggle) + -- TAB BUTTONS (Animations, Optimize) ---------------------------------------------------------------- for _, tab in ipairs(TABS) do local label = (tab == S.currentTab) and ("[" .. tab .. "]") or (" " .. tab .. " ") @@ -1329,23 +1313,6 @@ openMainDialog = function() } end - -- Preview toggle button - dlg:button{ - id = "tabPreview", - text = S.previewWindowOpen and "[Preview]" or " Preview ", - onclick = function() - if previewDlg then - -- Close it - pcall(function() previewDlg:close() end) - -- previewDlg onclose handler sets S.previewWindowOpen = false - dlg:modify{ id = "tabPreview", text = " Preview " } - else - openPreviewWindow() - dlg:modify{ id = "tabPreview", text = "[Preview]" } - end - end - } - ---------------------------------------------------------------- -- Config button (opens sub-dialog) - visible on ALL tabs ---------------------------------------------------------------- @@ -1359,9 +1326,9 @@ openMainDialog = function() d:slider{ id = "animSpeed", label = "Speed (ms):", min = 50, max = 1000, value = S.animSpeed } d:check{ id = "useBgColor", text = "Solid background", selected = S.useBgColor } d:color{ id = "bgColor", label = "Bg Color:", color = S.bgColor } - d:separator{ text = "GB Options" } - d:number{ id = "gbTileW", label = "GB Tile W:", text = tostring(S.gbTileW), decimals = 0 } - d:number{ id = "gbTileH", label = "GB Tile H:", text = tostring(S.gbTileH), decimals = 0 } + d:separator{ text = "Optimization" } + d:number{ id = "gbTileW", label = "Tile W:", text = tostring(S.gbTileW), decimals = 0 } + d:number{ id = "gbTileH", label = "Tile H:", text = tostring(S.gbTileH), decimals = 0 } local silColor = S.gbSilhouetteColor if not S.gbSilhouetteColorSet then silColor = getDarkestPaletteColor() @@ -1551,7 +1518,7 @@ openMainDialog = function() ---------------------------------------------------------------- -- Source canvas (L-click=add, R-click=add flipped) ---------------------------------------------------------------- - dlg:separator{ id = "sepSource", text = "Source (L-click=add frame, R-click=add flipped)" } + dlg:separator{ id = "sepSource", text = "Source (L-click=add, R-click=flip add) || Frames (L-click=select, R-click=remove, drag=move)" } dlg:canvas{ id = "canvasSource", @@ -1777,6 +1744,12 @@ openMainDialog = function() local idx = row * thumbsPerRow + col + 1 if idx >= 1 and idx <= #frames then if ev.button == MouseButton.LEFT then + -- Click on already-selected frame: deselect + if S.selectedFrame == idx then + S.selectedFrame = 0 + dlg:repaint() + return -- don't start drag + end S.selectedFrame = idx S.frameDragging = true S.frameDragFrom = idx @@ -1830,10 +1803,8 @@ openMainDialog = function() } ---------------------------------------------------------------- - -- Frame action buttons + -- Frame action buttons (no separator - combined into sepSource) ---------------------------------------------------------------- - dlg:separator{ id = "lblFrames", text = "Frames (L-click=select, R-click=remove, drag=reorder)" } - dlg:button{ id = "btnFlipX", text = "FlipX", @@ -1999,9 +1970,9 @@ openMainDialog = function() } ---------------------------------------------------------------- - -- GB TAB + -- OPTIMIZE TAB ---------------------------------------------------------------- - dlg:separator{ id = "sepGb", text = "GB Tile Optimizer", visible = false } + dlg:separator{ id = "sepGb", text = "Tile Optimizer", visible = false } -- Row: Flip + Offset + Silhouette + Compress (reordered) dlg:check{ @@ -2118,7 +2089,7 @@ openMainDialog = function() ---------------------------------------------------------------- -- Occurrences in Source canvas (BEFORE Optimized) ---------------------------------------------------------------- - dlg:separator{ id = "sepGbSource", text = "Occurrences in Source", visible = false } + dlg:separator{ id = "sepGbSource", text = "Source || Optimized (L-click=check occurrences, R-click=deselect)", visible = false } local GB_SRC_W = SOURCE_VIEWPORT_W @@ -2202,6 +2173,12 @@ openMainDialog = function() S.gbSrcDragging = true S.gbSrcDragLastX = ev.x S.gbSrcDragLastY = ev.y + elseif ev.button == MouseButton.RIGHT then + -- R-click: deselect + if S.gbSelectedTile > 0 then + S.gbSelectedTile = 0 + dlg:repaint() + end end end, onmousemove = function(ev) @@ -2283,7 +2260,7 @@ openMainDialog = function() } ---------------------------------------------------------------- - -- Optimized Spritesheet canvas (L-click=select) + -- Optimized Spritesheet canvas (L-click=select, R-click=deselect) ---------------------------------------------------------------- local GB_OPT_W = SOURCE_VIEWPORT_W @@ -2453,6 +2430,12 @@ openMainDialog = function() S.gbDragging = true S.gbDragLastX = ev.x S.gbDragLastY = ev.y + elseif ev.button == MouseButton.RIGHT then + -- R-click: deselect + if S.gbSelectedTile > 0 then + S.gbSelectedTile = 0 + dlg:repaint() + end end end, onmousemove = function(ev) @@ -2474,7 +2457,7 @@ openMainDialog = function() end, onmouseup = function(ev) if S.gbDragging and not S.gbOptIsDrag then - -- Click: select tile + -- Click: select tile using candidate-based logic if not S.gbOptImage then S.gbDragging = false return @@ -2485,33 +2468,86 @@ openMainDialog = function() local pixelX = math.floor((S.gbOptClickX + S.gbScrollX) / z) local pixelY = math.floor((S.gbOptClickY + S.gbScrollY) / z) + -- Find all tiles whose positions cover the clicked pixel + local candidates = {} + if S.gbCompress then + -- In compress mode, tiles are laid out in a grid local col = math.floor(pixelX / tw) local row = math.floor(pixelY / th) local idx = row * S.gbCols + col + 1 if idx >= 1 and idx <= #S.gbTiles then - -- Toggle: deselect if same tile clicked again - if S.gbSelectedTile == idx then - S.gbSelectedTile = 0 - else - S.gbSelectedTile = idx + local isOpaque = false + if pixelX >= 0 and pixelX < S.gbOptImage.width + and pixelY >= 0 and pixelY < S.gbOptImage.height then + isOpaque = pc.rgbaA(S.gbOptImage:getPixel(pixelX, pixelY)) > 0 end + table.insert(candidates, { tileIdx = idx, opaque = isOpaque }) end + + -- Also check silhouette areas in non-compress mode is N/A here, + -- but in compress mode with silhouette there's only one tile per grid cell else - local clickTileX = math.floor(pixelX / tw) * tw - local clickTileY = math.floor(pixelY / th) * th + -- Non-compress mode: candidate-based click like Occurrences canvas for i, tile in ipairs(S.gbTiles) do local pos = tile.positions[1] - if pos.x == clickTileX and pos.y == clickTileY then - -- Toggle: deselect if same tile clicked again - if S.gbSelectedTile == i then - S.gbSelectedTile = 0 - else - S.gbSelectedTile = i + if pixelX >= pos.x and pixelX < pos.x + tw and + pixelY >= pos.y and pixelY < pos.y + th then + local isOpaque = false + if S.gbOptImage and pixelX >= 0 and pixelX < S.gbOptImage.width + and pixelY >= 0 and pixelY < S.gbOptImage.height then + isOpaque = pc.rgbaA(S.gbOptImage:getPixel(pixelX, pixelY)) > 0 end + table.insert(candidates, { tileIdx = i, opaque = isOpaque }) + end + end + + -- Also check silhouette positions (duplicate tile positions rendered as silhouettes) + if S.gbSilhouette then + for i, tile in ipairs(S.gbTiles) do + for pi = 2, #tile.positions do + local pos = tile.positions[pi] + if pixelX >= pos.x and pixelX < pos.x + tw and + pixelY >= pos.y and pixelY < pos.y + th then + local isOpaque = false + if S.gbOptImage and pixelX >= 0 and pixelX < S.gbOptImage.width + and pixelY >= 0 and pixelY < S.gbOptImage.height then + isOpaque = pc.rgbaA(S.gbOptImage:getPixel(pixelX, pixelY)) > 0 + end + -- Check if not already in candidates + local alreadyIn = false + for _, c in ipairs(candidates) do + if c.tileIdx == i then alreadyIn = true; break end + end + if not alreadyIn then + table.insert(candidates, { tileIdx = i, opaque = isOpaque }) + end + end + end + end + end + end + + if #candidates > 0 then + -- Sort: opaque tiles first + table.sort(candidates, function(a, b) + if a.opaque ~= b.opaque then return a.opaque end + return a.tileIdx < b.tileIdx + end) + + -- Cycle through candidates if current selection is among them + local found = false + for ci, cand in ipairs(candidates) do + if cand.tileIdx == S.gbSelectedTile then + local nextIdx = (ci % #candidates) + 1 + S.gbSelectedTile = candidates[nextIdx].tileIdx + found = true break end end + if not found then + S.gbSelectedTile = candidates[1].tileIdx + end end end S.gbDragging = false @@ -2526,8 +2562,6 @@ openMainDialog = function() end } - dlg:separator{ id = "lblGbOpt", text = "Optimized Spritesheet (L-click=select)", visible = false } - -- Save as Layer button dlg:button{ id = "btnGbSaveLayer", @@ -2678,7 +2712,7 @@ openMainDialog = function() interval = 0.5, ontick = function() local isDragging = S.dragging or S.gbDragging or S.gbSrcDragging - if S.currentTab == "GB" and not isDragging then + if S.currentTab == "Optimize" and not isDragging then gbRefreshCounter = gbRefreshCounter + 1 if gbRefreshCounter >= 4 then gbRefreshCounter = 0