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: