Hi all,
I stumbled onto the feature where you can create a sprite with multiple palettes. There are quite a few requests for this on Github, and I’m sure here as well. So you may already know that it is partially implemented.
A problem is that it can be triggered unintentionally and without realization. For example, I found it by opening a sequentially named .png
saved with indexed color mode. When a Notice
appeared asking me if I wanted to open the files as an animation, I clicked Agree
.
Here are test images if you would like to try and reproduce:
Why this matters: There are recurring issues with sprites and palettes, especially on export to .gif
, but also with the concept of transparency when e.g. outline is used in indexed color mode. If you’ve had a problem like this, the possibility of multiple palettes make it harder to diagnose.
If you help others to fix this issue cluster on the forums, it is another question to add to your diagnostic checklist.
Because you can create multi-palette sprites unintentionally, each palette can have a different number of colors. A sprite’s transparent color index could point to two different colors depending on the frame, or become invalid if it points to an index that is out of bounds for the active palette.
If you add, remove or relocate frames from the sprite afterwards, it’s possible for the number of palettes and the number of frames in the sprite to go out of sync. From what I’ve tested thus far, it seems possible to have 4 palettes and 7 frames, with the frame at index 7 pointing to the palette at index 4.
There is some ability to work on this with Lua scripting. For example, you can get a palette from the palettes array. To set, ignore Sprite:setPalette
; instead, resize
the palette, then reassign its colors. I think you can get a frame from a palette,
for k, v in ipairs(app.activeSprite.palettes) do
print(v.frame)
print(v.frameNumber)
end
I don’t know how to get the appropriate palette from a given frame, though. Again, it seems possible for a palette to be used by multiple frames.
Here is a WIP script which attempts to clean up. It’s not a silver bullet, but based on what I know atm. I copypastaed supporting functions from here and here so this to stand on its own. See the sources for more comment.
local function aseColorToHex(clr, clrMode)
if clrMode == ColorMode.RGB then
return (clr.alpha << 0x18)
| (clr.blue << 0x10)
| (clr.green << 0x08)
| clr.red
elseif clrMode == ColorMode.GRAY then
return clr.grayPixel
elseif clrMode == ColorMode.INDEXED then
return math.tointeger(clr.index)
end
return 0
end
local function asePalettesToHexArr(palettes)
if palettes then
local lenPalettes = #palettes
local hexes = {}
for i = 1, lenPalettes, 1 do
local palette = palettes[i]
if palette then
local lenPalette = #palette
for j = 1, lenPalette, 1 do
local aseColor = palette:getColor(j - 1)
local hex = aseColorToHex(
aseColor, ColorMode.RGB)
table.insert(hexes, hex)
end
end
end
if #hexes == 1 then
local aMask = hexes[1] & 0xff000000
table.insert(hexes, 1, aMask)
hexes[3] = aMask | 0x00ffffff
end
return hexes
else
return { 0x00000000, 0xffffffff }
end
end
local function hexArrToDict(hexes, zeroAlpha)
local dict = {}
local idxKey = 1
local len = #hexes
for i = 1, len, 1 do
local hex = hexes[i]
if zeroAlpha then
local a = (hex >> 0x18) & 0xff
if a < 1 then hex = 0x00000000 end
end
if not dict[hex] then
dict[hex] = idxKey
idxKey = idxKey + 1
end
end
return dict
end
local function uniqueColors(hexes, zeroAlpha)
local dict = hexArrToDict(hexes, zeroAlpha)
local uniques = {}
for k, v in pairs(dict) do
uniques[v] = k
end
return uniques, dict
end
local function prependMask(hexes)
if hexes[1] == 0x0 then return hexes end
local cDict = hexArrToDict(hexes, false)
local maskIdx = cDict[0x0]
if maskIdx then
if maskIdx > 1 then
table.remove(hexes, maskIdx)
table.insert(hexes, 1, 0x0)
end
else
table.insert(hexes, 1, 0x0)
end
return hexes
end
local function hexToAseColor(hex)
return Color(hex & 0xff,
(hex >> 0x08) & 0xff,
(hex >> 0x10) & 0xff,
(hex >> 0x18) & 0xff)
end
local function changePixelFormat(format)
if format == ColorMode.INDEXED then
app.command.ChangePixelFormat { format = "indexed" }
elseif format == ColorMode.GRAY then
app.command.ChangePixelFormat { format = "gray" }
elseif format == ColorMode.RGB then
app.command.ChangePixelFormat { format = "rgb" }
end
end
local activeSprite = app.activeSprite
if not activeSprite then return end
local oldColorMode = activeSprite.colorMode
app.command.ChangePixelFormat { format = "rgb" }
activeSprite.transparentColor = 0
local hexesProfile = asePalettesToHexArr(activeSprite.palettes)
local uniques, _ = uniqueColors(hexesProfile, true)
local masked = prependMask(uniques)
local aseColors = {}
local lenAseColors = #masked
for i = 1, lenAseColors, 1 do
aseColors[i] = hexToAseColor(masked[i])
end
local palettes = activeSprite.palettes
local palettesLen = #palettes
-- Safe to assign the same Aseprite color to multiple
-- palettes because they are copied by value, not
-- passed by reference...?
-- https://github.com/aseprite/aseprite/blob/main/
-- src/app/script/palette_class.cpp#L196
app.transaction(function()
for i = 1, palettesLen, 1 do
local palette = palettes[i]
palette:resize(lenAseColors)
for j = 1, lenAseColors, 1 do
local aseColor = aseColors[j]
palette:setColor(j - 1, aseColor)
end
end
end)
changePixelFormat(oldColorMode)
app.refresh()
This summarizes what the script does:
- Convert sprite to RGB color mode.
- Set the sprite’s transparent color / mask index to zero.
- Gets all the colors from all the palettes in one array of 32 bit integers.
- Finds unique colors. Any color with zero alpha is treated as equal to clear black.
- Adds clear black as the first element of the palette (or relocates it).
- Converts the 32 bit integers to
Color
userdata. - Sets all sprite palettes to this composite.
- Returns sprite to former color mode.
I originally ran into this when making a custom sprite properties. As pictured above, I’ve since developed that script to include a sprite’s number of palettes. Something like that would tell you if you have to worry about this in the first place.
A feature request would be to make the default sprite preferences show this, at least so you can debug.
My final recommendation is to go to Edit > Preferences > Alerts
and to set the option labeled Open a sequence of static files as an animation
to either No
or Ask
. A feature request would be for such a notice to indicate it will create multiple palettes.
Cheers,
Jeremy