Improve animations tab, fix Save as Layer, and enhance GB similarity colors
- Fix Save as Layer: convert image to sprite color mode before creating cel - Remove "Selected ->" label (brackets on buttons suffice) - Add Remove button and right-click to remove frames in strip - Add Move Animation Up/Down buttons for reordering animations - Add configurable source viewport height and auto-resize with zoom - Rename GB buttons: Analyze Duplicates, Analyze Similars, Similarity >=X% - Reorder GB buttons: Analyze -> Similars -> Similarity threshold - Use distinct colors per group for similar tile highlighting (union-find) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
534fc09da3
commit
0c1d5d9c90
198
aniphallow.lua
198
aniphallow.lua
|
|
@ -80,6 +80,8 @@ local S = {
|
|||
gbLayerName = "auto-optimized-tiles", -- default layer name
|
||||
gbAlwaysOverwrite = false, -- overwrite layer checkbox
|
||||
gbTileSize = GB_TILE, -- current tile size used (8 for pixel, from tileset for tile)
|
||||
sourceViewportH = 250, -- configurable source viewport height
|
||||
sourceAutoResize = false, -- auto-resize source with zoom
|
||||
-- Dynamic animations: each frame is {x, y, flipped, flippedV}
|
||||
animNames = {}, -- ordered list of animation names
|
||||
anims = {}, -- name -> list of {x, y, flipped, flippedV}
|
||||
|
|
@ -100,10 +102,10 @@ local S = {
|
|||
-- Setup tab elements (static IDs, dynamic ones added at runtime)
|
||||
local SETUP_IDS = {
|
||||
"sepAnims", "btnNewAnim", "btnDelAnim",
|
||||
"lblSelectedAnim",
|
||||
"btnMoveAnimUp", "btnMoveAnimDown",
|
||||
"sepSource", "canvasSource",
|
||||
"sepFrames", "canvasStrips",
|
||||
"sepActions", "btnFlipX", "btnFlipY", "btnMoveLeft", "btnMoveRight", "btnClearAnim",
|
||||
"sepActions", "btnFlipX", "btnFlipY", "btnMoveLeft", "btnMoveRight", "btnRemoveFrame", "btnClearAnim",
|
||||
"sepPreviewAnim", "canvasPreviewAnim",
|
||||
}
|
||||
|
||||
|
|
@ -195,6 +197,8 @@ local function loadPrefs()
|
|||
if k == "gbLastSavePath" then S.gbLastSavePath = v end
|
||||
if k == "gbLayerName" then S.gbLayerName = v end
|
||||
if k == "gbAlwaysOverwrite" then S.gbAlwaysOverwrite = (v == "true") end
|
||||
if k == "sourceViewportH" then S.sourceViewportH = tonumber(v) or 250 end
|
||||
if k == "sourceAutoResize" then S.sourceAutoResize = (v == "true") end
|
||||
-- Dynamic anim frames: anim_0, anim_1, ...
|
||||
local animIdx = k and string.match(k, "^anim_(%d+)$")
|
||||
if animIdx then
|
||||
|
|
@ -237,6 +241,8 @@ local function savePrefs()
|
|||
f:write("gbLastSavePath=" .. S.gbLastSavePath .. "\n")
|
||||
f:write("gbLayerName=" .. S.gbLayerName .. "\n")
|
||||
f:write("gbAlwaysOverwrite=" .. tostring(S.gbAlwaysOverwrite) .. "\n")
|
||||
f:write("sourceViewportH=" .. S.sourceViewportH .. "\n")
|
||||
f:write("sourceAutoResize=" .. tostring(S.sourceAutoResize) .. "\n")
|
||||
for i, name in ipairs(S.animNames) do
|
||||
f:write("anim_" .. (i - 1) .. "=" .. serializeFrames(S.anims[name] or {}) .. "\n")
|
||||
end
|
||||
|
|
@ -310,7 +316,7 @@ end
|
|||
local function clampScroll()
|
||||
local contentW, contentH = getSourceContentSize()
|
||||
local maxScrollX = math.max(0, contentW - SOURCE_VIEWPORT_W)
|
||||
local maxScrollY = math.max(0, contentH - SOURCE_VIEWPORT_H)
|
||||
local maxScrollY = math.max(0, contentH - S.sourceViewportH)
|
||||
S.scrollX = clamp(S.scrollX, 0, maxScrollX)
|
||||
S.scrollY = clamp(S.scrollY, 0, maxScrollY)
|
||||
end
|
||||
|
|
@ -877,7 +883,7 @@ local function run()
|
|||
|
||||
local contentW, contentH = getSourceContentSize()
|
||||
local viewW = math.min(contentW, SOURCE_VIEWPORT_W)
|
||||
local viewH = math.min(contentH, SOURCE_VIEWPORT_H)
|
||||
local viewH = math.min(contentH, S.sourceViewportH)
|
||||
|
||||
local dlg = Dialog{
|
||||
title = "AniPhallow - Animation Builder",
|
||||
|
|
@ -909,8 +915,6 @@ local function run()
|
|||
end
|
||||
|
||||
local function updateAnimLabel()
|
||||
local name = currentAnimName() or "(none)"
|
||||
dlg:modify{ id = "lblSelectedAnim", text = "Selected -> " .. name }
|
||||
for i = 1, #S.animNames do
|
||||
pcall(function()
|
||||
local label = (i == S.currentAnim) and ("[" .. S.animNames[i] .. "]") or S.animNames[i]
|
||||
|
|
@ -1006,6 +1010,8 @@ local function run()
|
|||
d:number{ id = "previewZoom", label = "Zoom:", text = tostring(S.previewZoom), decimals = 0 }
|
||||
d:check{ id = "useBgColor", text = "Solid background", selected = S.useBgColor }
|
||||
d:color{ id = "bgColor", label = "Bg Color:", color = S.bgColor }
|
||||
d:number{ id = "sourceViewportH", label = "Source Height (px):", text = tostring(S.sourceViewportH), decimals = 0 }
|
||||
d:check{ id = "sourceAutoResize", text = "Auto-resize source with zoom", selected = S.sourceAutoResize }
|
||||
d:separator{ text = "GB Options" }
|
||||
-- Auto-detect darkest palette color on first open
|
||||
local silColor = S.gbSilhouetteColor
|
||||
|
|
@ -1032,6 +1038,10 @@ local function run()
|
|||
S.previewZoom = pz
|
||||
S.useBgColor = d.data.useBgColor
|
||||
S.bgColor = d.data.bgColor
|
||||
local svh = d.data.sourceViewportH
|
||||
if svh < 50 then svh = 50 elseif svh > 600 then svh = 600 end
|
||||
S.sourceViewportH = svh
|
||||
S.sourceAutoResize = d.data.sourceAutoResize
|
||||
S.gbSilhouetteColor = d.data.silhouetteColor
|
||||
S.gbSilhouetteColorSet = true
|
||||
S.gbLayerName = d.data.layerName
|
||||
|
|
@ -1091,11 +1101,32 @@ local function run()
|
|||
end
|
||||
}
|
||||
|
||||
dlg:newrow()
|
||||
dlg:button{
|
||||
id = "btnMoveAnimUp",
|
||||
text = "Move Up",
|
||||
onclick = function()
|
||||
if S.currentAnim <= 1 then return end
|
||||
local i = S.currentAnim
|
||||
S.animNames[i], S.animNames[i - 1] = S.animNames[i - 1], S.animNames[i]
|
||||
S.currentAnim = i - 1
|
||||
savePrefs()
|
||||
dlg:close()
|
||||
run()
|
||||
end
|
||||
}
|
||||
|
||||
dlg:label{
|
||||
id = "lblSelectedAnim",
|
||||
text = "Selected -> " .. (currentAnimName() or "(none)"),
|
||||
dlg:button{
|
||||
id = "btnMoveAnimDown",
|
||||
text = "Move Down",
|
||||
onclick = function()
|
||||
if S.currentAnim < 1 or S.currentAnim >= #S.animNames then return end
|
||||
local i = S.currentAnim
|
||||
S.animNames[i], S.animNames[i + 1] = S.animNames[i + 1], S.animNames[i]
|
||||
S.currentAnim = i + 1
|
||||
savePrefs()
|
||||
dlg:close()
|
||||
run()
|
||||
end
|
||||
}
|
||||
|
||||
dlg:newrow()
|
||||
|
|
@ -1225,6 +1256,10 @@ local function run()
|
|||
local dz = ev.deltaY < 0 and 1 or -1
|
||||
S.sourceZoom = math.max(1, math.min(10, S.sourceZoom + dz))
|
||||
clampScroll()
|
||||
if S.sourceAutoResize then
|
||||
local newH = math.max(60, math.floor(S.sourceViewportH / S.sourceZoom))
|
||||
dlg:modify{ id = "canvasSource", height = newH }
|
||||
end
|
||||
dlg:repaint()
|
||||
end
|
||||
}
|
||||
|
|
@ -1232,7 +1267,7 @@ local function run()
|
|||
----------------------------------------------------------------
|
||||
-- Frame strip for current animation
|
||||
----------------------------------------------------------------
|
||||
dlg:separator{ id = "sepFrames", text = "Frames (click to select, drag to reorder)" }
|
||||
dlg:separator{ id = "sepFrames", text = "Frames (left-click=select, right-click=remove, drag=reorder)" }
|
||||
|
||||
local STRIP_TOTAL_W = SOURCE_VIEWPORT_W
|
||||
local STRIP_CELL = THUMB_SIZE + 1
|
||||
|
|
@ -1339,6 +1374,13 @@ local function run()
|
|||
S.frameDragFrom = idx
|
||||
S.frameDragTo = idx
|
||||
dlg:repaint()
|
||||
elseif ev.button == MouseButton.RIGHT then
|
||||
table.remove(frames, idx)
|
||||
if S.selectedFrame > #frames then
|
||||
S.selectedFrame = #frames
|
||||
end
|
||||
if #frames == 0 then S.selectedFrame = 0 end
|
||||
dlg:repaint()
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
|
@ -1434,6 +1476,23 @@ local function run()
|
|||
end
|
||||
}
|
||||
|
||||
dlg:button{
|
||||
id = "btnRemoveFrame",
|
||||
text = "Remove",
|
||||
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
|
||||
table.remove(frames, S.selectedFrame)
|
||||
if S.selectedFrame > #frames then
|
||||
S.selectedFrame = #frames
|
||||
end
|
||||
if #frames == 0 then S.selectedFrame = 0 end
|
||||
dlg:repaint()
|
||||
end
|
||||
}
|
||||
|
||||
dlg:button{
|
||||
id = "btnClearAnim",
|
||||
text = "Clear Anim",
|
||||
|
|
@ -1609,10 +1668,10 @@ local function run()
|
|||
visible = false,
|
||||
onchange = function() S.gbAnalyzeMode = dlg.data.gbAnalyzeMode end
|
||||
}
|
||||
-- Row: Analyze + >=threshold + Similar (all on one line)
|
||||
-- Row: Analyze Duplicates + Analyze Similars + Similarity threshold (all on one line)
|
||||
dlg:button{
|
||||
id = "btnAnalyze",
|
||||
text = "Analyze",
|
||||
text = "Analyze Duplicates",
|
||||
visible = false,
|
||||
onclick = function()
|
||||
if not S.sourceImage then return end
|
||||
|
|
@ -1633,7 +1692,7 @@ local function run()
|
|||
S.gbScrollY = 0
|
||||
local unique = #S.gbTiles
|
||||
local total = S.gbTotalTiles
|
||||
dlg:modify{ id = "btnAnalyze", text = "Analyze (" .. unique .. "/" .. total .. ")" }
|
||||
dlg:modify{ id = "btnAnalyze", text = "Analyze Duplicates (" .. unique .. "/" .. total .. ")" }
|
||||
if S.gbOptImage then
|
||||
local z = S.gbZoomOpt
|
||||
dlg:modify{ id = "canvasGbOpt",
|
||||
|
|
@ -1643,9 +1702,26 @@ local function run()
|
|||
dlg:repaint()
|
||||
end
|
||||
}
|
||||
dlg:button{
|
||||
id = "btnFindSimilar",
|
||||
text = "Analyze Similars",
|
||||
visible = false,
|
||||
onclick = function()
|
||||
if #S.gbTiles == 0 then
|
||||
app.alert("Run Analyze first.")
|
||||
return
|
||||
end
|
||||
S.gbSimilarPairs = findSimilarTiles(
|
||||
S.sourceImage, S.gbTiles, S.gbSimilarThreshold,
|
||||
S.gbFlipOpt, S.gbOffsetOpt)
|
||||
dlg:modify{ id = "btnFindSimilar",
|
||||
text = "Analyze Similars (" .. #S.gbSimilarPairs .. ")" }
|
||||
dlg:repaint()
|
||||
end
|
||||
}
|
||||
dlg:button{
|
||||
id = "gbSimilarThreshold",
|
||||
text = ">=" .. S.gbSimilarThreshold .. "%",
|
||||
text = "Similarity >=" .. S.gbSimilarThreshold .. "%",
|
||||
visible = false,
|
||||
onclick = function()
|
||||
local d = Dialog{ title = "Similarity Threshold" }
|
||||
|
|
@ -1659,27 +1735,10 @@ local function run()
|
|||
if v < 1 then v = 1 end
|
||||
if v > 100 then v = 100 end
|
||||
S.gbSimilarThreshold = v
|
||||
dlg:modify{ id = "gbSimilarThreshold", text = ">=" .. v .. "%" }
|
||||
dlg:modify{ id = "gbSimilarThreshold", text = "Similarity >=" .. v .. "%" }
|
||||
end
|
||||
end
|
||||
}
|
||||
dlg:button{
|
||||
id = "btnFindSimilar",
|
||||
text = "Similar",
|
||||
visible = false,
|
||||
onclick = function()
|
||||
if #S.gbTiles == 0 then
|
||||
app.alert("Run Analyze first.")
|
||||
return
|
||||
end
|
||||
S.gbSimilarPairs = findSimilarTiles(
|
||||
S.sourceImage, S.gbTiles, S.gbSimilarThreshold,
|
||||
S.gbFlipOpt, S.gbOffsetOpt)
|
||||
dlg:modify{ id = "btnFindSimilar",
|
||||
text = "Similar (" .. #S.gbSimilarPairs .. ")" }
|
||||
dlg:repaint()
|
||||
end
|
||||
}
|
||||
dlg:label{ id = "lblSimilarStats", text = "", visible = false }
|
||||
|
||||
-- Optimized tileset canvas
|
||||
|
|
@ -1743,22 +1802,70 @@ local function run()
|
|||
end
|
||||
end
|
||||
|
||||
-- Highlight tiles involved in similar pairs
|
||||
-- Highlight tiles involved in similar pairs with distinct group colors
|
||||
if #S.gbSimilarPairs > 0 then
|
||||
-- Build groups using union-find
|
||||
local parent = {}
|
||||
local function find(x)
|
||||
if not parent[x] then parent[x] = x end
|
||||
while parent[x] ~= x do
|
||||
parent[x] = parent[parent[x]]
|
||||
x = parent[x]
|
||||
end
|
||||
return x
|
||||
end
|
||||
local function union(a, b)
|
||||
local ra, rb = find(a), find(b)
|
||||
if ra ~= rb then parent[ra] = rb end
|
||||
end
|
||||
|
||||
for _, pair in ipairs(S.gbSimilarPairs) do
|
||||
union(pair.i, pair.j)
|
||||
end
|
||||
|
||||
-- Assign color index to each group root
|
||||
local groupColors = {}
|
||||
local colorIdx = 0
|
||||
-- Predefined distinct hues
|
||||
local GROUP_HUES = {
|
||||
Color(255, 0, 255, 150), -- magenta
|
||||
Color(0, 200, 255, 150), -- cyan
|
||||
Color(255, 128, 0, 150), -- orange
|
||||
Color(0, 255, 128, 150), -- spring green
|
||||
Color(255, 255, 0, 150), -- yellow
|
||||
Color(128, 0, 255, 150), -- purple
|
||||
Color(255, 0, 128, 150), -- pink
|
||||
Color(0, 255, 0, 150), -- green
|
||||
Color(0, 128, 255, 150), -- blue
|
||||
Color(255, 64, 64, 150), -- red
|
||||
Color(128, 255, 0, 150), -- lime
|
||||
Color(255, 0, 64, 150), -- crimson
|
||||
Color(0, 255, 255, 150), -- aqua
|
||||
Color(200, 100, 255, 150), -- lavender
|
||||
Color(255, 200, 0, 150), -- gold
|
||||
Color(100, 255, 200, 150), -- mint
|
||||
}
|
||||
|
||||
local similarSet = {}
|
||||
for _, pair in ipairs(S.gbSimilarPairs) do
|
||||
similarSet[pair.i] = true
|
||||
similarSet[pair.j] = true
|
||||
local root = find(pair.i)
|
||||
if not groupColors[root] then
|
||||
colorIdx = colorIdx + 1
|
||||
groupColors[root] = GROUP_HUES[((colorIdx - 1) % #GROUP_HUES) + 1]
|
||||
end
|
||||
similarSet[pair.i] = groupColors[find(pair.i)]
|
||||
similarSet[pair.j] = groupColors[find(pair.j)]
|
||||
end
|
||||
|
||||
local rw = ts * z
|
||||
local rh = ts * z
|
||||
for idx, _ in pairs(similarSet) do
|
||||
for idx, c in pairs(similarSet) do
|
||||
local tx, ty = tilePos(idx)
|
||||
local srx = tx * z - S.gbScrollX
|
||||
local sry = ty * z - S.gbScrollY
|
||||
gc.color = Color(255, 0, 255, 40)
|
||||
gc.color = Color(c.red, c.green, c.blue, 40)
|
||||
gc:fillRect(Rectangle(srx, sry, rw, rh))
|
||||
gc.color = Color(255, 0, 255, 150)
|
||||
gc.color = c
|
||||
gc:fillRect(Rectangle(srx, sry, rw, 1))
|
||||
gc:fillRect(Rectangle(srx, sry + rh - 1, rw, 1))
|
||||
gc:fillRect(Rectangle(srx, sry, 1, rh))
|
||||
|
|
@ -1993,7 +2100,9 @@ local function run()
|
|||
end
|
||||
end
|
||||
|
||||
local imgCopy = S.gbOptImage:clone()
|
||||
local imgConverted = Image(S.gbOptImage.width, S.gbOptImage.height, sp.colorMode)
|
||||
imgConverted:clear()
|
||||
imgConverted:drawImage(S.gbOptImage, Point(0, 0))
|
||||
app.transaction("Save Optimized Layer", function()
|
||||
if existingLayer then
|
||||
if S.gbAlwaysOverwrite then
|
||||
|
|
@ -2001,7 +2110,8 @@ local function run()
|
|||
for _, cel in ipairs(existingLayer.cels) do
|
||||
sp:deleteCel(cel)
|
||||
end
|
||||
sp:newCel(existingLayer, app.frame, imgCopy, Point(0, 0))
|
||||
sp:newCel(existingLayer, app.frame, imgConverted, Point(0, 0))
|
||||
existingLayer.isEditable = false
|
||||
else
|
||||
local suffix = 2
|
||||
local newName = layerName .. "-" .. suffix
|
||||
|
|
@ -2019,12 +2129,14 @@ local function run()
|
|||
end
|
||||
local newLayer = sp:newLayer()
|
||||
newLayer.name = newName
|
||||
sp:newCel(newLayer, app.frame, imgCopy, Point(0, 0))
|
||||
sp:newCel(newLayer, app.frame, imgConverted, Point(0, 0))
|
||||
newLayer.isEditable = false
|
||||
end
|
||||
else
|
||||
local newLayer = sp:newLayer()
|
||||
newLayer.name = layerName
|
||||
sp:newCel(newLayer, app.frame, imgCopy, Point(0, 0))
|
||||
sp:newCel(newLayer, app.frame, imgConverted, Point(0, 0))
|
||||
newLayer.isEditable = false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue