SNKRX/engine/graphics/graphics.lua

393 lines
16 KiB
Lua

graphics = {}
graphics.debug_queries = {}
graphics.debug_draw = false
-- All operations after this is called will be affected by the transform.
function graphics.push(x, y, r, sx, sy)
love.graphics.push()
love.graphics.translate(x or 0, y or 0)
love.graphics.scale(sx or 1, sy or sx or 1)
love.graphics.rotate(r or 0)
love.graphics.translate(-x or 0, -y or 0)
end
-- All operations after this is called will not be affected by the transform set with graphics.push.
function graphics.pop()
love.graphics.pop()
end
function graphics.translate(x, y)
love.graphics.translate(x or 0, y or 0)
end
function graphics.rotate(r)
love.graphics.rotate(r or 0)
end
function graphics.scale(sx, sy)
love.graphics.scale(sx or 1, sy or sx or 1)
end
function graphics.update(dt)
for i = #self.debug_queries, 1, -1 do
local query = self.debug_queries[i]
query.frames = query.frames - 1
if query.frames <= 0 then table.remove(self.debug_queries, i) end
end
end
-- Adds a polygon to be drawn to the screen for a few frames. Useful when debugging visual shapes.
function graphics.add_polygon_debug_query(vertices, frames)
table.insert(self.debug_queries, {vertices = vertices, frames = frames, type = "polygon"})
end
function graphics.draw_debug_queries()
for _, query in ipairs(self.debug_queries) do
if query.type == "polygon" then
self:polygon(query.vertices)
end
end
end
-- Prints text to the screen, alternative to using a Text object.
function graphics.print(text, font, x, y, r, sx, sy, ox, oy, color)
local _r, g, b, a = love.graphics.getColor()
if color then love.graphics.setColor(color.r, color.g, color.b, color.a) end
love.graphics.print(text, font.font, x, y, r or 0, sx or 1, sy or 1, ox or 0, oy or 0)
if color then love.graphics.setColor(_r, g, b, a) end
end
-- Prints text to the screen centered on x, y, alternative to using a Text object.
function graphics.print_centered(text, font, x, y, r, sx, sy, ox, oy, color)
local _r, g, b, a = love.graphics.getColor()
if color then love.graphics.setColor(color.r, color.g, color.b, color.a) end
love.graphics.print(text, font.font, x, y, r or 0, sx or 1, sy or 1, (ox or 0) + font:get_text_width(text)/2, (oy or 0) + font.h/2)
if color then love.graphics.setColor(_r, g, b, a) end
end
function graphics.shape(shape, color, line_width, ...)
local r, g, b, a = love.graphics.getColor()
if not color and not line_width then love.graphics[shape]("line", ...)
elseif color and not line_width then
love.graphics.setColor(color.r, color.g, color.b, color.a)
love.graphics[shape]("fill", ...)
else
if color then love.graphics.setColor(color.r, color.g, color.b, color.a) end
love.graphics.setLineWidth(line_width)
love.graphics[shape]("line", ...)
love.graphics.setLineWidth(1)
end
love.graphics.setColor(r, g, b, a)
end
-- Draws a rectangle of size w, h centered on x, y.
-- If rx, ry are passed in, then the rectangle will have rounded corners with radius of that size.
-- If color is passed in then the rectangle will be filled with that color (color is Color object)
-- If line_width is passed in then the rectangle will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.rectangle(x, y, w, h, rx, ry, color, line_width)
graphics.shape("rectangle", color, line_width, x - w/2, y - h/2, w, h, rx, ry)
end
-- Draws a rectangle of size w, h centered on x - w/2, y - h/2.
-- If rx, ry are passed in, then the rectangle will have rounded corners with radius of that size.
-- If color is passed in then the rectangle will be filled with that color (color is Color object)
-- If line_width is passed in then the rectangle will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.rectangle2(x, y, w, h, rx, ry, color, line_width)
graphics.shape("rectangle", color, line_width, x, y, w, h, rx, ry)
end
-- Draws a dashed rectangle of size w, h centerd on x, y.
-- dash_size and gap_size correspond to the dimensions of the dashing behavior.
function graphics.dashed_rectangle(x, y, w, h, dash_size, gap_size, color, line_width)
graphics.dashed_line(x - w/2, y - h/2, x + w/2, y - h/2, dash_size, gap_size, color, line_width)
graphics.dashed_line(x - w/2, y - h/2, x - w/2, y + h/2, dash_size, gap_size, color, line_width)
graphics.dashed_line(x - w/2, y + h/2, x + w/2, y + h/2, dash_size, gap_size, color, line_width)
graphics.dashed_line(x + w/2, y - h/2, x + w/2, y + h/2, dash_size, gap_size, color, line_width)
end
-- Draws an isosceles triangle with size w, h centered on x, y pointed to the right (angle 0).
-- If color is passed in then the triangle will be filled with that color (color is Color object)
-- If line_width is passed in then the triangle will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.triangle(x, y, w, h, color, line_width)
local x1, y1 = x + h/2, y
local x2, y2 = x - h/2, y - w/2
local x3, y3 = x - h/2, y + w/2
graphics.polygon({x1, y1, x2, y2, x3, y3}, color, line_width)
end
-- Draws an equilateral triangle with size w centered on x, y pointed to the right (angle 0).
-- If color is passed in then the triangle will be filled with that color (color is Color object)
-- If line_width is passed in then the triangle will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.triangle_equilateral(x, y, w, color, line_width)
local h = math.sqrt(math.pow(w, 2) - math.pow(w/2, 2))
graphics.triangle(x, y, w, h, color, line_width)
end
-- Draws a circle of radius r centered on x, y.
-- If color is passed in then the circle will be filled with that color (color is Color object)
-- If line_width is passed in then the circle will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.circle(x, y, r, color, line_width)
graphics.shape("circle", color, line_width, x, y, r)
end
-- Draws an arc of radius r from angle r1 to angle r2 centered on x, y.
-- If color is passed in then the arc will be filled with that color (color is Color object)
-- If line_width is passed in then the arc will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.arc(arctype, x, y, r, r1, r2, color, line_width)
graphics.shape("arc", color, line_width, arctype, x, y, r, r1, r2)
end
-- Draws a polygon with the given points.
-- If color is passed in then the polygon will be filled with that color (color is Color object)
-- If line_width is passed in then the polygon will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.polygon(vertices, color, line_width)
graphics.shape("polygon", color, line_width, vertices)
end
-- Draws a line with the given points.
function graphics.line(x1, y1, x2, y2, color, line_width)
local r, g, b, a = love.graphics.getColor()
if color then love.graphics.setColor(color.r, color.g, color.b, color.a) end
if line_width then love.graphics.setLineWidth(line_width) end
love.graphics.line(x1, y1, x2, y2)
love.graphics.setColor(r, g, b, a)
love.graphics.setLineWidth(1)
end
function graphics.polyline(color, line_width, ...)
local r, g, b, a = love.graphics.getColor()
if color then love.graphics.setColor(color.r, color.g, color.b, color.a) end
if line_width then love.graphics.setLineWidth(line_width) end
love.graphics.line(...)
love.graphics.setColor(r, g, b, a)
love.graphics.setLineWidth(1)
end
-- Draws a line with rounded ends with the given points.
-- If color is passed in then the line will be filled with that color (color is Color object)
-- If line_width is passed in then the line will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.rounded_line(x1, y1, x2, y2, color, line_width)
love.graphics.push()
love.graphics.translate(x1, y1)
love.graphics.rotate(math.angle(x1, y1, x2, y2))
love.graphics.translate(-x1, -y1)
graphics.rectangle(x1, y1 - line_width/4, math.length(x2-x1, y2-y1), line_width/2, line_width/4, line_width/4, color)
love.graphics.pop()
end
-- Draws a dashed line with the given points.
-- dash_size and gap_size correspond to the dimensions of the dashing behavior.
-- If color is passed in then the lines will be filled with that color (color is Color object)
-- If line_width is passed in then the lines will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.dashed_line(x1, y1, x2, y2, dash_size, gap_size, color, line_width)
local r, g, b, a = love.graphics.getColor()
if color then love.graphics.setColor(color.r, color.g, color.b, color.a) end
if line_width then love.graphics.setLineWidth(line_width) end
local dx, dy = x2-x1, y2-y1
local an, st = math.atan2(dy, dx), dash_size + gap_size
local len = math.sqrt(dx*dx + dy*dy)
local nm = (len-dash_size)/st
love.graphics.push()
love.graphics.translate(x1, y1)
love.graphics.rotate(an)
for i = 0, nm do love.graphics.line(i*st, 0, i*st + dash_size, 0) end
love.graphics.line(nm*st, 0, nm*st + dash_size, 0)
love.graphics.pop()
end
-- Draws a dashed line with rounded ends with the given points.
-- dash_size and gap_size correspond to the dimensions of the dashing behavior.
-- If color is passed in then the lines will be filled with that color (color is Color object)
-- If line_width is passed in then the lines will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.dashed_rounded_line(x1, y1, x2, y2, dash_size, gap_size, color, line_width)
if color then love.graphics.setColor(color.r, color.g, color.b, color.a) end
if line_width then love.graphics.setLineWidth(line_width) end
local dx, dy = x2-x1, y2-y1
local an, st = math.atan2(dy, dx), dash_size + gap_size
local len = math.sqrt(dx*dx + dy*dy)
local nm = (len-dash_size)/st
love.graphics.push()
love.graphics.translate(x1, y1)
love.graphics.rotate(an)
for i = 0, nm do
love.graphics.push()
love.graphics.translate(i*st, 0)
love.graphics.rotate(math.angle(i*st, 0, i*st + dash_size, 0))
love.graphics.translate(-i*st, -0)
graphics.shape("rectangle", color, nil, i*st, 0, math.length((i*st + dash_size)-(i*st), 0-0), line_width/2, line_width/4, line_width/4)
love.graphics.pop()
end
love.graphics.push()
love.graphics.translate(nm*st, 0)
love.graphics.rotate(math.angle(nm*st, 0, nm*st + dash_size, 0))
love.graphics.translate(-nm*st, -0)
graphics.shape("rectangle", color, nil, nm*st, 0, math.length((nm*st + dash_size)-(nm*st), 0-0), line_width/2, line_width/4, line_width/4)
love.graphics.pop()
love.graphics.pop()
end
-- Draws an ellipse with radius rx, ry centered on x, y.
-- If color is passed in then the ellipse will be filled with that color (color is Color object)
-- If line_width is passed in then the ellipse will not be filled and will instead be drawn as a set of lines of the given width.
function graphics.ellipse(x, y, rx, ry, color, line_width)
graphics.shape("ellipse", color, line_width, x, y, rx, ry)
end
-- Sets the currently active shader, the passed in argument should be a Shader object.
function graphics.set_shader(shader)
if not shader then love.graphics.setShader()
else love.graphics.setShader(shader.shader) end
end
-- Sets the currently active color, the passed in argument should be a Color object.
function graphics.set_color(color)
love.graphics.setColor(color.r, color.g, color.b, color.a)
end
-- Sets the currently active background color, the passed in argument should be a Color object.
function graphics.set_background_color(color)
love.graphics.setBackgroundColor(color.r, color.g, color.b, color.a)
end
function graphics.set_line_width(line_width)
love.graphics.setLineWidth(line_width)
end
-- Sets the line style, possible values are 'rough' and 'smooth'.
function graphics.set_line_style(style)
love.graphics.setLineStyle(style)
end
-- Sets the default filter mode, possible values are 'nearest' and 'linear'.
function graphics.set_default_filter(min, max)
love.graphics.setDefaultFilter(min, max)
end
function graphics.set_mouse_visible(value)
love.mouse.setVisible(value)
end
function graphics.stencil(...)
love.graphics.stencil(...)
end
function graphics.set_stencil_test(...)
love.graphics.setStencilTest(...)
end
-- Draws the image masked by a shape, meaning that only parts inside (or outside) the shape that intersects the image are drawn.
-- By default only parts that intersect with the shape are drawn, pass the third argument as true to make it so that only parts that don't intersect are drawn.
-- action is a function that draws the image.
-- mask_action is a function that draws the shape.
function graphics.draw_with_mask(action, mask_action, invert_mask)
graphics.stencil(function() mask_action() end)
if not invert_mask then graphics.set_stencil_test('greater', 0)
else graphics.set_stencil_test('notequal', 1) end
action()
graphics.set_stencil_test()
end
local stencil_mask_shader = love.graphics.newShader[[
vec4 effect(vec4 color, Image texture, vec2 tc, vec2 pc) {
vec4 t = Texel(texture, tc);
if (t.a == 0.0) {
discard;
}
return t;
}
]]
-- Draws the second image on top of the first, but only the portions of the second image that aren't transparent are drawn.
-- This essentially applies the second image as a texture on top of the shape of the first.
-- action1 and action2 are functions that draw the images.
-- graphics.draw_intersection(function() player_image:draw(player.x, player.y) end, function() gradient_image:draw(player.x, player.y) end) -> draws the player with a gradient applied to it
function graphics.draw_intersection(action1, action2)
graphics.stencil(function() love.graphics.setShader(stencil_mask_shader); action1(); love.graphics.setShader() end, 'replace', 1)
graphics.set_stencil_test('greater', 0)
action2()
graphics.set_stencil_test()
end
-- A very specific function to be used with rtfx.bat to generated RTFX animations.
-- This is not very useful in contexts other than this very specific thing, so it should probably not be here.
-- TODO: some kind of plugin system for functions like these that are very specific and not generalizable but still useful.
function graphics.generate_rtfx_animation(w, h)
local files = system.enumerate_files("assets/frames")
if #files <= 0 then return end
local frames = {}
for _, file in ipairs(files) do table.insert(frames, love.image.newImageData(file)) end
local out_frames = {}
local idw, idh = frames[1]:getDimensions()
local tw, th = w or 4, h or 4
local ow, oh = idw/tw, idh/th
for k = 1, #frames do
local image_data = frames[k]
local out_image_data = love.image.newImageData(ow, oh)
for i = 1, image_data:getWidth(), tw do
for j = 1, image_data:getHeight(), th do
local yes_or_no = {}
for x = 0, tw-1 do
for y = 0, th-1 do
local r, g, b, a = image_data:getPixel(i+x-1, j+y-1)
-- if r >= 0.01 and g >= 0.01 and b >= 0.01 then table.insert(yes_or_no, 1) end
if a >= 0.5 then table.insert(yes_or_no, 1) end
end
end
if #yes_or_no >= (tw*th)/2 then
out_image_data:setPixel(math.floor((i-1)/tw), math.floor((j-1)/th), 1, 1, 1, 1)
end
end
end
table.insert(out_frames, out_image_data)
end
local spritesheet_data = love.image.newImageData(ow*#out_frames, oh)
for k = 1, #out_frames do
for i = 0, out_frames[k]:getWidth()-1 do
for j = 0, out_frames[k]:getHeight()-1 do
spritesheet_data:setPixel(i + (k-1)*ow, j, out_frames[k]:getPixel(i, j))
end
end
end
spritesheet_data:encode("png", "anim.png")
end