Placeholder image generator

This is nothing special it just generates placeholder images, when you need such a thing.

They’re not especially complex but they layer a few different procedural patterns on top of each other in different ways—the effect being that you can usually tell the images apart very easily, which can be helpful.

Here’s the project, usable as a dependency:
PlaceholderImage.zip (285.4 KB)

@Steppers (was not able to upload to WebRepo btw)

1 Like

Noted! Sorry, I’ve been a little busy recently. I’ll look into it when I can :+1:

1 Like

The (Claude) code:


--# Main
-- =========================================
-- Main
-- Simple placeholder image viewer with
-- "New Image" button
-- =========================================

local currentImage = nil
local buttonRect = {x=0, y=0, w=0, h=0}
local padding = 12
local buttonHeight = 50

function setup()
  -- Generate initial image
  generateNewImage()
end

function draw()
  background(40, 40, 45)
  
  -- Calculate button area
  buttonRect.x = padding
  buttonRect.y = HEIGHT - buttonHeight - padding
  buttonRect.w = 120
  buttonRect.h = buttonHeight
  
  -- Draw button
  drawButton()
  
  -- Draw current image centered
  if currentImage then
    local imgW = currentImage.width
    local imgH = currentImage.height
    local availH = HEIGHT - buttonHeight - padding * 3
    
    -- Scale to fit
    local scale = math.min(
    (WIDTH - padding * 2) / imgW,
    availH / imgH
    )
    local drawW = imgW * scale
    local drawH = imgH * scale
    local drawX = WIDTH * 0.5
    local drawY = (availH + padding) * 0.5 + padding
    
    noTint()
    sprite(currentImage, drawX, drawY, drawW, drawH)
  end
end

function touched(touch)
  if touch.state == ENDED then
    if pointInRect(touch.x, touch.y, buttonRect) then
      generateNewImage()
    end
  end
end

-- =========================================
-- Helper functions
-- =========================================

function generateNewImage()
  local w = 512
  local h = 512
  currentImage = placeholderImage(w, h)
end

function drawButton()
  -- Button background
  fill(100, 150, 200, 255)
  stroke(70, 120, 170, 255)
  strokeWidth(2)
  rect(buttonRect.x, buttonRect.y, 
  buttonRect.w, buttonRect.h)
  
  -- Button text
  fill(255, 255, 255, 255)
  noStroke()
  font("Helvetica")
  fontSize(18)
  textAlign(CENTER)
  text("New Image", 
  buttonRect.x + buttonRect.w * 0.5,
  buttonRect.y + buttonRect.h * 0.5)
end

function pointInRect(px, py, rect)
  return px >= rect.x and 
  px <= rect.x + rect.w and
  py >= rect.y and 
  py <= rect.y + rect.h
end

--# PlaceholderImage
-- PlaceholderImage.lua
-- Six procedural image generators.
-- Usage:
--   local img = PlaceholderImage.generate(w, h)
--   local img = PlaceholderImage.generate(w, h, {mode="flowfield"})
-- Modes: "flowfield", "circlepack", "mondrian", "phyllotaxis", "lissajous", "truchet"

PlaceholderImage = {}

local MODES = {
  "flowfield", "circlepack", "mondrian",
  "phyllotaxis", "lissajous", "truchet"
}

-- =========================================
-- Color utilities
-- =========================================

local function hsv(h, s, v)
  h = h % 1
  local i = math.floor(h * 6)
  local f = h * 6 - i
  local p = v * (1 - s)
  local q = v * (1 - f * s)
  local t = v * (1 - (1 - f) * s)
  local r, g, b
  i = i % 6
  if     i == 0 then r,g,b = v,t,p
  elseif i == 1 then r,g,b = q,v,p
  elseif i == 2 then r,g,b = p,v,t
  elseif i == 3 then r,g,b = p,q,v
  elseif i == 4 then r,g,b = t,p,v
  else               r,g,b = v,p,q
  end
  return color(r*255, g*255, b*255, 255)
