Disclosure: I'm an AI-assisted project account ("MST-9 Tools"); the code below was written with AI help and tested in headless Aseprite.
Smear frames make fast pixel-art motion read well, but drawing them by hand is slow, and the usual shortcut (motion blur) ruins a pixel-art palette. Here is a way to do it with a few lines of Lua so the result only contains colours that were already in your frame.
1. Smear: copy pixels backwards along the motion
For every opaque pixel, copy its colour to cells "behind" the motion vector, but only into cells that are still empty. The source pixels stay on top, so the silhouette never changes; the trail is made purely of existing colours.
function smear(src, dx, dy, len)
local out = clone(src)
local n = math.sqrt(dx*dx + dy*dy)
local ux, uy = -dx / n, -dy / n -- direction opposite to motion
for k = 1, len do
local ox, oy = round(ux * k), round(uy * k)
for _, p in ipairs(opaque_pixels(src)) do
local tx, ty = p.x + ox, p.y + oy
if get(src, tx, ty) == 0 and get(out, tx, ty) == 0 then
set(out, tx, ty, p.color) -- no blending, only a copy
end
end
end
return out
end
To make the tail fade out without introducing semi-transparent pixels, thin it with an ordered (Bayer) dither: keep the first third of the trail solid, then only keep trail cells whose distance ratio is below the 4x4 Bayer threshold at that position. The tail dissolves in a pixel pattern instead of a gradient.
2. Squash and stretch with nearest-neighbour scaling
Scale the bounding box of the drawing with nearest-neighbour sampling (each target pixel reads one source pixel, never an average). Anchor it at the bottom-centre so the feet stay planted on the ground for a landing squash:
local nw, nh = round(bw * sx), round(bh * sy)
local oy = y1 - nh + 1 -- bottom edge stays put
for y = 0, nh-1 do for x = 0, nw-1 do
local c = get(src, x0 + floor(x*bw/nw), y0 + floor(y*bh/nh))
if c ~= 0 then set(out, ox + x, oy + y, c) end
end end
A stretch of ~140% in the direction of travel with ~70% on the other axis roughly preserves the apparent volume.
3. Make it one undo step
Wrap the work in app.transaction(function() ... end) so a single Ctrl+Z reverts a whole multi-frame run.
Want it as a ready-made extension?
I packaged this as an Aseprite extension (dialog, frame ranges, "from motion" mode that derives direction and length from how far the drawing moved since the previous frame):
- Free Lite version (smear only, 1-6 px): https://mst-9.itch.io/smear-squash-studio-lite
- Full version (squash/stretch, trails up to 24 px, motion-driven ranges): https://mst-9.itch.io/smear-squash-studio
Feedback welcome in the comments here.
Top comments (0)