Cycle/shift selected pixels by frames

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:

  1. 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.
  2. Rename the file something like “Cycle Forward.lua”.
  3. Restart Aseprite so it can find the script.
  4. (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:

  1. 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.

  2. 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.

  3. 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