Rotating Square Brush Script

Below is a script that draws a rotated square on a canvas graphics context. When the ok button is pressed, it creates an image from the canvas and assigns it to a custom brush.

The beta version of Aseprite is required to assign the square to a brush. The screen cap is from 1.3.14-beta1.

The built-in square brush is red. The script brush is in green. The anti-aliased brush is in blue.

local prevBrush = app.brush
local defaultSize = 32
local defaultAngle = 0
if prevBrush then
    defaultSize = math.max(1, prevBrush.size or 1)
    defaultAngle = prevBrush.angle or 0
end

local function checker(
    wImg, hImg, bpp, wCheck, hCheck, aColor, bColor)
    local checkered = {}
    local lenTrg = wImg * hImg
    local fmtStr = "<I" .. bpp
    local strpack = string.pack
    local i = 0
    while i < lenTrg do
        local x = i // wImg
        local y = i % wImg
        local c = bColor
        if (((x // wCheck) + (y // hCheck)) % 2) ~= 1 then
            c = aColor
        end
        checkered[1 + i] = strpack(fmtStr, c)
        i = i + 1
    end
    return table.concat(checkered)
end

local function drawSquare(ctx, useAntiAlias, brushColor, size, degrees)
    local wCanvas = ctx.width
    local hCanvas = ctx.height

    local xCenterReal = wCanvas * 0.5
    local yCenterReal = hCanvas * 0.5
    local xCenterInt = math.floor(xCenterReal)
    local yCenterInt = math.floor(yCenterReal)
    local xCenter = useAntiAlias and xCenterReal or xCenterInt
    local yCenter = useAntiAlias and yCenterReal or yCenterInt

    local sizeHalfReal = size * 0.5

    local points = {
        { xCenter - sizeHalfReal, yCenter + sizeHalfReal },
        { xCenter + sizeHalfReal, yCenter + sizeHalfReal },
        { xCenter + sizeHalfReal, yCenter - sizeHalfReal },
        { xCenter - sizeHalfReal, yCenter - sizeHalfReal }
    }

    local degMod90 = degrees % 90
    if degMod90 ~= 0 then
        local radians = math.rad(degrees)
        local cosa = math.cos(radians)
        local sina = math.sin(radians)
        local cosaSzHf = cosa * sizeHalfReal
        local sinaSzHf = sina * sizeHalfReal

        points[1][1] = xCenter - cosaSzHf + sinaSzHf
        points[1][2] = yCenter + cosaSzHf + sinaSzHf
        points[2][1] = xCenter + cosaSzHf + sinaSzHf
        points[2][2] = yCenter + cosaSzHf - sinaSzHf
        points[3][1] = xCenter + cosaSzHf - sinaSzHf
        points[3][2] = yCenter - cosaSzHf - sinaSzHf
        points[4][1] = xCenter - cosaSzHf - sinaSzHf
        points[4][2] = yCenter - cosaSzHf + sinaSzHf
    end

    if not useAntiAlias then
        points[1][1] = math.floor(points[1][1])
        points[1][2] = math.floor(points[1][2])
        points[2][1] = math.floor(points[2][1])
        points[2][2] = math.floor(points[2][2])
        points[3][1] = math.floor(points[3][1])
        points[3][2] = math.floor(points[3][2])
        points[4][1] = math.floor(points[4][1])
        points[4][2] = math.floor(points[4][2])
    end

    ctx.antialias = useAntiAlias
    ctx.color = brushColor
    ctx:beginPath()
    ctx:moveTo(points[1][1], points[1][2])
    ctx:lineTo(points[2][1], points[2][2])
    ctx:lineTo(points[3][1], points[3][2])
    ctx:lineTo(points[4][1], points[4][2])
    ctx:closePath()
    ctx:fill()
end

local dlg = Dialog { title = "Square Brush" }

dlg:canvas {
    id = "brushPreview",
    label = "Preview:",
    focus = true,
    width = 128,
    height = 128,
    onpaint = function(event)
        local args = dlg.data
        local useAntiAlias = (args.useAntiAlias --[[@as boolean]])
            or false
        local brushColor = (args.brushColor --[[@as Color]])
            or Color { r = 255, g = 255, b = 255, a = 255 }
        local size = (args.size --[[@as integer]]) or 1
        local degrees = (args.degrees --[[@as integer]]) or 0

        local ctx = event.context
        local wCanvas = ctx.width
        local hCanvas = ctx.height

        local bkgChecker = Image(
            wCanvas, hCanvas,
            ColorMode.RGB)
        bkgChecker.bytes = checker(
            wCanvas, hCanvas,
            4,
            16, 16,
            0xff3b3b3b, 0xff262626)
        ctx:drawImage(
            bkgChecker,
            Rectangle(0, 0, wCanvas, hCanvas),
            Rectangle(0, 0, wCanvas, hCanvas))

        drawSquare(ctx, useAntiAlias, brushColor, size, degrees)
    end
}

dlg:check {
    id = "useAntiAlias",
    label = "Enable:",
    text = "&Anti-Alias",
    selected = false,
    onclick = function() dlg:repaint() end
}

dlg:color {
    id = "brushColor",
    label = "Color:",
    color = Color {
        r = app.fgColor.red,
        g = app.fgColor.green,
        b = app.fgColor.blue,
        a = app.fgColor.alpha > 0
            and app.fgColor.alpha
            or 255
    },
    onchange = function() dlg:repaint() end
}

dlg:slider {
    id = "size",
    label = "Size:",
    min = 1,
    max = 64,
    value = defaultSize,
    onchange = function() dlg:repaint() end
}

dlg:slider {
    id = "degrees",
    label = "Angle:",
    min = -180,
    max = 180,
    value = defaultAngle,
    onchange = function() dlg:repaint() end
}

dlg:button {
    id = "confirm",
    text = "&OK",
    focus = false,
    onclick = function()
        local args = dlg.data
        local useAntiAlias = (args.useAntiAlias --[[@as boolean]])
            or false
        local brushColor = (args.brushColor --[[@as Color]])
            or Color { r = 255, g = 255, b = 255, a = 255 }
        local size = (args.size --[[@as integer]]) or 1
        local degrees = (args.degrees --[[@as integer]]) or 0

        local alphaIndex = 0
        local colorMode = ColorMode.RGB
        local colorSpace = ColorSpace { sRGB = false }

        local activeSprite = app.sprite
        if activeSprite then
            local activeSpec = activeSprite.spec
            alphaIndex = activeSpec.transparentColor
            colorMode = activeSpec.colorMode
            colorSpace = activeSpec.colorSpace
        end

        if size <= 1 then
            local imageSpec = ImageSpec {
                width = 1,
                height = 1,
                transparentColor = alphaIndex,
                colorMode = colorMode
            }
            imageSpec.colorSpace = colorSpace
            local image = Image(imageSpec)
            image:clear(brushColor)
            app.brush = Brush {
                type = BrushType.IMAGE,
                image = image,
                center = Point(0, 0)
            }
            app.tool = "pencil"
            return
        end

        local imageSpec = ImageSpec {
            width = 128,
            height = 128,
            transparentColor = alphaIndex,
            colorMode = colorMode
        }
        imageSpec.colorSpace = colorSpace
        local image = Image(imageSpec)

        local ctx = image.context
        if ctx then
            local useAAVerif = useAntiAlias
                and colorMode ~= ColorMode.INDEXED
            drawSquare(ctx, useAAVerif, brushColor, size, degrees)

            local trimRect = image:shrinkBounds(alphaIndex)
            local rectIsValid = trimRect.width > 0
                and trimRect.height > 0
            if rectIsValid then
                local trimSpec = ImageSpec {
                    width = trimRect.width,
                    height = trimRect.height,
                    transparentColor = imageSpec.transparentColor,
                    colorMode = imageSpec.colorMode
                }
                trimSpec.colorSpace = imageSpec.colorSpace
                local trimmed = Image(trimSpec)
                trimmed:drawImage(
                    image, Point(-trimRect.x, -trimRect.y),
                    255, BlendMode.SRC)

                app.brush = Brush {
                    type = BrushType.IMAGE,
                    image = trimmed,
                    center = Point(
                        trimRect.width // 2,
                        trimRect.height // 2)
                }
                app.tool = "pencil"
            end
        end
    end
}

dlg:button {
    id = "cancel",
    text = "&CANCEL",
    focus = false,
    onclick = function()
        dlg:close()
    end
}

dlg:show {
    autoscrollbars = false,
    wait = false,
}

[Edit: Fixed missing beginPath]

For grayscale color mode, there’s an extra wrinkle.

The idea can be generalized to any shape you can make with straight lines and cubic Bezier curves. You might be able to make updating the brush more convenient, too, so you don’t have to press the ok button.

Prior discussions that prompted me to share the code:

1 Like