Drawing and Graphics
Chapter 5: Drawing and Graphics
Understanding the Graphics System
Love2D’s graphics system is built on OpenGL and provides a simple yet powerful API for 2D rendering. Everything drawn appears on a coordinate system where (0,0) is the top-left corner, X increases to the right, and Y increases downward.
Coordinate System
function love.draw()
-- Draw coordinate reference points
love.graphics.setColor(1, 0, 0) -- Red
love.graphics.circle("fill", 0, 0, 5) -- Top-left (0,0)
love.graphics.setColor(0, 1, 0) -- Green
love.graphics.circle("fill", 800, 0, 5) -- Top-right (800,0)
love.graphics.setColor(0, 0, 1) -- Blue
love.graphics.circle("fill", 0, 600, 5) -- Bottom-left (0,600)
love.graphics.setColor(1, 1, 0) -- Yellow
love.graphics.circle("fill", 800, 600, 5) -- Bottom-right (800,600)
love.graphics.setColor(1, 1, 1)
love.graphics.print("(0,0)", 10, 10)
love.graphics.print("(800,0)", 750, 10)
love.graphics.print("(0,600)", 10, 580)
love.graphics.print("(800,600)", 730, 580)
end
Basic Shapes
Drawing Circles
function love.draw()
-- Filled circle
love.graphics.setColor(1, 0, 0) -- Red
love.graphics.circle("fill", 150, 150, 50)
-- Circle outline
love.graphics.setColor(0, 1, 0) -- Green
love.graphics.circle("line", 300, 150, 50)
-- Circle with custom line width
love.graphics.setColor(0, 0, 1) -- Blue
love.graphics.setLineWidth(5)
love.graphics.circle("line", 450, 150, 50)
love.graphics.setLineWidth(1) -- Reset line width
-- Semi-circles and arcs
love.graphics.setColor(1, 0, 1) -- Magenta
love.graphics.arc("fill", 150, 300, 50, 0, math.pi) -- Half circle
love.graphics.setColor(1, 1, 0) -- Yellow
love.graphics.arc("line", 300, 300, 50, 0, math.pi * 1.5) -- 3/4 circle
end
Drawing Rectangles
function love.draw()
-- Filled rectangle
love.graphics.setColor(0.8, 0.3, 0.3)
love.graphics.rectangle("fill", 50, 50, 100, 60)
-- Rectangle outline
love.graphics.setColor(0.3, 0.8, 0.3)
love.graphics.rectangle("line", 200, 50, 100, 60)
-- Rounded rectangle
love.graphics.setColor(0.3, 0.3, 0.8)
love.graphics.rectangle("fill", 350, 50, 100, 60, 10, 10)
-- Rectangle with different corner radii
love.graphics.setColor(0.8, 0.8, 0.3)
love.graphics.rectangle("fill", 500, 50, 100, 60, 15, 5)
end
Drawing Lines and Polygons
function love.draw()
-- Simple line
love.graphics.setColor(1, 1, 1)
love.graphics.line(50, 200, 200, 250)
-- Connected lines
love.graphics.setColor(1, 0, 0)
love.graphics.setLineWidth(3)
love.graphics.line(250, 200, 350, 220, 400, 180, 450, 240)
-- Triangle
love.graphics.setColor(0, 1, 1)
love.graphics.polygon("fill", 100, 350, 150, 300, 200, 350)
-- Complex polygon
love.graphics.setColor(1, 0.5, 0)
love.graphics.polygon("line",
300, 350, -- Point 1
350, 320, -- Point 2
400, 330, -- Point 3
420, 380, -- Point 4
380, 400, -- Point 5
320, 390 -- Point 6
)
love.graphics.setLineWidth(1) -- Reset
end
Colors and Transparency
Color Management
function love.draw()
-- RGB colors (values from 0 to 1)
love.graphics.setColor(1, 0, 0) -- Pure red
love.graphics.rectangle("fill", 50, 50, 50, 50)
love.graphics.setColor(0, 1, 0) -- Pure green
love.graphics.rectangle("fill", 120, 50, 50, 50)
love.graphics.setColor(0, 0, 1) -- Pure blue
love.graphics.rectangle("fill", 190, 50, 50, 50)
-- RGBA colors (with alpha/transparency)
love.graphics.setColor(1, 0, 0, 0.5) -- Semi-transparent red
love.graphics.rectangle("fill", 50, 120, 50, 50)
love.graphics.setColor(0, 1, 0, 0.3) -- More transparent green
love.graphics.rectangle("fill", 120, 120, 50, 50)
-- Color mixing through transparency
love.graphics.setColor(1, 1, 0, 0.7) -- Semi-transparent yellow
love.graphics.rectangle("fill", 85, 85, 100, 100)
-- Reset to opaque white
love.graphics.setColor(1, 1, 1, 1)
end
Color Palettes and Themes
local colorPalette = {
background = {0.1, 0.1, 0.2},
primary = {0.2, 0.7, 0.9},
secondary = {0.9, 0.4, 0.2},
accent = {0.9, 0.9, 0.3},
text = {0.9, 0.9, 0.9},
danger = {0.9, 0.2, 0.2}
}
function setColor(colorName, alpha)
local color = colorPalette[colorName]
if color then
love.graphics.setColor(color[1], color[2], color[3], alpha or 1)
else
love.graphics.setColor(1, 1, 1, alpha or 1) -- Default to white
end
end
function love.draw()
-- Set background
setColor("background")
love.graphics.rectangle("fill", 0, 0, love.graphics.getWidth(), love.graphics.getHeight())
-- Draw UI elements with consistent colors
setColor("primary")
love.graphics.rectangle("fill", 50, 50, 200, 40)
setColor("text")
love.graphics.print("Primary Button", 60, 60)
setColor("secondary")
love.graphics.rectangle("fill", 50, 110, 200, 40)
setColor("text")
love.graphics.print("Secondary Button", 60, 120)
setColor("danger", 0.8) -- Semi-transparent danger color
love.graphics.rectangle("fill", 50, 170, 200, 40)
setColor("text")
love.graphics.print("Danger Zone", 60, 180)
end
Working with Images
Loading and Drawing Images
local images = {}
function love.load()
-- Load images (make sure these files exist in your project)
images.player = love.graphics.newImage("assets/images/player.png")
images.background = love.graphics.newImage("assets/images/background.png")
images.coin = love.graphics.newImage("assets/images/coin.png")
-- Set image filter for pixel art (optional)
love.graphics.setDefaultFilter("nearest", "nearest")
end
function love.draw()
-- Draw background (stretched to fit screen)
love.graphics.draw(images.background, 0, 0, 0,
love.graphics.getWidth() / images.background:getWidth(),
love.graphics.getHeight() / images.background:getHeight())
-- Draw player at original size
love.graphics.draw(images.player, 100, 100)
-- Draw player scaled 2x
love.graphics.draw(images.player, 200, 100, 0, 2, 2)
-- Draw player rotated and scaled
local rotation = love.timer.getTime() -- Rotate based on time
love.graphics.draw(images.player, 350, 150, rotation, 1.5, 1.5,
images.player:getWidth()/2, images.player:getHeight()/2) -- Center origin
-- Draw coins in a row with transparency
love.graphics.setColor(1, 1, 1, 0.8)
for i = 1, 5 do
love.graphics.draw(images.coin, i * 60, 300)
end
love.graphics.setColor(1, 1, 1, 1) -- Reset alpha
end
Image Manipulation
local playerImage
local playerQuad
function love.load()
-- Load sprite sheet
playerImage = love.graphics.newImage("assets/images/player_spritesheet.png")
-- Create a quad to display part of the image
-- Quad parameters: x, y, width, height, image_width, image_height
playerQuad = love.graphics.newQuad(0, 0, 32, 32,
playerImage:getWidth(), playerImage:getHeight())
end
function love.update(dt)
-- Animate through sprite sheet frames
local frameTime = 0.2
local currentTime = love.timer.getTime()
local frame = math.floor(currentTime / frameTime) % 4 -- 4 frames
playerQuad:setViewport(frame * 32, 0, 32, 32)
end
function love.draw()
-- Draw the current frame
love.graphics.draw(playerImage, playerQuad, 400, 300, 0, 3, 3)
-- Draw image information
love.graphics.print("Image size: " .. playerImage:getWidth() .. "x" .. playerImage:getHeight(), 10, 10)
love.graphics.print("Quad animating through sprite frames", 10, 30)
end
Text Rendering
Basic Text
function love.draw()
-- Default font
love.graphics.print("Default font text", 10, 10)
-- Text with different sizes using default font
love.graphics.print("Normal size", 10, 40)
love.graphics.print("Large text", 10, 70, 0, 2, 2) -- 2x scale
love.graphics.print("Rotated text", 200, 100, math.pi/4) -- 45 degrees
-- Colored text
love.graphics.setColor(1, 0, 0)
love.graphics.print("Red text", 10, 120)
love.graphics.setColor(0, 1, 0)
love.graphics.print("Green text", 10, 140)
love.graphics.setColor(1, 1, 1) -- Reset to white
end
Custom Fonts
local fonts = {}
function love.load()
-- Load custom fonts
fonts.small = love.graphics.newFont(12)
fonts.medium = love.graphics.newFont(18)
fonts.large = love.graphics.newFont(32)
-- Load font from file (if you have a .ttf file)
-- fonts.custom = love.graphics.newFont("assets/fonts/custom.ttf", 24)
end
function love.draw()
-- Small font
love.graphics.setFont(fonts.small)
love.graphics.print("Small font (12px)", 10, 10)
-- Medium font
love.graphics.setFont(fonts.medium)
love.graphics.print("Medium font (18px)", 10, 40)
-- Large font
love.graphics.setFont(fonts.large)
love.graphics.print("Large font (32px)", 10, 80)
-- Text with word wrapping
love.graphics.setFont(fonts.medium)
local text = "This is a long line of text that will be wrapped to fit within the specified width."
love.graphics.printf(text, 10, 150, 300, "left") -- Wrap at 300 pixels
-- Centered text
love.graphics.printf("Centered Text", 0, 300, love.graphics.getWidth(), "center")
-- Right-aligned text
love.graphics.printf("Right-aligned", 0, 330, love.graphics.getWidth() - 20, "right")
end
Text Effects
local time = 0
function love.update(dt)
time = time + dt
end
function love.draw()
-- Rainbow text
local text = "RAINBOW TEXT"
for i = 1, string.len(text) do
local char = string.sub(text, i, i)
local hue = (time + i * 0.1) % 1
local r, g, b = hslToRgb(hue, 1, 0.5)
love.graphics.setColor(r, g, b)
love.graphics.print(char, 50 + i * 20, 50)
end
-- Waving text
love.graphics.setColor(1, 1, 1)
local waveText = "WAVE EFFECT"
for i = 1, string.len(waveText) do
local char = string.sub(waveText, i, i)
local wave = math.sin(time * 3 + i * 0.5) * 10
love.graphics.print(char, 50 + i * 20, 150 + wave)
end
-- Pulsing text
local pulse = math.abs(math.sin(time * 2))
local scale = 1 + pulse * 0.5
love.graphics.setColor(1, pulse, pulse)
love.graphics.print("PULSING TEXT", 200, 250, 0, scale, scale)
love.graphics.setColor(1, 1, 1) -- Reset
end
-- Helper function to convert HSL to RGB
function hslToRgb(h, s, l)
local r, g, b
if s == 0 then
r, g, b = l, l, l
else
local function hue2rgb(p, q, t)
if t < 0 then t = t + 1 end
if t > 1 then t = t - 1 end
if t < 1/6 then return p + (q - p) * 6 * t end
if t < 1/2 then return q end
if t < 2/3 then return p + (q - p) * (2/3 - t) * 6 end
return p
end
local q = l < 0.5 and l * (1 + s) or l + s - l * s
local p = 2 * l - q
r = hue2rgb(p, q, h + 1/3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1/3)
end
return r, g, b
end
Graphics State Management
Transformations
function love.draw()
-- Save the current transformation
love.graphics.push()
-- Translate (move the origin)
love.graphics.translate(200, 200)
love.graphics.setColor(1, 0, 0)
love.graphics.rectangle("fill", 0, 0, 50, 50) -- Drawn at (200, 200)
-- Rotate around the new origin
love.graphics.rotate(math.pi / 4) -- 45 degrees
love.graphics.setColor(0, 1, 0)
love.graphics.rectangle("fill", 60, 0, 50, 50) -- Green rotated rectangle
-- Scale
love.graphics.scale(1.5, 1.5)
love.graphics.setColor(0, 0, 1)
love.graphics.rectangle("fill", 80, 0, 30, 30) -- Blue scaled rectangle
-- Restore the previous transformation
love.graphics.pop()
-- This rectangle is drawn in the original coordinate system
love.graphics.setColor(1, 1, 0)
love.graphics.rectangle("fill", 400, 100, 50, 50)
-- Multiple transformations example
love.graphics.push()
love.graphics.translate(400, 300)
love.graphics.rotate(love.timer.getTime()) -- Rotate based on time
love.graphics.setColor(1, 0, 1)
love.graphics.rectangle("fill", -25, -25, 50, 50) -- Centered on origin
love.graphics.pop()
end
Blend Modes
function love.draw()
-- Default blend mode (alpha)
love.graphics.setColor(1, 0, 0, 0.7)
love.graphics.circle("fill", 150, 150, 60)
love.graphics.setColor(0, 1, 0, 0.7)
love.graphics.circle("fill", 200, 150, 60)
-- Additive blending (colors add together)
love.graphics.setBlendMode("add")
love.graphics.setColor(0, 0, 1, 0.7)
love.graphics.circle("fill", 175, 200, 60)
love.graphics.setColor(1, 1, 0, 0.7)
love.graphics.circle("fill", 225, 200, 60)
-- Multiply blending (colors multiply together)
love.graphics.setBlendMode("multiply")
love.graphics.setColor(1, 0.5, 0.5)
love.graphics.circle("fill", 350, 150, 80)
love.graphics.setColor(0.5, 1, 0.5)
love.graphics.circle("fill", 400, 150, 80)
-- Screen blending
love.graphics.setBlendMode("screen")
love.graphics.setColor(0.8, 0.2, 0.8)
love.graphics.circle("fill", 500, 150, 60)
love.graphics.setColor(0.8, 0.8, 0.2)
love.graphics.circle("fill", 550, 150, 60)
-- Reset to default
love.graphics.setBlendMode("alpha")
love.graphics.setColor(1, 1, 1)
-- Labels
love.graphics.print("Alpha", 120, 250)
love.graphics.print("Add", 350, 250)
love.graphics.print("Multiply", 320, 100)
love.graphics.print("Screen", 500, 250)
end
Drawing Optimization
Batching Draw Calls
local sprites = {}
local particles = {}
function love.load()
sprites.particle = love.graphics.newImage("assets/images/particle.png")
-- Create many particles
for i = 1, 1000 do
table.insert(particles, {
x = math.random(0, 800),
y = math.random(0, 600),
vx = math.random(-50, 50),
vy = math.random(-50, 50),
life = math.random(1, 3),
maxLife = math.random(1, 3)
})
end
end
function love.update(dt)
for _, particle in ipairs(particles) do
particle.x = particle.x + particle.vx * dt
particle.y = particle.y + particle.vy * dt
particle.life = particle.life - dt
if particle.life <= 0 then
particle.life = particle.maxLife
particle.x = math.random(0, 800)
particle.y = math.random(0, 600)
end
end
end
function love.draw()
-- Batch similar draw operations
love.graphics.setColor(1, 1, 1, 1)
-- Draw all particles with the same sprite in one go
for _, particle in ipairs(particles) do
local alpha = particle.life / particle.maxLife
love.graphics.setColor(1, 1, 1, alpha)
love.graphics.draw(sprites.particle, particle.x, particle.y)
end
love.graphics.setColor(1, 1, 1, 1)
love.graphics.print("Particles: " .. #particles, 10, 10)
love.graphics.print("FPS: " .. love.timer.getFPS(), 10, 30)
end
Using SpriteBatch for Performance
local spriteBatch
local particleImage
function love.load()
particleImage = love.graphics.newImage("assets/images/particle.png")
-- Create a SpriteBatch for efficient drawing of many instances
spriteBatch = love.graphics.newSpriteBatch(particleImage, 1000)
end
function love.update(dt)
-- Clear the sprite batch
spriteBatch:clear()
-- Add sprites to the batch
for i = 1, 500 do
local x = 400 + math.cos(love.timer.getTime() + i * 0.1) * (100 + i * 0.5)
local y = 300 + math.sin(love.timer.getTime() + i * 0.1) * (100 + i * 0.5)
local rotation = love.timer.getTime() + i * 0.2
local scale = 0.5 + math.sin(love.timer.getTime() * 2 + i * 0.05) * 0.3
spriteBatch:add(x, y, rotation, scale, scale)
end
end
function love.draw()
-- Draw entire batch in one call
love.graphics.draw(spriteBatch)
love.graphics.print("Drawing 500 sprites efficiently with SpriteBatch", 10, 10)
love.graphics.print("FPS: " .. love.timer.getFPS(), 10, 30)
end
Advanced Graphics Techniques
Creating Procedural Graphics
function love.draw()
-- Procedural star field
love.graphics.setColor(1, 1, 1)
math.randomseed(42) -- Fixed seed for consistent stars
for i = 1, 200 do
local x = math.random(0, 800)
local y = math.random(0, 600)
local brightness = math.random(0.3, 1)
love.graphics.setColor(brightness, brightness, brightness)
love.graphics.circle("fill", x, y, math.random(1, 2))
end
-- Procedural grid pattern
love.graphics.setColor(0.2, 0.2, 0.3, 0.5)
for x = 0, 800, 40 do
love.graphics.line(x, 0, x, 600)
end
for y = 0, 600, 40 do
love.graphics.line(0, y, 800, y)
end
-- Procedural wave
love.graphics.setColor(0, 0.8, 1)
love.graphics.setLineWidth(3)
local points = {}
for x = 0, 800, 5 do
local y = 300 + math.sin(x * 0.02 + love.timer.getTime() * 2) * 50
table.insert(points, x)
table.insert(points, y)
end
love.graphics.line(points)
love.graphics.setLineWidth(1)
love.graphics.setColor(1, 1, 1)
end
Drawing Performance Tips
-- Tips demonstrated in code:
function love.draw()
-- 1. Minimize color changes
love.graphics.setColor(1, 0, 0)
-- Draw all red objects here
love.graphics.rectangle("fill", 50, 50, 50, 50)
love.graphics.circle("fill", 150, 75, 25)
-- 2. Batch similar operations
love.graphics.setColor(0, 1, 0)
-- Draw all green objects here
love.graphics.rectangle("fill", 250, 50, 50, 50)
love.graphics.circle("fill", 350, 75, 25)
-- 3. Use appropriate drawing functions
-- For filled shapes, use "fill" mode
-- For outlines, use "line" mode
-- 4. Avoid unnecessary transformations
-- Group transformed objects together
love.graphics.push()
love.graphics.translate(400, 300)
love.graphics.rotate(love.timer.getTime())
love.graphics.setColor(1, 1, 0)
love.graphics.rectangle("fill", -25, -25, 50, 50)
love.graphics.circle("fill", 0, -50, 15)
love.graphics.pop()
love.graphics.setColor(1, 1, 1)
end
-- Performance monitoring
function love.update(dt)
-- Monitor frame time
if dt > 1/30 then -- If frame took longer than ~33ms
print("Frame lag detected: " .. dt .. "s")
end
end
Common Graphics Pitfalls
Avoiding Common Mistakes
function love.draw()
-- MISTAKE: Not resetting graphics state
-- love.graphics.setColor(1, 0, 0)
-- love.graphics.rectangle("fill", 50, 50, 50, 50)
-- -- Text will be red too!
-- love.graphics.print("This text is red!", 10, 10)
-- CORRECT: Always reset state
love.graphics.setColor(1, 0, 0)
love.graphics.rectangle("fill", 50, 50, 50, 50)
love.graphics.setColor(1, 1, 1) -- Reset to white
love.graphics.print("This text is white", 10, 10)
-- MISTAKE: Forgetting coordinate system
-- Drawing at (0, 0) might be off-screen or hard to see
-- CORRECT: Consider your coordinate system
local centerX = love.graphics.getWidth() / 2
local centerY = love.graphics.getHeight() / 2
love.graphics.circle("fill", centerX, centerY, 30)
-- MISTAKE: Not handling window resize
-- Hard-coded positions break when window size changes
-- CORRECT: Use relative positioning
local screenWidth = love.graphics.getWidth()
local screenHeight = love.graphics.getHeight()
love.graphics.rectangle("fill", screenWidth - 60, 10, 50, 30) -- Top-right corner
end
Next Steps
You now have a solid foundation in Love2D’s graphics system. In the next chapter, we’ll explore input handling in detail, including keyboard, mouse, and gamepad input, along with creating responsive controls for your games.
Chapter Summary:
- Love2D uses a coordinate system with (0,0) at top-left
- Basic shapes include circles, rectangles, lines, and polygons
- Colors use RGB values from 0 to 1, with optional alpha channel
- Images can be loaded, scaled, rotated, and animated
- Text rendering supports custom fonts and formatting
- Graphics transformations allow complex positioning and effects
- Blend modes create interesting visual effects
- Performance optimization techniques improve frame rates
- SpriteBatch enables efficient drawing of many similar objects
- Proper graphics state management prevents common issues