end

local function randomHue()
  return math.random() 
end

-- =========================================
-- 1. Flow field
-- Uses noise() to set angles, draws short
-- strokes following the field.
-- =========================================

local function genFlowField(w, h)
  local hue     = randomHue()
  local bg      = hsv(hue, 0.7, 0.12)
  background(bg.r, bg.g, bg.b, 255)
  
  local nOff    = math.random() * 100
  local nScale  = 0.003 + math.random() * 0.004
  local nLines  = 500
  local nSteps  = 30
  local stepLen = math.max(w, h) * 0.018
  local alpha   = 120
  
  strokeWidth(1.2)
  noFill()
  
  for _ = 1, nLines do
    local x   = math.random() * w
    local y   = math.random() * h
    local sat = 0.5 + math.random() * 0.5
    local val = 0.6 + math.random() * 0.4
    local c   = hsv(hue + math.random() * 0.15 - 0.07, sat, val)
    stroke(c.r, c.g, c.b, alpha)
    
    for _ = 1, nSteps do
      local nx    = x * nScale + nOff
      local ny    = y * nScale + nOff
      local angle = noise(nx, ny) * math.pi * 4
      local x2    = x + math.cos(angle) * stepLen
      local y2    = y + math.sin(angle) * stepLen
      line(x, y, x2, y2)
      x, y = x2, y2
      if x < 0 or x > w or y < 0 or y > h then break end
    end
  end
end

-- =========================================
-- 2. Circle packing
-- Place non-overlapping circles, grow them.
-- =========================================

local function genCirclePack(w, h)
  local hue = randomHue()
  local bg  = hsv(hue + 0.5, 0.6, 0.1)
  background(bg.r, bg.g, bg.b, 255)
  
  local circles = {}
  local maxR    = math.min(w, h) * 0.18
  local minR    = math.min(w, h) * 0.02
  local tries   = 800
  
  local function overlaps(cx, cy, cr)
    for _, c in ipairs(circles) do
      local dx = cx - c.x
      local dy = cy - c.y
      if dx*dx + dy*dy < (cr + c.r + 2)^2 then
        return true
      end
    end
    return false
  end
  
  for _ = 1, tries do
    local cx = minR + math.random() * (w - minR * 2)
    local cy = minR + math.random() * (h - minR * 2)
    if not overlaps(cx, cy, minR) then
      -- grow
      local cr = minR
      while cr < maxR do
        if overlaps(cx, cy, cr + 1) then break end
        cr = cr + 1
      end
      table.insert(circles, {x=cx, y=cy, r=cr})
    end
  end
  
  noStroke()
  for _, c in ipairs(circles) do
    local t   = c.r / maxR
    local col = hsv(hue + t * 0.3, 0.7, 0.4 + t * 0.5)
    fill(col.r, col.g, col.b, 210)
    ellipse(c.x, c.y, c.r * 2)
    -- inner highlight
    fill(255, 255, 255, 30)
    ellipse(c.x + c.r * 0.2, c.y + c.r * 0.2, c.r * 0.6)
  end
end

-- =========================================
-- 3. Mondrian
-- Recursive rectangle subdivision.
-- =========================================

