Round 3: frame offsets, compact layout, auto-reopen preview

- Remove preview selection/offset buttons (reverted jump feature)
- Add per-frame pixel offsets (Up/Down/Left/Right/Reset) in Animations
  tab for visual jump effects without creating extra tiles
- Frame offsets applied in all previews (main + separate window)
- Remove separator between Source and Frames canvases for compact layout
- Move "Frames" label below frames canvas, remove action separator
- Shared zoom between Source and Frames canvases (wheel affects both)
- Separate preview window: content-based sizing instead of copying main
- Remember and auto-reopen separate preview window on plugin launch
- GB: both Occurrences and Optimized canvases same height (150px)
- GB: remove separator between canvases, label below Optimized
- GB: shared zoom between both canvases
- Config: remove view heights options (didn't work)
This commit is contained in:
Cidwel Highwind 2026-04-03 16:29:39 +02:00
parent 6449f20a35
commit 99ca77ef3b
1 changed files with 229 additions and 311 deletions

View File

@ -82,13 +82,8 @@ local S = {
gbAlwaysOverwrite = false, -- overwrite layer checkbox gbAlwaysOverwrite = false, -- overwrite layer checkbox
gbTileW = DEFAULT_GB_TILE_W, -- configurable tile width for GB analysis gbTileW = DEFAULT_GB_TILE_W, -- configurable tile width for GB analysis
gbTileH = DEFAULT_GB_TILE_H, -- configurable tile height for GB analysis gbTileH = DEFAULT_GB_TILE_H, -- configurable tile height for GB analysis
-- Configurable view heights -- Remember if separate preview window was open
sourceViewH = 400, -- configurable source canvas height previewWindowOpen = false,
gbSrcViewH = 150, -- configurable occurrences canvas height
gbOptViewH = 120, -- configurable optimized canvas height
-- Preview offsets
previewOffsets = {}, -- name -> {x=0, y=0} for preview visual offsets
previewSelected = nil, -- name of selected animation in Preview tab
-- Click vs drag for GB canvases -- Click vs drag for GB canvases
gbOptClickX = 0, gbOptClickX = 0,
gbOptClickY = 0, gbOptClickY = 0,
@ -96,9 +91,9 @@ local S = {
gbSrcClickX = 0, gbSrcClickX = 0,
gbSrcClickY = 0, gbSrcClickY = 0,
gbSrcIsDrag = false, gbSrcIsDrag = false,
-- Dynamic animations: each frame is {x, y, flipped, flippedV} -- Dynamic animations: each frame is {x, y, flipped, flippedV, offX, offY}
animNames = {}, -- ordered list of animation names (sorted alphabetically) animNames = {}, -- ordered list of animation names (sorted alphabetically)
anims = {}, -- name -> list of {x, y, flipped, flippedV} anims = {}, -- name -> list of {x, y, flipped, flippedV, offX, offY}
-- Frame selection & drag -- Frame selection & drag
selectedFrame = 0, selectedFrame = 0,
frameDragging = false, frameDragging = false,
@ -117,14 +112,14 @@ local S = {
local SETUP_IDS = { local SETUP_IDS = {
"sepAnims", "cmbAnimList", "btnNewAnim", "btnDelAnim", "btnRenameAnim", "btnCloneAnim", "sepAnims", "cmbAnimList", "btnNewAnim", "btnDelAnim", "btnRenameAnim", "btnCloneAnim",
"sepSource", "canvasSource", "sepSource", "canvasSource",
"sepFrames", "canvasStrips", "canvasStrips",
"sepActions", "btnFlipX", "btnFlipY", "btnMoveLeft", "btnMoveRight", "btnRemoveFrame", "lblFrames", "btnFlipX", "btnFlipY", "btnMoveLeft", "btnMoveRight", "btnRemoveFrame",
"btnFrameUp", "btnFrameDown", "btnFrameLeft", "btnFrameRight", "btnFrameReset",
} }
-- Render tab elements -- Render tab elements
local RENDER_IDS = { local RENDER_IDS = {
"sepRender", "btnSeparateWindow", "canvasRender", "sepRender", "btnSeparateWindow", "canvasRender",
"pvBtnUp", "pvBtnDown", "pvBtnLeft", "pvBtnRight", "pvBtnReset",
} }
-- GB tab elements (reordered: Flip, Offset, Silhouette, Compress) -- GB tab elements (reordered: Flip, Offset, Silhouette, Compress)
@ -133,7 +128,7 @@ local GB_IDS = {
"gbAnalyzeMode", "gbAnalyzeMode",
"gbSimilarThreshold", "btnFindSimilar", "gbSimilarThreshold", "btnFindSimilar",
"sepGbSource", "canvasGbSource", "sepGbSource", "canvasGbSource",
"sepGbOpt", "canvasGbOpt", "canvasGbOpt", "lblGbOpt",
"btnGbSaveLayer", "btnGbCopyClipboard", "btnGbSave", "btnGbSaveLayer", "btnGbCopyClipboard", "btnGbSave",
} }
@ -142,11 +137,11 @@ local GB_IDS = {
---------------------------------------------------------------------- ----------------------------------------------------------------------
local PREFS_FILE = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_prefs.ini") local PREFS_FILE = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_prefs.ini")
-- Serialize frames list: "x1,y1,f;x2,y2,f;..." (f=0 normal, f=1 flipped) -- Serialize frames list: "x1,y1,f,fv,ox,oy;x2,y2,f,fv,ox,oy;..."
local function serializeFrames(frames) local function serializeFrames(frames)
local parts = {} local parts = {}
for _, f in ipairs(frames) do for _, f in ipairs(frames) do
table.insert(parts, f.x .. "," .. f.y .. "," .. (f.flipped and "1" or "0") .. "," .. (f.flippedV and "1" or "0")) table.insert(parts, f.x .. "," .. f.y .. "," .. (f.flipped and "1" or "0") .. "," .. (f.flippedV and "1" or "0") .. "," .. (f.offX or 0) .. "," .. (f.offY or 0))
end end
return table.concat(parts, ";") return table.concat(parts, ";")
end end
@ -156,12 +151,14 @@ local function deserializeFrames(str)
local frames = {} local frames = {}
if not str or str == "" then return frames end if not str or str == "" then return frames end
for part in string.gmatch(str, "([^;]+)") do for part in string.gmatch(str, "([^;]+)") do
local x, y, fh, fv = string.match(part, "([%d-]+),([%d-]+),?(%d?),?(%d?)") local x, y, fh, fv, ox, oy = string.match(part, "([%d-]+),([%d-]+),?(%d?),?(%d?),?([%d-]*),?([%d-]*)")
if x and y then if x and y then
table.insert(frames, { table.insert(frames, {
x = tonumber(x), y = tonumber(y), x = tonumber(x), y = tonumber(y),
flipped = (fh == "1"), flipped = (fh == "1"),
flippedV = (fv == "1") flippedV = (fv == "1"),
offX = tonumber(ox) or 0,
offY = tonumber(oy) or 0
}) })
end end
end end
@ -222,20 +219,8 @@ local function loadPrefs()
S.gbTileW = ts S.gbTileW = ts
S.gbTileH = ts S.gbTileH = ts
end end
-- Load configurable view heights -- Load previewWindowOpen
if k == "sourceViewH" then S.sourceViewH = tonumber(v) or 400 end if k == "previewWindowOpen" then S.previewWindowOpen = (v == "true") end
if k == "gbSrcViewH" then S.gbSrcViewH = tonumber(v) or 150 end
if k == "gbOptViewH" then S.gbOptViewH = tonumber(v) or 120 end
-- Load preview offsets: pvOff_<name>=x,y
if k then
local pvName = string.match(k, "^pvOff_(.+)$")
if pvName then
local ox, oy = string.match(v, "([%d-]+),([%d-]+)")
if ox and oy then
S.previewOffsets[pvName] = { x = tonumber(ox) or 0, y = tonumber(oy) or 0 }
end
end
end
-- Dynamic anim frames: anim_0, anim_1, ... -- Dynamic anim frames: anim_0, anim_1, ...
local animIdx = k and string.match(k, "^anim_(%d+)$") local animIdx = k and string.match(k, "^anim_(%d+)$")
if animIdx then if animIdx then
@ -301,14 +286,8 @@ local function savePrefs()
f:write("gbAlwaysOverwrite=" .. tostring(S.gbAlwaysOverwrite) .. "\n") f:write("gbAlwaysOverwrite=" .. tostring(S.gbAlwaysOverwrite) .. "\n")
f:write("gbTileW=" .. S.gbTileW .. "\n") f:write("gbTileW=" .. S.gbTileW .. "\n")
f:write("gbTileH=" .. S.gbTileH .. "\n") f:write("gbTileH=" .. S.gbTileH .. "\n")
-- Save configurable view heights -- Save previewWindowOpen
f:write("sourceViewH=" .. S.sourceViewH .. "\n") f:write("previewWindowOpen=" .. tostring(S.previewWindowOpen) .. "\n")
f:write("gbSrcViewH=" .. S.gbSrcViewH .. "\n")
f:write("gbOptViewH=" .. S.gbOptViewH .. "\n")
-- Save preview offsets
for name, off in pairs(S.previewOffsets) do
f:write("pvOff_" .. name .. "=" .. off.x .. "," .. off.y .. "\n")
end
for i, name in ipairs(S.animNames) do for i, name in ipairs(S.animNames) do
f:write("anim_" .. (i - 1) .. "=" .. serializeFrames(S.anims[name] or {}) .. "\n") f:write("anim_" .. (i - 1) .. "=" .. serializeFrames(S.anims[name] or {}) .. "\n")
end end
@ -382,7 +361,7 @@ end
local function clampScroll() local function clampScroll()
local contentW, contentH = getSourceContentSize() local contentW, contentH = getSourceContentSize()
local maxScrollX = math.max(0, contentW - SOURCE_VIEWPORT_W) local maxScrollX = math.max(0, contentW - SOURCE_VIEWPORT_W)
local maxScrollY = math.max(0, contentH - S.sourceViewH) local maxScrollY = math.max(0, contentH - SOURCE_VIEWPORT_H)
S.scrollX = clamp(S.scrollX, 0, maxScrollX) S.scrollX = clamp(S.scrollX, 0, maxScrollX)
S.scrollY = clamp(S.scrollY, 0, maxScrollY) S.scrollY = clamp(S.scrollY, 0, maxScrollY)
end end
@ -930,7 +909,7 @@ local function run()
local contentW, contentH = getSourceContentSize() local contentW, contentH = getSourceContentSize()
local viewW = math.min(contentW, SOURCE_VIEWPORT_W) local viewW = math.min(contentW, SOURCE_VIEWPORT_W)
local viewH = math.min(contentH, S.sourceViewH) local viewH = math.min(contentH, SOURCE_VIEWPORT_H)
local dlg = Dialog{ local dlg = Dialog{
title = "AniPhallow - Animation Builder", title = "AniPhallow - Animation Builder",
@ -985,7 +964,7 @@ local function run()
if gridX < 0 or gridX + S.tileW > S.sourceImage.width then return end if gridX < 0 or gridX + S.tileW > S.sourceImage.width then return end
if gridY < 0 or gridY + S.tileH > S.sourceImage.height then return end if gridY < 0 or gridY + S.tileH > S.sourceImage.height then return end
if not S.anims[name] then S.anims[name] = {} end if not S.anims[name] then S.anims[name] = {} end
table.insert(S.anims[name], { x = gridX, y = gridY, flipped = flipped }) table.insert(S.anims[name], { x = gridX, y = gridY, flipped = flipped, offX = 0, offY = 0 })
dlg:repaint() dlg:repaint()
end end
@ -1009,26 +988,12 @@ local function run()
local label = (t == tab) and ("[" .. t .. "]") or (" " .. t .. " ") local label = (t == tab) and ("[" .. t .. "]") or (" " .. t .. " ")
dlg:modify{ id = "tab" .. t, text = label } dlg:modify{ id = "tab" .. t, text = label }
end end
-- If not Preview tab, deselect preview and hide buttons
if tab ~= "Preview" then
S.previewSelected = nil
pcall(function() dlg:modify{ id = "pvBtnUp", visible = false } end)
pcall(function() dlg:modify{ id = "pvBtnDown", visible = false } end)
pcall(function() dlg:modify{ id = "pvBtnLeft", visible = false } end)
pcall(function() dlg:modify{ id = "pvBtnRight", visible = false } end)
pcall(function() dlg:modify{ id = "pvBtnReset", visible = false } end)
else
-- On Preview tab, show buttons only if something is selected
local pvVis = (S.previewSelected ~= nil)
pcall(function() dlg:modify{ id = "pvBtnUp", visible = pvVis } end)
pcall(function() dlg:modify{ id = "pvBtnDown", visible = pvVis } end)
pcall(function() dlg:modify{ id = "pvBtnLeft", visible = pvVis } end)
pcall(function() dlg:modify{ id = "pvBtnRight", visible = pvVis } end)
pcall(function() dlg:modify{ id = "pvBtnReset", visible = pvVis } end)
end
dlg:repaint() dlg:repaint()
end end
-- Forward-declare openPreviewWindow so it can be used from button and auto-open
local openPreviewWindow
---------------------------------------------------------------- ----------------------------------------------------------------
-- TAB BUTTONS -- TAB BUTTONS
---------------------------------------------------------------- ----------------------------------------------------------------
@ -1070,10 +1035,6 @@ local function run()
d:color{ id = "silhouetteColor", label = "Silhouette:", color = silColor } d:color{ id = "silhouetteColor", label = "Silhouette:", color = silColor }
d:entry{ id = "layerName", label = "Layer name:", text = S.gbLayerName } d:entry{ id = "layerName", label = "Layer name:", text = S.gbLayerName }
d:check{ id = "alwaysOverwrite", text = "Always overwrite layer", selected = S.gbAlwaysOverwrite } d:check{ id = "alwaysOverwrite", text = "Always overwrite layer", selected = S.gbAlwaysOverwrite }
d:separator{ text = "View Heights" }
d:number{ id = "sourceViewH", label = "Source View H:", text = tostring(S.sourceViewH), decimals = 0 }
d:number{ id = "gbSrcViewH", label = "Occurrences View H:", text = tostring(S.gbSrcViewH), decimals = 0 }
d:number{ id = "gbOptViewH", label = "Optimized View H:", text = tostring(S.gbOptViewH), decimals = 0 }
d:button{ id = "ok", text = "OK" } d:button{ id = "ok", text = "OK" }
d:button{ text = "Cancel" } d:button{ text = "Cancel" }
d:show() d:show()
@ -1098,26 +1059,6 @@ local function run()
S.gbSilhouetteColorSet = true S.gbSilhouetteColorSet = true
S.gbLayerName = d.data.layerName S.gbLayerName = d.data.layerName
S.gbAlwaysOverwrite = d.data.alwaysOverwrite S.gbAlwaysOverwrite = d.data.alwaysOverwrite
-- View heights
local oldSrcH = S.sourceViewH
local oldGbSrcH = S.gbSrcViewH
local oldGbOptH = S.gbOptViewH
local svh = d.data.sourceViewH
if svh < 50 then svh = 50 elseif svh > 600 then svh = 600 end
S.sourceViewH = svh
local gsvh = d.data.gbSrcViewH
if gsvh < 50 then gsvh = 50 elseif gsvh > 600 then gsvh = 600 end
S.gbSrcViewH = gsvh
local govh = d.data.gbOptViewH
if govh < 50 then govh = 50 elseif govh > 600 then govh = 600 end
S.gbOptViewH = govh
-- If view heights changed, rebuild dialog
if svh ~= oldSrcH or gsvh ~= oldGbSrcH or govh ~= oldGbOptH then
savePrefs()
dlg:close()
run()
return
end
dlg:repaint() dlg:repaint()
end end
end end
@ -1212,11 +1153,6 @@ local function run()
S.anims[newName] = S.anims[name] S.anims[newName] = S.anims[name]
S.anims[name] = nil S.anims[name] = nil
S.animNames[S.currentAnim] = newName S.animNames[S.currentAnim] = newName
-- Transfer preview offset if exists
if S.previewOffsets[name] then
S.previewOffsets[newName] = S.previewOffsets[name]
S.previewOffsets[name] = nil
end
-- Re-sort alphabetically -- Re-sort alphabetically
table.sort(S.animNames, function(a, b) return a:lower() < b:lower() end) table.sort(S.animNames, function(a, b) return a:lower() < b:lower() end)
for i, n in ipairs(S.animNames) do for i, n in ipairs(S.animNames) do
@ -1248,7 +1184,7 @@ local function run()
local srcFrames = S.anims[name] or {} local srcFrames = S.anims[name] or {}
local newFrames = {} local newFrames = {}
for _, fr in ipairs(srcFrames) do for _, fr in ipairs(srcFrames) do
local nf = { x = fr.x, y = fr.y, flipped = fr.flipped, flippedV = fr.flippedV } local nf = { x = fr.x, y = fr.y, flipped = fr.flipped, flippedV = fr.flippedV, offX = fr.offX or 0, offY = fr.offY or 0 }
if d.data.flipX then if d.data.flipX then
nf.flipped = not nf.flipped nf.flipped = not nf.flipped
end end
@ -1283,7 +1219,7 @@ local function run()
local gc = ev.context local gc = ev.context
local cW, cH = getSourceContentSize() local cW, cH = getSourceContentSize()
local vw = math.min(cW, SOURCE_VIEWPORT_W) local vw = math.min(cW, SOURCE_VIEWPORT_W)
local vh = math.min(cH, S.sourceViewH) local vh = math.min(cH, SOURCE_VIEWPORT_H)
drawCheckerboard(gc, vw, vh, S.sourceZoom) drawCheckerboard(gc, vw, vh, S.sourceZoom)
@ -1386,8 +1322,6 @@ local function run()
---------------------------------------------------------------- ----------------------------------------------------------------
-- Frame strip for current animation -- Frame strip for current animation
---------------------------------------------------------------- ----------------------------------------------------------------
dlg:separator{ id = "sepFrames", text = "Frames (L-click=select, R-click=remove, drag=reorder)" }
local STRIP_TOTAL_W = SOURCE_VIEWPORT_W local STRIP_TOTAL_W = SOURCE_VIEWPORT_W
local STRIP_CELL = THUMB_SIZE + 1 local STRIP_CELL = THUMB_SIZE + 1
local STRIP_H = STRIP_CELL * 2 + 4 local STRIP_H = STRIP_CELL * 2 + 4
@ -1537,13 +1471,19 @@ local function run()
S.frameDragging = false S.frameDragging = false
dlg:repaint() dlg:repaint()
end end
end,
onwheel = function(ev)
local dz = ev.deltaY < 0 and 1 or -1
S.sourceZoom = math.max(1, math.min(10, S.sourceZoom + dz))
clampScroll()
dlg:repaint()
end end
} }
---------------------------------------------------------------- ----------------------------------------------------------------
-- Frame action buttons -- Frame action buttons
---------------------------------------------------------------- ----------------------------------------------------------------
dlg:separator{ id = "sepActions" } dlg:label{ id = "lblFrames", text = "Frames: L-click=select, R-click=remove, drag=reorder" }
dlg:button{ dlg:button{
id = "btnFlipX", id = "btnFlipX",
@ -1643,16 +1583,79 @@ local function run()
end end
} }
----------------------------------------------------------------
-- Frame offset buttons
----------------------------------------------------------------
dlg:newrow()
dlg:button{
id = "btnFrameUp",
text = "Up",
onclick = function()
local name = currentAnimName()
if not name or S.selectedFrame < 1 then return end
local frames = S.anims[name]
if not frames or S.selectedFrame > #frames then return end
frames[S.selectedFrame].offY = (frames[S.selectedFrame].offY or 0) - 1
dlg:repaint()
end
}
dlg:button{
id = "btnFrameDown",
text = "Down",
onclick = function()
local name = currentAnimName()
if not name or S.selectedFrame < 1 then return end
local frames = S.anims[name]
if not frames or S.selectedFrame > #frames then return end
frames[S.selectedFrame].offY = (frames[S.selectedFrame].offY or 0) + 1
dlg:repaint()
end
}
dlg:button{
id = "btnFrameLeft",
text = "Left",
onclick = function()
local name = currentAnimName()
if not name or S.selectedFrame < 1 then return end
local frames = S.anims[name]
if not frames or S.selectedFrame > #frames then return end
frames[S.selectedFrame].offX = (frames[S.selectedFrame].offX or 0) - 1
dlg:repaint()
end
}
dlg:button{
id = "btnFrameRight",
text = "Right",
onclick = function()
local name = currentAnimName()
if not name or S.selectedFrame < 1 then return end
local frames = S.anims[name]
if not frames or S.selectedFrame > #frames then return end
frames[S.selectedFrame].offX = (frames[S.selectedFrame].offX or 0) + 1
dlg:repaint()
end
}
dlg:button{
id = "btnFrameReset",
text = "Reset",
onclick = function()
local name = currentAnimName()
if not name or S.selectedFrame < 1 then return end
local frames = S.anims[name]
if not frames or S.selectedFrame > #frames then return end
frames[S.selectedFrame].offX = 0
frames[S.selectedFrame].offY = 0
dlg:repaint()
end
}
---------------------------------------------------------------- ----------------------------------------------------------------
-- RENDER TAB (Preview - all animations) -- RENDER TAB (Preview - all animations)
---------------------------------------------------------------- ----------------------------------------------------------------
dlg:separator{ id = "sepRender", text = "All Animations Preview", visible = false } dlg:separator{ id = "sepRender", text = "All Animations Preview", visible = false }
dlg:button{ -- Define openPreviewWindow function
id = "btnSeparateWindow", openPreviewWindow = function()
text = "Separate Window",
visible = false,
onclick = function()
-- Close existing preview window if open -- Close existing preview window if open
if previewDlg then if previewDlg then
pcall(function() previewDlg:close() end) pcall(function() previewDlg:close() end)
@ -1662,7 +1665,18 @@ local function run()
end end
local pvAnimFrame = { value = 0 } local pvAnimFrame = { value = 0 }
local RENDER_MARGIN = 2 local PV_RENDER_MARGIN = 2
-- Calculate content-based size
local numAnims = #S.animNames
local ptw = S.tileW * S.previewZoom
local pth = S.tileH * S.previewZoom
local cellW = ptw + PV_RENDER_MARGIN * 2
local cellH = pth + PV_RENDER_MARGIN * 2
local pvCols = 2
local pvRows = math.max(1, math.ceil(numAnims / pvCols))
local pvWidth = pvCols * cellW
local pvHeight = pvRows * cellH
previewDlg = Dialog{ previewDlg = Dialog{
title = "AniPhallow Preview", title = "AniPhallow Preview",
@ -1672,43 +1686,40 @@ local function run()
end end
previewTimer = nil previewTimer = nil
previewDlg = nil previewDlg = nil
S.previewWindowOpen = false
savePrefs()
end end
} }
previewDlg:canvas{ previewDlg:canvas{
id = "pvCanvas", id = "pvCanvas",
width = SOURCE_VIEWPORT_W, width = pvWidth,
height = S.sourceViewH, height = pvHeight,
autoscaling = false, autoscaling = false,
onpaint = function(ev) onpaint = function(ev)
local gc = ev.context local gc = ev.context
local numAnims = #S.animNames local na = #S.animNames
if numAnims == 0 then return end if na == 0 then return end
local ptw = S.tileW * S.previewZoom local pw = S.tileW * S.previewZoom
local pth = S.tileH * S.previewZoom local ph = S.tileH * S.previewZoom
local cellW = ptw + RENDER_MARGIN * 2 local cW = pw + PV_RENDER_MARGIN * 2
local cellH = pth + RENDER_MARGIN * 2 local cH = ph + PV_RENDER_MARGIN * 2
local cols = 2 local cols = 2
local af = pvAnimFrame.value local af = pvAnimFrame.value
for i, name in ipairs(S.animNames) do for i, name in ipairs(S.animNames) do
local col = (i - 1) % cols local col = (i - 1) % cols
local row = math.floor((i - 1) / cols) local row = math.floor((i - 1) / cols)
local ox = col * cellW local ox = col * cW
local oy = row * cellH local oy = row * cH
-- Apply preview offsets
local pvOff = S.previewOffsets[name]
local offX = pvOff and (pvOff.x * S.previewZoom) or 0
local offY = pvOff and (pvOff.y * S.previewZoom) or 0
if S.useBgColor then if S.useBgColor then
gc.color = S.bgColor gc.color = S.bgColor
gc:fillRect(Rectangle(ox + RENDER_MARGIN, oy + RENDER_MARGIN, ptw, pth)) gc:fillRect(Rectangle(ox + PV_RENDER_MARGIN, oy + PV_RENDER_MARGIN, pw, ph))
else else
drawCheckerboard(gc, ptw, pth, S.previewZoom, drawCheckerboard(gc, pw, ph, S.previewZoom,
ox + RENDER_MARGIN, oy + RENDER_MARGIN) ox + PV_RENDER_MARGIN, oy + PV_RENDER_MARGIN)
end end
local frames = S.anims[name] or {} local frames = S.anims[name] or {}
@ -1717,10 +1728,13 @@ local function run()
local idx = (af % totalFrames) + 1 local idx = (af % totalFrames) + 1
local fimg = getFrameImage(name, idx) local fimg = getFrameImage(name, idx)
if fimg then if fimg then
local f = frames[idx]
local fOffX = (f.offX or 0) * S.previewZoom
local fOffY = (f.offY or 0) * S.previewZoom
gc:drawImage( gc:drawImage(
fimg, fimg,
Rectangle(0, 0, S.tileW, S.tileH), Rectangle(0, 0, S.tileW, S.tileH),
Rectangle(ox + RENDER_MARGIN + offX, oy + RENDER_MARGIN + offY, ptw, pth) Rectangle(ox + PV_RENDER_MARGIN + fOffX, oy + PV_RENDER_MARGIN + fOffY, pw, ph)
) )
end end
end end
@ -1741,8 +1755,20 @@ local function run()
end end
} }
previewTimer:start() previewTimer:start()
S.previewWindowOpen = true
savePrefs()
previewDlg:show{ wait = false } previewDlg:show{ wait = false }
end end
dlg:button{
id = "btnSeparateWindow",
text = "Separate Window",
visible = false,
onclick = function()
openPreviewWindow()
end
} }
local RENDER_MARGIN = 2 local RENDER_MARGIN = 2
@ -1750,7 +1776,7 @@ local function run()
dlg:canvas{ dlg:canvas{
id = "canvasRender", id = "canvasRender",
width = SOURCE_VIEWPORT_W, width = SOURCE_VIEWPORT_W,
height = S.sourceViewH, height = SOURCE_VIEWPORT_H,
autoscaling = false, autoscaling = false,
visible = false, visible = false,
onpaint = function(ev) onpaint = function(ev)
@ -1770,11 +1796,6 @@ local function run()
local ox = col * cellW local ox = col * cellW
local oy = row * cellH local oy = row * cellH
-- Apply preview offsets
local pvOff = S.previewOffsets[name]
local offX = pvOff and (pvOff.x * S.previewZoom) or 0
local offY = pvOff and (pvOff.y * S.previewZoom) or 0
if S.useBgColor then if S.useBgColor then
gc.color = S.bgColor gc.color = S.bgColor
gc:fillRect(Rectangle(ox + RENDER_MARGIN, oy + RENDER_MARGIN, ptw, pth)) gc:fillRect(Rectangle(ox + RENDER_MARGIN, oy + RENDER_MARGIN, ptw, pth))
@ -1789,59 +1810,16 @@ local function run()
local idx = (S.animFrame % totalFrames) + 1 local idx = (S.animFrame % totalFrames) + 1
local fimg = getFrameImage(name, idx) local fimg = getFrameImage(name, idx)
if fimg then if fimg then
local f = frames[idx]
local fOffX = (f.offX or 0) * S.previewZoom
local fOffY = (f.offY or 0) * S.previewZoom
gc:drawImage( gc:drawImage(
fimg, fimg,
Rectangle(0, 0, S.tileW, S.tileH), Rectangle(0, 0, S.tileW, S.tileH),
Rectangle(ox + RENDER_MARGIN + offX, oy + RENDER_MARGIN + offY, ptw, pth) Rectangle(ox + RENDER_MARGIN + fOffX, oy + RENDER_MARGIN + fOffY, ptw, pth)
) )
end end
end end
-- Draw border around selected animation
if S.previewSelected == name then
gc.color = Color(255, 255, 0, 220)
gc:fillRect(Rectangle(ox, oy, cellW, 2))
gc:fillRect(Rectangle(ox, oy + cellH - 2, cellW, 2))
gc:fillRect(Rectangle(ox, oy, 2, cellH))
gc:fillRect(Rectangle(ox + cellW - 2, oy, 2, cellH))
end
end
end,
onmousedown = function(ev)
if ev.button == MouseButton.LEFT then
local numAnims = #S.animNames
if numAnims == 0 then return end
local ptw = S.tileW * S.previewZoom
local pth = S.tileH * S.previewZoom
local cellW = ptw + RENDER_MARGIN * 2
local cellH = pth + RENDER_MARGIN * 2
local cols = 2
for i, name in ipairs(S.animNames) do
local col = (i - 1) % cols
local row = math.floor((i - 1) / cols)
local ox = col * cellW
local oy = row * cellH
if ev.x >= ox and ev.x < ox + cellW and ev.y >= oy and ev.y < oy + cellH then
-- Toggle selection
if S.previewSelected == name then
S.previewSelected = nil
else
S.previewSelected = name
end
-- Show/hide preview offset buttons
local pvVis = (S.previewSelected ~= nil)
pcall(function() dlg:modify{ id = "pvBtnUp", visible = pvVis } end)
pcall(function() dlg:modify{ id = "pvBtnDown", visible = pvVis } end)
pcall(function() dlg:modify{ id = "pvBtnLeft", visible = pvVis } end)
pcall(function() dlg:modify{ id = "pvBtnRight", visible = pvVis } end)
pcall(function() dlg:modify{ id = "pvBtnReset", visible = pvVis } end)
dlg:repaint()
return
end
end
end end
end, end,
onwheel = function(ev) onwheel = function(ev)
@ -1851,75 +1829,6 @@ local function run()
end end
} }
-- Preview offset buttons (hidden by default)
dlg:button{
id = "pvBtnUp",
text = "Up",
visible = false,
onclick = function()
if not S.previewSelected then return end
if not S.previewOffsets[S.previewSelected] then
S.previewOffsets[S.previewSelected] = { x = 0, y = 0 }
end
S.previewOffsets[S.previewSelected].y = S.previewOffsets[S.previewSelected].y - 1
savePrefs()
dlg:repaint()
end
}
dlg:button{
id = "pvBtnDown",
text = "Down",
visible = false,
onclick = function()
if not S.previewSelected then return end
if not S.previewOffsets[S.previewSelected] then
S.previewOffsets[S.previewSelected] = { x = 0, y = 0 }
end
S.previewOffsets[S.previewSelected].y = S.previewOffsets[S.previewSelected].y + 1
savePrefs()
dlg:repaint()
end
}
dlg:button{
id = "pvBtnLeft",
text = "Left",
visible = false,
onclick = function()
if not S.previewSelected then return end
if not S.previewOffsets[S.previewSelected] then
S.previewOffsets[S.previewSelected] = { x = 0, y = 0 }
end
S.previewOffsets[S.previewSelected].x = S.previewOffsets[S.previewSelected].x - 1
savePrefs()
dlg:repaint()
end
}
dlg:button{
id = "pvBtnRight",
text = "Right",
visible = false,
onclick = function()
if not S.previewSelected then return end
if not S.previewOffsets[S.previewSelected] then
S.previewOffsets[S.previewSelected] = { x = 0, y = 0 }
end
S.previewOffsets[S.previewSelected].x = S.previewOffsets[S.previewSelected].x + 1
savePrefs()
dlg:repaint()
end
}
dlg:button{
id = "pvBtnReset",
text = "Reset",
visible = false,
onclick = function()
if not S.previewSelected then return end
S.previewOffsets[S.previewSelected] = { x = 0, y = 0 }
savePrefs()
dlg:repaint()
end
}
---------------------------------------------------------------- ----------------------------------------------------------------
-- GB TAB -- GB TAB
---------------------------------------------------------------- ----------------------------------------------------------------
@ -1994,7 +1903,7 @@ local function run()
local z = S.gbZoomOpt local z = S.gbZoomOpt
dlg:modify{ id = "canvasGbOpt", dlg:modify{ id = "canvasGbOpt",
width = math.min(S.gbOptImage.width * z, SOURCE_VIEWPORT_W), width = math.min(S.gbOptImage.width * z, SOURCE_VIEWPORT_W),
height = math.min(S.gbOptImage.height * z, S.gbOptViewH) } height = math.min(S.gbOptImage.height * z, GB_OPT_H) }
end end
dlg:repaint() dlg:repaint()
end end
@ -2043,7 +1952,7 @@ local function run()
dlg:separator{ id = "sepGbSource", text = "Occurrences in Source", visible = false } dlg:separator{ id = "sepGbSource", text = "Occurrences in Source", visible = false }
local GB_SRC_W = SOURCE_VIEWPORT_W local GB_SRC_W = SOURCE_VIEWPORT_W
local GB_SRC_H = S.gbSrcViewH local GB_SRC_H = 150
-- Flip colors -- Flip colors
local FLIP_COLORS = { local FLIP_COLORS = {
@ -2198,7 +2107,9 @@ local function run()
end, end,
onwheel = function(ev) onwheel = function(ev)
local dz = ev.deltaY < 0 and 1 or -1 local dz = ev.deltaY < 0 and 1 or -1
S.gbZoomSrc = math.max(1, math.min(10, S.gbZoomSrc + dz)) local newZoom = math.max(1, math.min(10, S.gbZoomSrc + dz))
S.gbZoomSrc = newZoom
S.gbZoomOpt = newZoom
dlg:repaint() dlg:repaint()
end end
} }
@ -2206,10 +2117,8 @@ local function run()
---------------------------------------------------------------- ----------------------------------------------------------------
-- Optimized Spritesheet canvas (L-click=select) -- Optimized Spritesheet canvas (L-click=select)
---------------------------------------------------------------- ----------------------------------------------------------------
dlg:separator{ id = "sepGbOpt", text = "Optimized Spritesheet (L-click=select)", visible = false }
local GB_OPT_W = SOURCE_VIEWPORT_W local GB_OPT_W = SOURCE_VIEWPORT_W
local GB_OPT_H = S.gbOptViewH local GB_OPT_H = 150
dlg:canvas{ dlg:canvas{
id = "canvasGbOpt", id = "canvasGbOpt",
@ -2443,11 +2352,15 @@ local function run()
end, end,
onwheel = function(ev) onwheel = function(ev)
local dz = ev.deltaY < 0 and 1 or -1 local dz = ev.deltaY < 0 and 1 or -1
S.gbZoomOpt = math.max(1, math.min(10, S.gbZoomOpt + dz)) local newZoom = math.max(1, math.min(10, S.gbZoomOpt + dz))
S.gbZoomOpt = newZoom
S.gbZoomSrc = newZoom
dlg:repaint() dlg:repaint()
end end
} }
dlg:label{ id = "lblGbOpt", text = "Optimized Spritesheet (L-click=select)", visible = false }
-- Save as Layer button -- Save as Layer button
dlg:button{ dlg:button{
id = "btnGbSaveLayer", id = "btnGbSaveLayer",
@ -2619,6 +2532,11 @@ local function run()
startAnimTimer() startAnimTimer()
switchTab(S.currentTab) switchTab(S.currentTab)
dlg:show{ wait = false } dlg:show{ wait = false }
-- Auto-reopen separate preview window if it was open
if S.previewWindowOpen then
openPreviewWindow()
end
end end
run() run()