Screen recording of the program:
The Lorenz system is a set of three differential equations:
dx/dt = σ (y − x)
dy/dt = x (ρ − z) − y
dz/dt = x y − β z
With the classic parameters σ = 10, ρ = 28, β = 8/3, the system becomes chaotic. Small differences in initial conditions lead to wildly different trajectories.
Create a folder for your LÖVE project:
lorenz/
main.lua
Then run it with:
love lorenz
We start by defining the Lorenz equations and an RK4 integrator. RK4 gives smooth, stable motion.
-- Lorenz equations
local function lorenz(x, y, z, sigma, rho, beta)
local dx = sigma * (y - x)
local dy = x * (rho - z) - y
local dz = x * y - beta * z
return dx, dy, dz
end
-- RK4 integration step
local function rk4_step(x, y, z, dt, sigma, rho, beta)
local k1x, k1y, k1z = lorenz(x, y, z, sigma, rho, beta)
local x2 = x + 0.5 * dt * k1x
local y2 = y + 0.5 * dt * k1y
local z2 = z + 0.5 * dt * k1z
local k2x, k2y, k2z = lorenz(x2, y2, z2, sigma, rho, beta)
local x3 = x + 0.5 * dt * k2x
local y3 = y + 0.5 * dt * k2y
local z3 = z + 0.5 * dt * k2z
local k3x, k3y, k3z = lorenz(x3, y3, z3, sigma, rho, beta)
local x4 = x + dt * k3x
local y4 = y + dt * k3y
local z4 = z + dt * k3z
local k4x, k4y, k4z = lorenz(x4, y4, z4, sigma, rho, beta)
local nx = x + (dt/6) * (k1x + 2*k2x + 2*k3x + k4x)
local ny = y + (dt/6) * (k1y + 2*k2y + 2*k3y + k4y)
local nz = z + (dt/6) * (k1z + 2*k2z + 2*k3z + k4z)
return nx, ny, nz
end
We keep parameters, the current point, the trail, and camera controls in a single table:
local state = {
sigma = 10, rho = 28, beta = 8/3,
dt = 0.01,
x = 0.1, y = 0, z = 0,
trail = {},
maxTrail = 2000,
zoom = 8,
rotX = 0,
rotY = 0,
}
Each frame, we integrate several small steps for smoothness:
function love.update(dt)
for i = 1, 4 do
state.x, state.y, state.z =
rk4_step(state.x, state.y, state.z,
state.dt, state.sigma, state.rho, state.beta)
table.insert(state.trail, 1, {x=state.x, y=state.y, z=state.z})
if #state.trail > state.maxTrail then
state.trail[#state.trail] = nil
end
end
end
We rotate the 3D point around X and Y, then drop the Z coordinate:
local function project(px, py, pz)
local cx, sx = math.cos(state.rotX), math.sin(state.rotX)
local cy, sy = math.cos(state.rotY), math.sin(state.rotY)
local ry = py * cx - pz * sx
local rz = py * sx + pz * cx
local rx = px * cy + rz * sy
return rx * state.zoom, ry * state.zoom
end
We draw line segments between consecutive points:
function love.draw()
local w, h = love.graphics.getDimensions()
love.graphics.push()
love.graphics.translate(w/2, h/2)
for i = 1, #state.trail - 1 do
local p1 = state.trail[i]
local p2 = state.trail[i+1]
local x1, y1 = project(p1.x, p1.y, p1.z)
local x2, y2 = project(p2.x, p2.y, p2.z)
love.graphics.setColor(0.2 + i/#state.trail, 0.6, 1 - i/#state.trail)
love.graphics.line(x1, y1, x2, y2)
end
love.graphics.pop()
end
function love.mousemoved(x, y, dx, dy)
if love.mouse.isDown(1) then
state.rotY = state.rotY + dx * 0.005
state.rotX = state.rotX + dy * 0.005
end
end
function love.wheelmoved(_, y)
if y > 0 then state.zoom = state.zoom * 1.1
else state.zoom = state.zoom / 1.1 end
end
function love.keypressed(key)
if key == "up" then state.dt = state.dt * 1.2 end
if key == "down" then state.dt = state.dt / 1.2 end
if key == "r" then state.trail = {} end
end
main.luaHere is the complete program assembled into one file:
-- Interactive Lorenz attractor for LÖVE2D (0.10+)
-- Drop into a folder and run `love .`
local state = {
-- Lorenz parameters (classic chaotic values)
sigma = 10.0,
rho = 28.0,
beta = 8.0/3.0,
-- integration
dt = 0.01,
paused = false,
-- point and trail
x = 0.1, y = 0.0, z = 0.0,
trail = {},
maxTrail = 2000,
drawTrails = true,
-- view transform
zoom = 8.0,
rotX = 0.0,
rotY = 0.0,
dragging = false,
lastMouseX = 0,
lastMouseY = 0,
-- color
hue = 200,
-- performance
stepsPerFrame = 4
}
local function lorenz(x, y, z, sigma, rho, beta)
local dx = sigma * (y - x)
local dy = x * (rho - z) - y
local dz = x * y - beta * z
return dx, dy, dz
end
-- RK4 integrator for system
local function rk4_step(x, y, z, dt, sigma, rho, beta)
local k1x, k1y, k1z = lorenz(x, y, z, sigma, rho, beta)
local x2 = x + 0.5 * dt * k1x
local y2 = y + 0.5 * dt * k1y
local z2 = z + 0.5 * dt * k1z
local k2x, k2y, k2z = lorenz(x2, y2, z2, sigma, rho, beta)
local x3 = x + 0.5 * dt * k2x
local y3 = y + 0.5 * dt * k2y
local z3 = z + 0.5 * dt * k2z
local k3x, k3y, k3z = lorenz(x3, y3, z3, sigma, rho, beta)
local x4 = x + dt * k3x
local y4 = y + dt * k3y
local z4 = z + dt * k3z
local k4x, k4y, k4z = lorenz(x4, y4, z4, sigma, rho, beta)
local nx = x + (dt/6) * (k1x + 2*k2x + 2*k3x + k4x)
local ny = y + (dt/6) * (k1y + 2*k2y + 2*k3y + k4y)
local nz = z + (dt/6) * (k1z + 2*k2z + 2*k3z + k4z)
return nx, ny, nz
end
local function pushTrail(s)
table.insert(state.trail, 1, {x = s.x, y = s.y, z = s.z})
if #state.trail > state.maxTrail then
for i = state.maxTrail + 1, #state.trail do state.trail[i] = nil end
end
end
local function resetState()
state.x = 0.1 + (math.random() - 0.5) * 0.2
state.y = 0.0 + (math.random() - 0.5) * 0.2
state.z = 0.0 + (math.random() - 0.5) * 0.2
state.trail = {}
end
function love.load()
love.window.setTitle("Lorenz Attractor — Interactive (LÖVE)")
love.graphics.setBackgroundColor(0.02, 0.02, 0.02)
math.randomseed(os.time())
resetState()
love.graphics.setLineWidth(1)
end
function love.update(dt)
-- allow multiple small steps per frame for stability
if not state.paused then
local steps = state.stepsPerFrame
local stepDt = state.dt
for i = 1, steps do
state.x, state.y, state.z = rk4_step(state.x, state.y, state.z, stepDt, state.sigma, state.rho, state.beta)
pushTrail(state)
end
end
end
-- simple 3D -> 2D projection with rotation
local function project(px, py, pz)
-- rotate around X then Y
local cx = math.cos(state.rotX)
local sx = math.sin(state.rotX)
local cy = math.cos(state.rotY)
local sy = math.sin(state.rotY)
-- rotate X
local ry = py * cx - pz * sx
local rz = py * sx + pz * cx
-- rotate Y
local rx = px * cy + rz * sy
local rz2 = -px * sy + rz * cy
-- perspective-ish scale (optional)
local scale = state.zoom
local sx2 = rx * scale
local sy2 = ry * scale
return sx2, sy2, rz2
end
function love.draw()
local w, h = love.graphics.getDimensions()
love.graphics.push()
love.graphics.translate(w/2, h/2)
-- draw trail
if state.drawTrails and #state.trail > 1 then
for i = 1, #state.trail - 1 do
local a = 1.0 - (i / #state.trail) -- fade
local p1 = state.trail[i]
local p2 = state.trail[i+1]
local x1, y1 = project(p1.x, p1.y, p1.z)
local x2, y2 = project(p2.x, p2.y, p2.z)
love.graphics.setColor(0.2 + 0.8 * (i/#state.trail), 0.6, 1.0 - 0.6*(i/#state.trail), a)
love.graphics.line(x1, y1, x2, y2)
end
end
-- draw current point
local cx, cy = project(state.x, state.y, state.z)
love.graphics.setColor(1, 0.9, 0.2)
love.graphics.circle("fill", cx, cy, 3)
love.graphics.pop()
-- HUD
love.graphics.setColor(1,1,1,0.9)
love.graphics.print(string.format("sigma: %.3f rho: %.3f beta: %.4f", state.sigma, state.rho, state.beta), 10, 10)
love.graphics.print(string.format("dt: %.4f steps/frame: %d trail: %d paused: %s", state.dt, state.stepsPerFrame, state.maxTrail, tostring(state.paused)), 10, 30)
love.graphics.print("Controls: Space pause | R reset | T toggle trails | Arrows dt/trail | W/S/A/D tweak rho/sigma/beta", 10, 50)
love.graphics.print("Mouse drag to rotate, scroll to zoom", 10, 70)
love.graphics.setColor(1,1,1,0.6)
love.graphics.print("FPS: "..tostring(love.timer.getFPS()), 10, love.graphics.getHeight() - 20)
end
function love.keypressed(key)
if key == "space" then
state.paused = not state.paused
elseif key == "r" then
resetState()
elseif key == "t" then
state.drawTrails = not state.drawTrails
elseif key == "up" then
state.dt = math.min(0.1, state.dt * 1.2)
elseif key == "down" then
state.dt = math.max(1e-5, state.dt / 1.2)
elseif key == "right" then
state.maxTrail = math.min(20000, state.maxTrail + 200)
elseif key == "left" then
state.maxTrail = math.max(10, state.maxTrail - 200)
elseif key == "w" then
state.rho = state.rho + 0.5
elseif key == "s" then
state.rho = math.max(0, state.rho - 0.5)
elseif key == "a" then
state.sigma = math.max(0, state.sigma - 0.5)
elseif key == "d" then
state.sigma = state.sigma + 0.5
elseif key == "q" then
state.beta = math.max(0, state.beta - 0.05)
elseif key == "e" then
state.beta = state.beta + 0.05
end
end
function love.mousepressed(x, y, button)
if button == 1 then
state.dragging = true
state.lastMouseX = x
state.lastMouseY = y
end
end
function love.mousereleased(x, y, button)
if button == 1 then
state.dragging = false
end
end
function love.mousemoved(x, y, dx, dy)
if state.dragging then
state.rotY = state.rotY + dx * 0.005
state.rotX = state.rotX + dy * 0.005
end
end
function love.wheelmoved(x, y)
if y > 0 then
state.zoom = state.zoom * 1.1
elseif y < 0 then
state.zoom = state.zoom / 1.1
end
end