local function genMondrian(w, h)
  background(240, 235, 220, 255)
  
  local COLORS = {
    color(220, 50,  40),
    color(30,  80,  160),
    color(240, 200, 50),
    color(240, 235, 220),
    color(240, 235, 220),
    color(240, 235, 220),
  }
  
  local rects  = {}
  local border = 3
  
  local function split(x, y, rw, rh, depth)
    if depth == 0 or (rw < w*0.12 and rh < h*0.12) then
      table.insert(rects, {x=x, y=y, w=rw, h=rh})
      return
    end
    local horiz = rh > rw
    if rw > w * 0.5 and rh > h * 0.5 then
      horiz = math.random() < 0.5
    elseif rw > rh then
      horiz = false
    end
    
    if horiz then
      local cut = rh * (0.3 + math.random() * 0.4)
      split(x, y,      rw, cut,    depth - 1)
      split(x, y+cut,  rw, rh-cut, depth - 1)
    else
      local cut = rw * (0.3 + math.random() * 0.4)
      split(x,     y, cut,    rh, depth - 1)
      split(x+cut, y, rw-cut, rh, depth - 1)
    end
  end
  
  split(0, 0, w, h, 5)
  
  for _, r in ipairs(rects) do
    local c = COLORS[math.random(#COLORS)]
    fill(c.r, c.g, c.b, 255)
    stroke(20, 20, 20, 255)
    strokeWidth(border * 2)
    rect(r.x + border, r.y + border,
    r.w - border*2, r.h - border*2)
  end
end

-- =========================================
-- 4. Phyllotaxis
-- Sunflower spiral using golden angle.
-- =========================================

local function genPhyllotaxis(w, h)
  local hue    = randomHue()
  local bg     = hsv(hue + 0.5, 0.8, 0.08)
  background(bg.r, bg.g, bg.b, 255)
  
  local golden = math.pi * (3 - math.sqrt(5))
  local n      = 600 + math.random(300)
  local scale  = math.min(w, h) * 0.46
  local cx     = w * 0.5
  local cy     = h * 0.5
  local spread = 0.95 + math.random() * 0.3
  
  noStroke()
  for i = 1, n do
    local r     = scale * math.sqrt(i / n) * spread
    local theta = i * golden
    local x     = cx + r * math.cos(theta)
    local y     = cy + r * math.sin(theta)
    local t     = i / n
    local size  = (2 + t * 6)
    local col   = hsv(hue + t * 0.25, 0.8, 0.5 + t * 0.5)
    fill(col.r, col.g, col.b, 200)
    ellipse(x, y, size, size)
  end
end

-- =========================================
-- 5. Lissajous
-- Parametric curves: x=sin(at+d), y=sin(bt)
-- =========================================

local function genLissajous(w, h)
  local hue = randomHue()
  local bg  = hsv(hue + 0.5, 0.9, 0.07)
  background(bg.r, bg.g, bg.b, 255)
  
  -- Pick two small coprime-ish integers for a, b
  local pairs_ab = {
    {1,2},{1,3},{2,3},{3,4},{3,5},{4,5},{5,6},{2,5}
  }
  local ab    = pairs_ab[math.random(#pairs_ab)]
  local a     = ab[1]
  local b     = ab[2]
  local delta = math.random() * math.pi
  
  local nCurves = 4
  local steps   = 600
  local rx      = w * 0.42
  local ry      = h * 0.42
  local cx      = w * 0.5
  local cy      = h * 0.5
  
  noFill()
  for c = 1, nCurves do
    local d   = delta + (c - 1) * math.pi / (nCurves * 2)
    local col = hsv(hue + c / nCurves * 0.3, 0.8, 0.9)
    stroke(col.r, col.g, col.b, 180 - c * 20)
    strokeWidth(1.5)
    
    local prev_x, prev_y
    for i = 0, steps do
      local t  = i / steps * math.pi * 2
      local x  = cx + rx * math.sin(a * t + d)
      local y  = cy + ry * math.sin(b * t)
      if prev_x then line(prev_x, prev_y, x, y) end
      prev_x, prev_y = x, y
    end
  end
end

-- =========================================
-- 6. Truchet tiles
-- Grid of squares with randomly oriented arcs.
-- =========================================

local function genTruchet(w, h)
  local hue  = randomHue()
  local bg   = hsv(hue, 0.5, 0.15)
  background(bg.r, bg.g, bg.b, 255)
  
  local cols  = 10 + math.random(6)
  local rows  = cols
  local tw    = w / cols
  local th    = h / rows
  local lw    = tw * 0.18
  
  noFill()
  local fg = hsv(hue + 0.55, 0.7, 0.9)
  stroke(fg.r, fg.g, fg.b, 220)
  strokeWidth(lw)
  
  -- Each tile: two quarter-circle arcs, one of two orientations
  -- Orientation 0: arcs connect bottom-left to top-left, and top-right to bottom-right
  -- Orientation 1: arcs connect bottom-left to bottom-right, and top-left to top-right
  -- We approximate arcs with line segments
  local arcSteps = 12
  
  local function arc(ox, oy, r, startAngle, endAngle)
    local prev_x, prev_y
    for i = 0, arcSteps do
      local t     = i / arcSteps
      local angle = startAngle + t * (endAngle - startAngle)
      local x     = ox + r * math.cos(angle)
      local y     = oy + r * math.sin(angle)
      if prev_x then line(prev_x, prev_y, x, y) end
      prev_x, prev_y = x, y
    end
  end
  
  for col = 0, cols - 1 do
    for row = 0, rows - 1 do
      local x  = col * tw
      local y  = row * th
      local cx = x + tw * 0.5
      local cy = y + th * 0.5
      
      if math.random() < 0.5 then
        -- Arcs from corners: BL-TL and TR-BR
        arc(x,      y,      tw*0.5,  0,          math.pi*0.5)
        arc(x + tw, y + th, tw*0.5,  math.pi,    math.pi*1.5)
      else
        -- Arcs from corners: BL-BR and TL-TR
        arc(x,      y + th, tw*0.5, -math.pi*0.5, 0)
        arc(x + tw, y,      tw*0.5,  math.pi*0.5, math.pi)
      end
    end
  end
end

-- =========================================
-- Public API
-- =========================================

local generators = {
  flowfield  = genFlowField,
  circlepack = genCirclePack,
  mondrian   = genMondrian,
  phyllotaxis = genPhyllotaxis,
  lissajous  = genLissajous,
  truchet    = genTruchet,
}

function PlaceholderImage.generate(w, h, opts)
  opts = opts or {}
  
  -- Pick two different modes
  local shuffled = {table.unpack(MODES)}
  for i = #shuffled, 2, -1 do
    local j = math.random(i)
    shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
  end
  local modeA = opts.mode or shuffled[1]
  local modeB = shuffled[2]
  
  -- Random background
  local bgCol = hsv(math.random(), 0.4 + math.random() * 0.4, 0.1 + math.random() * 0.2)
  
  -- Generate each pattern into its own offscreen image
  local function makeLayer(modeName)
    local img = image(w, h)
    setContext(img)
    pushStyle()
    background(0, 0, 0, 0)
    generators[modeName](w, h)
    popStyle()
    setContext()
    return img
  end
  
  local imgA = makeLayer(modeA)
  local imgB = makeLayer(modeB)
  
  -- Composite onto final image
  local final = image(w, h)
  setContext(final)
  pushStyle()
  
  -- Background
  background(bgCol.r, bgCol.g, bgCol.b, 255)
  
  local function drawTransformed(img, alpha)
    local angle = math.random() * 360
    local zoom  = 0.7 + math.random() * 0.9
    pushMatrix()
    translate(w * 0.5, h * 0.5)
    rotate(angle)
    scale(zoom)
    translate(-w * 0.5, -h * 0.5)
    tint(255, 255, 255, alpha)
    sprite(img, w * 0.5, h * 0.5, w, h)
    noTint()
    popMatrix()
  end
  
  drawTransformed(imgA, 255)
  drawTransformed(imgB, 80 + math.random(140))
  
  popStyle()
  setContext()
  
  return final, modeA .. "+" .. modeB
end


function PlaceholderImage.modes()
  return MODES
end

-- =========================================
-- Simple wrapper for convenience
-- =========================================

function placeholderImage(w, h)
  local img, modes = PlaceholderImage.generate(w, h)
  return img
end
1 Like