previous sketch Skyline next sketch

Click your mouse for a new image

Each postcard is unique and will never appear again in exactly the same way. But of course all of these images are variations on a theme that I created when I wrote the program.

To draw a new card, just click your mouse in the graphics window (it may take a moment for the new card to be created and displayed).

You can see from the program below that drawing these cards takes a lot of steps. We have to draw every little element, from the windows to the fuzzy reflections in the water. There are lots of subtleties, from the colors of the background to how the stars appear. Once you've finished the course, everything in this program will make sense to you, and you'll be able to make your own postcards of your own imaginary scenes.

The Program

int NumLayers;     // how many layers of buildings to draw
int Waterline;     // height of waterline
float MaxHeight;   // maximmum building height
color WindowColor = color(220, 220, 20);  // average color of windows
boolean Redraw = true;  // do we need to draw a new postcard?

void setup() {
  size(600, 400);  // create graphics window 600 wide by 400 high
  smooth();        // draw everything pretty
  noStroke();      // don't draw outlines
}

void draw() {
  if (Redraw) {
     makeNewCard();  // make a new cared
     Redraw = false; // don't repeat until requested
  } 
}

void makeNewCard() {   // create a new card
   Waterline = int(height * 0.8);            // height of waterline
   MaxHeight = random(.2*height, .6*height); // building max height
   NumLayers = int(random(6, 10));           // building density
   drawSkyline();                            // and draw the result
}

void drawSkyline() {
  drawBackground();   // draw the sky and water
  for (int layer=0; layer<NumLayers; layer++) {  // draw each layer of the building
    drawLayer(layer);
  }
  drawLights();  // draw the lights and reflections
}

void drawLayer(int layer) {  // draw one layer of buildings
  float a = norm(layer, 0, NumLayers-1);  // how far back we are
  float avgWidth = lerp(width/50.0, width/20.0, a);   // average building width
  float avgHeight = lerp(MaxHeight, height/20.0, a);  // and height
  float layerDensity = lerp(.1, 1, a);  // guides how many buildings get drawn
  float left = -avgWidth;               // draw left to right, starting here

  while (left < width) {
    float buildingWidth = vary(avgWidth, .1);    // pick a width
    float buildingHeight = vary(avgHeight, .2);  // and height
    boolean drawMe = random(0, 1) < layerDensity;  // draw this one?
    if (drawMe) {
      drawBuilding(left, Waterline, buildingWidth, buildingHeight);  // draw it
    }
    left += buildingWidth; // and move right
  }
}

void drawBuilding(float bLeft, float bBottom, float bWid, float bHgt) {
  float buildingGrayColor = random(30, 90);  // basic building color
  fill(buildingGrayColor);                   // use that color
  rect(bLeft, bBottom, bWid, -bHgt);         // draw the building

  // now get a window color based on varying the generic window color
  color windowColor = color(vary(red(WindowColor), .1), 
                            vary(green(WindowColor), .1), 
                            vary(blue(WindowColor), .1));
  fill(windowColor);

  // figure out how many windows to draw, then draw each one
  int numAcross = int(random(10.0, 20.0));
  int numHigh = int(random(10.0, 20));
  float wWid = bWid / (numAcross*2.0);
  float wHgt = bHgt / (numHigh*2.0);

  float windowDensity = random(0.1, 0.7); // density of drawn windows
  for (int wx=0; wx<numAcross; wx++) {
    for (int wy=0; wy<numHigh; wy++) {
      float wLeft = (1.0/(numAcross*2.0)) + (wx*2*wWid);
      float wBottom = (1.0/(numHigh*2.0)) + (wy*2*wHgt);
      if (random(0, 1) < windowDensity) {  // draw this one?
         rect(bLeft+wLeft, bBottom-wBottom, wWid, -wHgt);
      }
    }
  }  
}

void drawBackground() {
  // draw the sky: a radial gradient blend from the moon at (cx, cy) 
  float cx = width * random(.6, .8);   // the moon is on the right-ish
  float cy = vary(Waterline, .1);      // and near the waterline
  float distToUL = dist(cx, cy, 0, 0); // distance to upper-left (0,0)
  color lighter = color(5, 60, 130);   // light color for gradient
  color darker = color(0, 15, 45);     // dark color for gradient
  
  for (int y=0; y<height; y++) {
    for (int x=0; x<width; x++) {
      float a = dist(x, y, cx, cy)/distToUL;  // distance from (x,y) to moon
      a = constrain(a, 0, 1);
      color clr = lerpColor(lighter, darker, a); // get the color here
      float ya = 1 - norm(y, 0, Waterline);      // height above waterline
      float threshold = .001 * ya;               // draw a star here?
      if (random(0, 1) < threshold) {
        // stars are dim at the waterline, and brighter as we go up.  
        // The square root function creates a nice-looking blend of intensity
        a = sqrt(a);  
        clr = lerpColor(clr, color(255), a);     // make this pixel bright
      }
      set(x, y, clr);  // set this pixel's colors
    }
  }

  // draw the (fake) building reflections
  color waterColor = color(10,10,30);      // color of the water
  for (int y=Waterline; y<height; y++) {
    for (int x=0; x<width; x++) {
      // ya is distance from Waterline
      float ya = 1-norm(y, Waterline, height-1);
      // ya2 creates a short fade right at the Waterline
      float ya2 = constrain((y-Waterline)/3.0, 0, 1);
      float wnoise = noise(x*.04, y*.01);
      wnoise = ya2 * ya * sq(wnoise);
      color clr = lerpColor(waterColor, WindowColor, wnoise);
      set(x, y, clr);
    }
  }
}

void drawLights() {
  int numLights = 20;
  noStroke();
  int lradius = 8;
  for (int l=0; l<numLights; l++) {
    int lx = int(random(0, width));
    int ly = int(Waterline-lradius+vary(lradius, .1));
    color lightColor = color(
    random(210, 255),  random(210, 255),  random(210, 255));

    // draw the light as two circles to fake a glow
    fill(red(lightColor), green(lightColor), blue(lightColor), 128);
    ellipse(lx, ly, lradius, lradius);
    fill(red(lightColor), green(lightColor), blue(lightColor), 255);
    ellipse(lx, ly, lradius/2, lradius/2);

    // draw fake reflections and add to water color
    for (int y=Waterline; y<height; y++) {
      for (int x=lx-2; x<lx+2; x++) {
        float ya = 1-norm(y, Waterline, height-1);  
        float wnoise = noise(x*.04, y*.01);
        wnoise = sq(ya) * sq(wnoise);  // fade out noise 
        color oldclr = get(x, y);
        color clr = lerpColor(oldclr, lightColor, wnoise);
        set(x, y, clr);
      }
    }
  }
}

// wiggle a number by a given percent and return the result
float vary(float value, float percent) {
  float range = value * percent;
  value += random(-range, range);
  return(value);
}

void mousePressed() { Redraw = true; }