Skip to content

Instantly share code, notes, and snippets.

@Clemapfel
Last active May 13, 2026 19:33
Show Gist options
  • Select an option

  • Save Clemapfel/cde6f9e3c8585f0559da1b35d651724e to your computer and use it in GitHub Desktop.

Select an option

Save Clemapfel/cde6f9e3c8585f0559da1b35d651724e to your computer and use it in GitHub Desktop.
9-Slice Frame in Love2D
local create_frame = function(
frame_x, frame_y, frame_w, frame_h, -- dimensions of the frame in world space, in px
frame_x_thickness, -- width of the vertical part of the frame in world space, in px
frame_y_thickness, -- height of the horizontal part of the frame in world space, in px
texture_atlas, -- love.Texture
-- pixel coordinates of the sprites in the texture atlas, in texture space, non-normalized
top_x, top_y, top_w, top_h, -- top horizontal bar
right_x, right_y, right_w, right_h, -- right vertical bar
bottom_x, bottom_y, bottom_w, bottom_h, -- bottom horizontal bar
left_x, left_y, left_w, left_h, -- left vertical bar
top_left_corner_x, top_left_corner_y, -- top left corner sprite
top_left_corner_w, top_left_corner_h,
top_right_corner_x, top_right_corner_y, -- top right corner sprite
top_right_corner_w, top_right_corner_h,
bottom_right_corner_x, bottom_right_corner_y, -- bottom right corner sprite
bottom_right_corner_w, bottom_right_corner_h,
bottom_left_corner_x, bottom_left_corner_y, -- bottom left corner sprite
bottom_left_corner_w, bottom_left_corner_h,
backdrop_x, backdrop_y, backdrop_w, backdrop_h -- full frame background, optional
)
assert(texture_atlas:typeOf("Texture"), "In create_frame: wrong argument type for argument #7: expected `Texture`")
local data = {}
local indices = {}
local index_offset = 1
local atlas_w, atlas_h = texture_atlas:getDimensions()
local add_quad = function(min_x, min_y, max_x, max_y, tx, ty, tw, th)
local w, h = max_x - min_x, max_y - min_y
table.insert(data, {
min_x + 0, min_y + 0, -- vertex xy
(tx + 0) / atlas_w, (ty + 0) / atlas_h, -- normalize px coordinates to [0, 1] texture uv
1, 1, 1, 1 -- vertex rgba
})
table.insert(data, {
min_x + w, min_y + 0,
(tx + tw) / atlas_w, (ty + 0) / atlas_h,
1, 1, 1, 1
})
table.insert(data, {
min_x + w, min_y + h,
(tx + tw) / atlas_w, (ty + th) / atlas_h,
1, 1, 1, 1
})
table.insert(data, {
min_x + 0, min_y + h,
(tx + 0) / atlas_w, (ty + th) / atlas_h,
1, 1, 1, 1
})
table.insert(indices, index_offset + 0)
table.insert(indices, index_offset + 1)
table.insert(indices, index_offset + 2)
table.insert(indices, index_offset + 0)
table.insert(indices, index_offset + 2)
table.insert(indices, index_offset + 3)
index_offset = index_offset + 4
end
if backdrop_x ~= nil and backdrop_y ~= nil and backdrop_w ~= nil and backdrop_h ~= nil then
add_quad(
frame_x + frame_x_thickness, frame_y + frame_y_thickness,
frame_x + frame_w - frame_x_thickness, frame_y + frame_h - frame_y,
backdrop_x, backdrop_y, backdrop_w, backdrop_h
)
end
add_quad(
frame_x, frame_y,
frame_x + frame_x_thickness, frame_y + frame_y_thickness,
top_left_corner_x, top_left_corner_y, top_left_corner_w, top_left_corner_h
)
add_quad(
frame_x + frame_w - frame_x_thickness, frame_y,
frame_x + frame_w, frame_y + frame_y_thickness,
top_right_corner_x, top_right_corner_y, top_right_corner_h, top_right_corner_h
)
add_quad(
frame_x + frame_w - frame_x_thickness, frame_y + frame_h - frame_y_thickness,
frame_x + frame_w, frame_y + frame_h,
bottom_right_corner_x, bottom_right_corner_y, bottom_right_corner_w, bottom_right_corner_h
)
add_quad(
frame_x, frame_y + frame_h - frame_y_thickness,
frame_x + frame_x_thickness, frame_y + frame_h,
bottom_right_corner_x, bottom_right_corner_y, bottom_right_corner_w, bottom_right_corner_h
)
add_quad(
frame_x + frame_x_thickness, frame_y,
frame_x + frame_w - frame_x_thickness, frame_y + frame_y_thickness,
top_x, top_y, top_w, top_h
)
add_quad(
frame_x + frame_w - frame_x_thickness, frame_y + frame_y_thickness,
frame_x + frame_w, frame_y + frame_h - frame_y_thickness,
right_x, right_y, right_w, right_h
)
add_quad(
frame_x + frame_x_thickness, frame_y + frame_h - frame_y_thickness,
frame_x + frame_w - frame_x_thickness, frame_y + frame_h,
bottom_x, bottom_y, bottom_w, bottom_h
)
add_quad(
frame_x, frame_y + frame_y_thickness,
frame_x + frame_x_thickness, frame_y + frame_h - frame_y_thickness,
left_x, left_y, left_w, left_h
)
local mesh = love.graphics.newMesh(data, "triangles", "dynamic")
mesh:setVertexMap(indices)
mesh:setTexture(texture_atlas)
return mesh
end
-- ### usage ###
local mesh = nil
local allocate_mesh = function()
local atlas = love.graphics.newImage("assets/sprites/barboach.png")
atlas:setFilter("nearest") -- not required
-- pixel positions for the sprites inside the texture atlas
local sprite_x, sprite_y = 0, 0
local sprite_w, sprite_h = atlas:getDimensions()
local frame_x_thickness = 2 * sprite_w
local frame_y_thickness = 2 * sprite_h
-- pixel positions of the area the frame should cover
local margin = 50
local x, y = margin, margin
local w = love.graphics.getWidth() - 2 * margin
local h = love.graphics.getHeight() - 2 * margin
-- inside of the frame is the area at position: x + frame_x_thickness, y + frame_y_thickness,
-- with width / height: w - 2 * frame_x_thickness, y - 2 * frame_y_thickness
mesh = create_frame(
x, y, w, h,
frame_x_thickness, frame_y_thickness,
atlas,
-- dummy pixel positions, replace these with ones from an actual spritesheet
sprite_x, sprite_y, sprite_w, sprite_h,
sprite_x, sprite_y, sprite_w, sprite_h,
sprite_x, sprite_y, sprite_w, sprite_h,
sprite_x, sprite_y, sprite_w, sprite_h,
sprite_x, sprite_y, sprite_w, sprite_h,
sprite_x, sprite_y, sprite_w, sprite_h,
sprite_x, sprite_y, sprite_w, sprite_h,
sprite_x, sprite_y, sprite_w, sprite_h,
nil, nil, nil, nil -- no backdrop
)
end
love.load = function()
allocate_mesh()
end
love.draw = function()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(mesh)
end
love.resize = function()
allocate_mesh()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment