Animated Plasma in Love2D — Tutorial with Calculation Explanations

Screen recording of the actual program:


main.lua — plasma using ImageData
-- main.lua (plasma using ImageData)
local img, texture, w, h
local t = 0

function love.load()
  w, h = 400, 300
  img = love.image.newImageData(w, h)
  texture = love.graphics.newImage(img)
end

local function plasma(x, y, time)
  local nx = (x / w) - 0.5
  local ny = (y / h) - 0.5
  local v = 0
  v = v + math.sin((nx * 10) + time)
  v = v + math.sin((ny * 10) - time * 0.8)
  v = v + math.sin((nx * 5 + ny * 5) + time * 0.6)
  v = v + math.sin(math.sqrt(nx*nx + ny*ny) * 20 - time)
  v = v / 4
  return v
end

function love.update(dt)
  t = t + dt * 1.2
  img:mapPixel(function(x, y, r, g, b, a)
    local v = plasma(x - 1, y - 1, t) -- ImageData coords start at 1
    local cr = 0.5 + 0.5 * math.cos(3 + v * math.pi + 0.0)
    local cg = 0.5 + 0.5 * math.cos(1 + v * math.pi + 2.0)
    local cb = 0.5 + 0.5 * math.cos(2 + v * math.pi + 4.0)
    return cr, cg, cb, 1
  end)
  texture:replacePixels(img)
end

function love.draw()
  local ww, wh = love.graphics.getDimensions()
  love.graphics.draw(texture, 0, 0, 0, ww / w, wh / h)
  love.graphics.setColor(1,1,1)
  love.graphics.print("plasma - size "..w.."x"..h.." FPS: "..love.timer.getFPS(), 10, 10)
end

Pixel to normalized coordinates nx ny

What — nx and ny map integer pixel indices into a -0.5..+0.5 range so center is near zero.

Why — Using a centered coordinate system simplifies using radial functions and creates symmetric patterns around the screen center.

Scaling factors in sin arguments

What — multipliers like 10, 5, and 20 control spatial frequency (how many bands or ripples fit across the screen).

Interpretation — Larger multiplier => more cycles over the same area => finer detail; smaller multiplier => broader, smoother blobs.

Combining and averaging

What — We sum several sin terms and divide by 4 to keep the result roughly in -1..1.

Why — Averaging prevents values from going beyond the expected range and makes color mapping more stable.

Color mapping using cos with offsets

What — The scalar v is fed into cos(... + offset) for each channel with different offsets to separate R G B phases.

Why — Different phase offsets cause colors to shift independently as v changes, producing colorful gradients instead of grayscale waves.

Performance note: ImageData mapPixel is CPU bound. Use low w,h while iterating on formulas. When satisfied, switch to the shader version for real-time at large sizes.

Stepwise Summary of the Math

  1. Normalize coordinates — Convert pixel coords to a centered, scale‑invariant space so math behaves the same across resolutions.
  2. Apply multiple wave functions — Use sines on different linear combinations of coordinates and on radial distance to produce varied motifs.
  3. Animate with time — Add a time-dependent phase to make waves move and evolve.
  4. Combine and normalize — Sum layers and scale to keep the value in a predictable range.
  5. Map scalar to RGB — Use cosine-based palettes or palette lookups to turn scalar values into smooth, pleasing colors.

Experiment Ideas

Next tutorial: Interactive fractal graphics zoom