Create Silhouette Feature?

Please add a feature where with a click of a button a silhouette is created from a sprite (on a separate frame or layer).

In case this feature request doesn’t get any traction, you could make a Lua script to implement the feature per your needs. Scripts can be assigned keyboard shortcuts from in the edit menu. For sample code, see below.

While not as convenient as one button press, there are some built-in tools that are also helpful:

  • The lock alpha ink plus the the pencil or paint bucket tool.
  • Hue/Saturation adjustment can make silhouettes if you set the HSL lightness to -100 or +100. (RGB color mode only. In Grayscale it seems to work differently. In Indexed it targets the palette, not the canvas.)
  • A selection can be made around non-zero alpha pixels in a cel image by holding down Ctrl and clicking on a layer’s name in the timeline. (It can be finicky, though, if the layer is already active or selected in a range.) There are also Lua scripts/plugins that use do the same with a hotkey.

Test image: Chun-Li from Street Fighter Chun-Li/Sprites | Street Fighter Wiki | Fandom

local activeSprite = app.activeSprite
if not activeSprite then return end

-- Unpack sprite spec.
local spec = activeSprite.spec
local colorMode = spec.colorMode
local alphaIndex = spec.transparentColor

local bkgLayer = activeSprite.backgroundLayer
if colorMode ~= ColorMode.INDEXED
    and bkgLayer and bkgLayer.isVisible then
    app.alert { title = "Error", text = "Sprite has an opaque background." }
    return
end

-- Base silhouette color on foreground color.
local silhColor = app.fgColor

-- For RGB color mode.
local rSilh = silhColor.red
local gSilh = silhColor.green
local bSilh = silhColor.blue
local silhAbgr32 = app.pixelColor.rgba(rSilh, gSilh, bSilh, 255)
local silhBgr32 = silhAbgr32 & 0x00ffffff

-- For gray color mode.
-- Aseprite formula for luma gray conversion:
local gray = (rSilh * 2126 + gSilh * 7152 + bSilh * 722) // 10000
local silhAv16 = app.pixelColor.graya(gray, 255)
local silhV8 = silhAv16 & 0x00ff

-- For indexed color mode.
local silhIndex = silhColor.index
if silhIndex == alphaIndex or silhColor.alpha <= 0 then
    local palettes = activeSprite.palettes
    local palette = palettes[1]
    local lenPalette = #palette
    local h = 0
    while h < lenPalette do
        if h ~= alphaIndex and palette:getColor(h).alpha > 0 then
            silhIndex = h
            break
        end
        h = h + 1
    end
end

app.transaction(function()
    local silhLayer = activeSprite:newLayer()
    silhLayer.name = "Silhouette"

    local frames = activeSprite.frames
    local lenFrames = #frames

    local i = 0
    while i < lenFrames do
        i = i + 1
        local frame = frames[i]

        local flattened = Image(spec)
        flattened:drawSprite(activeSprite, frame)

        if colorMode == ColorMode.INDEXED then
            for pixel in flattened:pixels() do
                if pixel() ~= alphaIndex then
                    pixel(silhIndex)
                else
                    pixel(alphaIndex)
                end
            end
        elseif colorMode == ColorMode.GRAY then
            for pixel in flattened:pixels() do
                local sourceAv16 = pixel()
                pixel(sourceAv16 & 0xff00 | silhV8)
            end
        else
            for pixel in flattened:pixels() do
                local sourceAbgr32 = pixel()
                pixel(sourceAbgr32 & 0xff000000 | silhBgr32)
            end
        end

        activeSprite:newCel(silhLayer, frame, flattened)
    end
end)

The foreground color is used as the silhouette color. This creates a silhouette for all frames, not just the active frame. The silhouette images are not trimmed of extra alpha, though they could be either with app.command.CanvasSize or with a function dependent on Image:shrinkBounds (in newer versions of Aseprite).

1 Like