Does anyone know a way to move/cycle selected pixels from the selected frames ahead or back by a number of frames? This kind of functionality would be extremely useful when animating and you’re trying to play around with secondary motion offsets. I see there is the ability to move entire frames by dragging the cells in the timeline, but this ignores any marquee selection you have. Would it be possible to have this operation optionally respect the marquee selection? Could be a setting in the preferences.
1 Like
I’ve gone ahead and added this functionality as a script to be used in Aseprite. It cycles images or selected pixels on selected frames forward by a frame. I’ve found this very useful for tweaking and retiming animation cycles.
Setup:
- Copy the provided code into a text file and place into your Aseprite scripts folder. You can find this folder from Aseprite by going File->Scripts->Open Scripts Folder.
- Rename the file something like “Cycle Forward.lua”.
- Restart Aseprite so it can find the script.
- (Optional) I’ve found it handy to assign it to a hotkey via Edit->Keyboard Shortcuts.
-- Cycle images or selected pixels in selected frames ahead by a frame
-- Created by Neil Rubie July 2020 <neilrubie@yahoo.ca>
-- Ensure we can use UI
if not app.isUIAvailable then
return
end
-- Ensure a sprite is loaded
if app.activeSprite == nil then
app.alert("No open sprite found.")
return
end
-- Ensure the current layer is a valid one
if not app.activeLayer.isEditable then
app.alert("Unable to operate on locked layer.")
return
end
-- Ensure that only a single layer is being operated on
if #app.range.layers ~= 1 then
app.alert("Please select frames/cels from a single layer.")
return
end
-- Ensure that more than one frame/cel is selected
if #app.range.frames < 2 then
app.alert("Please select multiple frames/cels to cycle forward.")
return
end
local function get_mask_pixels(selection)
-- Get any pixels that are within the selection bounds, but not actually selected.
-- This is to handle custom non-rectangular pixel selections.
local mask_pixels = {}
local i = 1
for x = 0, selection.bounds.width - 1 do
for y = 0, selection.bounds.height - 1 do
if not selection:contains(selection.bounds.x + x, selection.bounds.y + y) then
mask_pixels[i] = Point(x, y)
i = i + 1
end
end
end
return mask_pixels
end
local function copy_image(layer, frame_number, selection)
-- If there's no pixel selection, copy the entire canvas image
if selection.isEmpty then
selection = app.activeSprite.bounds
end
-- Start with a blank image the size of the selection rectangle.
local image = Image(selection.width, selection.height)
-- If there's an existing cel, check to see if its image overlaps
-- with our selection rectangle. If it does, copy it over.
local cel = layer:cel(frame_number)
if cel then
local offset_rectangle = Rectangle(cel.bounds.x-selection.x, cel.bounds.y-selection.y, cel.bounds.width, cel.bounds.height)
if Rectangle(0, 0, image.width, image.height):intersects(offset_rectangle) then
image:drawImage(cel.image, Point(offset_rectangle.x, offset_rectangle.y))
end
end
return image
end
local function paste_image(image, layer, frame_number, selection, mask_pixels)
-- Start with a blank image the size of the canvas.
local new_image = Image(app.activeSprite.width, app.activeSprite.height)
-- If there's an existing cel, draw it's image to our new image
local cel = layer:cel(frame_number)
if cel then
new_image:drawImage(cel.image, cel.position)
end
-- If there are mask pixels, simulate a masking effect for the relevant pixels on the final image
if mask_pixels[1] then
-- Make a copy of the destination bounds to be pasted to. We will use
-- this to sample pixels that need to be masked in the final image.
local destination_copy = copy_image(layer, frame_number, selection.bounds)
-- For all mask pixels, sample the destination copy and draw that pixel
-- value onto the final image instead of the passed in image pixel value.
for i = 1, #mask_pixels do
local pixel = mask_pixels[i]
image:drawPixel(pixel.x, pixel.y, destination_copy:getPixel(pixel.x, pixel.y))
end
end
-- Draw the passed in image to our new image
new_image:drawImage(image, selection.origin)
-- Bind our newly constructed image to a cell and assign it to the specified frame
cel = app.activeSprite:newCel(layer, frame_number, new_image, Point(0, 0))
-- Delete cel if it's got an empty image in it. Perhaps not necessary but Aseprite seems to do it.
if cel.image:isEmpty() then
app.activeSprite:deleteCel(cel)
end
end
local function cycle_frame_images()
local layer = app.activeLayer
local selected_pixels = app.activeSprite.selection
local selected_frames = app.range.frames
local frame_count = #selected_frames
local frame_offset = 1
local copied_images = {}
local mask_pixels = get_mask_pixels(selected_pixels)
-- Copy images for all selected frames to memory
for i = 1, frame_count do
copied_images[i] = copy_image(layer, selected_frames[i].frameNumber, selected_pixels.bounds)
end
-- Paste images to their respective offset frames
for i = 1, frame_count do
-- Calculate the frame offset index
offset_i = ((i + frame_offset - 1) % frame_count) + 1
-- Draw the image to its new offset frame
paste_image(copied_images[i], layer, selected_frames[offset_i].frameNumber, selected_pixels, mask_pixels)
end
end
-- Wrap the operation in a transaction so it can be undone in one chunk
app.transaction(
function()
-- local x = os.clock()
cycle_frame_images()
-- print(string.format("elapsed time: %.2f\n", os.clock() - x))
end
)
Usage:
-
Select pixels on your canvas that you want to cycle forward by a frame. If you don’t select anything, the entire frame image will be cycled forward.
-
Select frames/cels in the timeline that you wish to be cycled. They don’t have to be consecutive, however they must be on the same layer.
-
Run the script by going File->Scripts->Cycle Forward (or use a hotkey if you assigned one)
I’ve only used this in rgba mode, havent tested other modes. Hope someone else finds this useful!
Cheers!
4 Likes