Genuary 2026.08: A city

8) A city

Simple parallax graphics and (slowly) blinking windows. I enjoyed this one.


let gui;
let params = {
  layers: 3, layersMin: 1, layersMax: 5,
  stars: 120, starsMin: 0, starsMax: 1000,
  moon: true,
  moonSpeed: 0.15, moonSpeedMin: 0, moonSpeedMax: 2, moonSpeedStep: 0.01,
};


let layers = [];
let stars = [];
let scrollSpeed = 0.2;
let moon; 

function setup() {
  createCanvas(400, 400);
  noStroke();
  
  gui = createGuiPanel('params');
  gui.addObject(params);
  gui.setPosition(420, 0);
}

function draw() {
  // Update objects based on changing params
  while (stars.length > params.stars) stars.shift();
  while (stars.length < params.stars || random() < 0.01) {
    stars.push({
      x: random(width),
      y: random(height),
      r: random(1, 3)
    });
  }
  
  if (moon == undefined) {
    moon = {
      t: 0,
      phase: random(0, 1)
    };    
  }
  
  while (layers.length > params.layers) layers.pop();
  while (layers.length < params.layers) {
    let i = layers.length;
    let total = params.layers;

    let speed = map(i, 0, total - 1, 0.05, 0.25);
    let minH = map(i, 0, total - 1, 120, 200);
    let maxH = map(i, 0, total - 1, 200, 320);

    let shade = map(i, 0, total - 1, 65, 30);
    let col = color(shade - 10, shade - 5, shade);

    layers.push(createLayer(speed, col, minH, maxH));
  }
  
  drawSky();
  for (let layer of layers) {
    drawLayer(layer);
  }
}

function drawSky() {
  for (let y = 0; y < height; y++) {
    let t = map(y, 0, height, 0, 1);
    let c = lerpColor(color(10, 15, 40), color(40, 40, 70), t);
    stroke(c);
    line(0, y, width, y);
  }
  noStroke();

  fill(255, 200);
  for (let s of stars) {
    circle(s.x, s.y, s.r);
  }
  
  if (params.moon) drawMoon();
}

function drawMoon() {
  moon.t += params.moonSpeed / 100;

  // Reset arc when finished
  if (moon.t > 1) {
    moon.t = 0;
    moon.phase = (moon.phase + random(0.1, 0.25)) % 1;
  }

  // Arc path
  let startX = -50;
  let endX = width + 50;

  let x = lerp(startX, endX, moon.t);

  // Parabolic arc across the sky
  let arcHeight = height * 0.35;
  let y = height * 0.55 - sin(moon.t * PI) * arcHeight;

  // Moon body
  push();
  translate(x, y);

  noStroke();
  fill(245, 245, 230);
  circle(0, 0, 28);

  // Phase shadow
  fill(10, 15, 40);
  let phaseOffset = map(moon.phase, 0, 1, -14, 14);
  circle(phaseOffset, 0, 28);

  pop();
}

function createLayer(speed, col, minH, maxH) {
  let buildings = [];
  let x = 0;

  while (x < width * 2) {
    let w = random(30, 70);
    let h = random(minH, maxH);
    buildings.push({
      x,
      w,
      h,
      windows: generateWindows(w, h)
    });
    x += w + random(10);
  }

  return { speed, col, buildings };
}

function drawLayer(layer) {
  let baseY = height;
  fill(layer.col);

  for (let b of layer.buildings) {
    rect(b.x, baseY - b.h, b.w, b.h);

    for (let win of b.windows) {
      fill(win.on ? win.c : color(25));
      rect(
        b.x + win.x,
        baseY - win.y,
        win.w,
        win.h
      );

      if (frameCount % win.rate === 0) {
        win.on = random() > 0.4;
      }
    }

    fill(layer.col);
  }

  for (let b of layer.buildings) {
    b.x -= scrollSpeed * layer.speed * 60;
    if (b.x + b.w < 0) {
      b.x = width + random(40, 100);
    }
  }
}

function generateWindows(w, h) {
  let wins = [];
  for (let x = 6; x < w - 6; x += 10) {
    for (let y = 10; y < h - 10; y += 14) {
      wins.push({
        x,
        y,
        w: 6,
        h: 8,
        on: random() > 0.5,
        rate: int(random(180, 420)),
        c: color(255, 220, 140, 200)
      });
    }
  }
  return wins;
}