2D Procedural Generation with Dither

Dither's standard library for 2D graphics, gx, has an easy-to-use interface that resembles that of Processing/p5.js. So if you're familiar with these platforms, you already know how to do draw in 2D with Dither. Nevertheless, this tutorial goes through the basics by writing a small but interesting project together with the reader. The hope is that we learn not only Dither, but also some useful ideas in procedural generation.

Overview

The gx library uses native rendering on each platform: Direct2D on windows, Coregraphics on mac, canvas API on the web... in order to give you the fastest and nicest-looking 2D graphics wherever you are.

Our example project is a generative building facade inspired by old apartment buildings in some parts of China.

Getting Started

Let's start by initializing an empty window and filling it with a background:

include "std/gx" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); while (1){ gx.poll(); }

We use the size function to initialize a window and 2D context given width and height (it calls win.init from "std/win" internally). background function takes RGB values in 0-1.0 range and fills the background with given color.

The poll function refreshes the display and listens for user events. We call it once per iteration in the main loop. This is one of the major differences from Processing/p5.js — instead of having predefined magic functions setup/draw, you're responsible for maintaining your own draw loop.

Next, let's sketch up the “scaffold” for our building facade project. We divide the problem up by drawing 1 apartment (or cell as we call it here) at a time. We write a randomized draw_cell() function, and use a nested for loop to call it once for each cell, positioning it correctly along the way.

include "std/gx" include "std/rand" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); cw : f32; // cell width ch : f32; // cell height cc : vec[f32,3]; // cell color func draw_cell(){ gx.no_stroke(); bc := cc+rand.random({0.05,0.05,0.05}); gx.fill(...bc); gx.rect(0,0,cw,ch); } x := 0; y := 0; while (x < W){ y = 0; cw = rand.random(80,120); ch = rand.random(98,102); cc = rand.random({0.8,0.7,0.6},{0.9,0.8,0.7}) while (y < H){ gx.push_matrix(); gx.translate(x,y); draw_cell(); gx.pop_matrix(); y += ch; } x += cw; } while (1){ gx.poll(); }

As the first step we just make width and height of each cell slightly different, as well as color. We randomize the color by giving rand.random function vector arguments (min and max), and thanks to template matching in Dither it selects the correct function to use. Since width and height are also randomized, we keep counters x and y to dynamically place the cells, since we don't know where each is going to end up before time.

rand.random can also take 1 argument (range: 0.0 to max) or no argument (range: 0 to 1.0).

In gx.fill(...bc), ... is the spread operator that puts components of a vector or tuple into arguments, it is as if writing out gx.fill(bc.b, bc.g, bc.b).

Functions of interest here include gx.push_matrix() and gx.pop_matrix(), which saves the current transformation state (and restores it later), and gx.translate which modifies the transformation state by applying a translation (movement) to it. The no_stroke and fill function styles subsequent draw calls, and rect simply draws a rectangle given left, top, width, height arguments.

Next up, let's add some windows to each cell.

include "std/gx" include "std/rand" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); cw : f32; // cell width ch : f32; // cell height cc : vec[f32,3]; // cell color wx : f32; // window x wy : f32; // window y ww : f32; // window width wh : f32; // window height nw : i32; // number of window panes dw : i32; // horizontal window divider? func draw_cell(){ gx.no_stroke(); bc := cc+rand.random({0.05,0.05,0.05}); gx.fill(...bc); gx.rect(0,0,cw,ch); // draw window outline gx.stroke(rand.random(0.7,1.0)); gx.stroke_weight(5); gx.rect(wx,wy,ww,wh); gx.stroke_weight(3); // draw individual panes for (i := 0; i < nw; i++){ bc := rand.random(0.5,0.6); if (rand.random()<0.2){ bc *= 0.6; } gx.fill(...(bc+rand.random({0.1,0.1,0.1}))); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw beam if (dw){ gx.line(wx,wy+wh*0.4,wx+ww,wy+wh*0.4); } } x := 0; y := 0; while (x < W){ y = 0; cw = rand.random(80,120); ch = rand.random(98,102); cc = rand.random({0.8,0.7,0.6},{0.9,0.8,0.7}) wx = rand.random(cw*0.1,cw*0.2); ww = cw-wx*2; wy = rand.random(ch*0.1,ch*0.3); wh = rand.random(ch*0.5,ch*0.7)-wy; nw = rand.random(2,5) dw = rand.random()<0.8 while (y < H){ gx.push_matrix(); gx.translate(x,y); draw_cell(); gx.pop_matrix(); y += ch; } x += cw; } while (1){ gx.poll(); }

We added a few new randomized variables for the windows (not the application window, nor microsoft Windows, I mean the real apartment windows), such as coordinates, size, and number of panes. In draw_cell, we first stroke a big frame, and use a for loop to stroke and fill individual panes and their (thinner) frames. We use a random muddy colors for the panes, with some “light” muddy ones and “dark” extra muddy ones.

There's also a chance to add a horizontal bar/beam/divider to somewhere near the middle of the panes. For this, we use gx.line, which takes start X, start Y, end X, end Y and draws a line.

A word about organizing code. As you might have noticed, our strategy is to have a few shared global variables for the main parameters of each cell, which gets randomized each time before we invoke the cell-drawing function. The minor, inconsequential parameters are generated within the subroutine. You can, of course, organize it some other, more pedantic way, for example, pass all parameters to the subroutine (maybe even packed together in a struct), or, have a class called Cell with the parameters as fields, and so on — You can do whatever you like, here, for simplicity's sake, we're doing it this way.

Adding Details

Next, let's add the outdoor AC units, which is a central visual element to Chinese urban landscape.

include "std/gx" include "std/rand" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); cw : f32; // cell width ch : f32; // cell height cc : vec[f32,3]; // cell color wx : f32; // window x wy : f32; // window y ww : f32; // window width wh : f32; // window height nw : i32; // number of window panes dw : i32; // horizontal window divider? func draw_cell(){ gx.no_stroke(); bc := cc+rand.random({0.05,0.05,0.05}); gx.fill(...bc); gx.rect(0,0,cw,ch); // draw window outline gx.stroke(rand.random(0.7,1.0)); gx.stroke_weight(5); gx.rect(wx,wy,ww,wh); gx.stroke_weight(3); // draw individual panes for (i := 0; i < nw; i++){ bc := rand.random(0.5,0.6); if (rand.random()<0.2){ bc *= 0.6; } gx.fill(...(bc+rand.random({0.1,0.1,0.1}))); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw beam if (dw){ gx.line(wx,wy+wh*0.4,wx+ww,wy+wh*0.4); } // draw AC lsp := ch-(wy+wh); // left-over space if (rand.random()<0.65 && lsp > 40){ acx := rand.random(0.,cw-40); acy := ch-lsp+5 // draw 3d box gx.no_stroke(); gx.fill(0.6); gx.rect(acx,acy,40,35); gx.fill(rand.random(0.85,0.95)); gx.rect(acx,acy,40,25); // fan variant if (rand.random()<0.3){ gx.fill(0.5); gx.rect(acx+2,acy+2,22,22) } // draw logo if (rand.random()<0.4){ gx.fill(...rand.random({0.1,0.1,0.1},{0.9,0.9,0.9})); gx.rect(acx+28,acy+3,7,5); } // draw fan gx.fill(0.3); gx.circle(acx+13,acy+12,20); gx.stroke(0.2,0.1,0.0); // draw brackets/support gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(acx+4,acy+40); gx.vertex(acx+4,acy+30); gx.vertex(acx+36,acy+30); gx.vertex(acx+36,acy+40); gx.end_shape(); } } x := 0; y := 0; while (x < W){ y = 0; cw = rand.random(80,120); ch = rand.random(98,102); cc = rand.random({0.8,0.7,0.6},{0.9,0.8,0.7}) wx = rand.random(cw*0.1,cw*0.2); ww = cw-wx*2; wy = rand.random(ch*0.1,ch*0.3); wh = rand.random(ch*0.5,ch*0.7)-wy; nw = rand.random(2,5) dw = rand.random()<0.8 while (y < H){ gx.push_matrix(); gx.translate(x,y); draw_cell(); gx.pop_matrix(); y += ch; } x += cw; } while (1){ gx.poll(); }

First, we calculate whether there's enough left-over space from drawing the windows, then we find a good location for the AC. We create an illusion of 3D by drawing 2 rectangles of different shades on top of each other, then decorate the box with fan, logo and brackets.

Functions of interest includes gx.circle, which draws a circle given center X, center Y, and diameter. It has a “sister” function (not shown here) called ellipse, which has diameter X (width) and diameter Y (height) in place of circle's last argument.

gx.begin_shape, gx.vertex and gx.end_shape functions allow us to draw arbitrary polygon shapes (which pretty much allow us to approximately draw any shape). Between begin_shape and end_shape, all the vertices of the desired polygon are to be listed, in order, with vertex. You can also supply a 0 or 1 to end_shape to indicate whether the shape is to be closed — more on this later.

Next, we are going to add some metal bars/“cages” to some of the windows. Don't worry! they're only for keeping burglars out.

include "std/gx" include "std/rand" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); cw : f32; // cell width ch : f32; // cell height cc : vec[f32,3]; // cell color wx : f32; // window x wy : f32; // window y ww : f32; // window width wh : f32; // window height nw : i32; // number of window panes dw : i32; // horizontal window divider? func draw_cell(){ gx.no_stroke(); bc := cc+rand.random({0.05,0.05,0.05}); gx.fill(...bc); gx.rect(0,0,cw,ch); // draw window outline gx.stroke(rand.random(0.7,1.0)); gx.stroke_weight(5); gx.rect(wx,wy,ww,wh); gx.stroke_weight(3); // draw individual panes for (i := 0; i < nw; i++){ bc := rand.random(0.5,0.6); if (rand.random()<0.2){ bc *= 0.6; } gx.fill(...(bc+rand.random({0.1,0.1,0.1}))); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw beam if (dw){ gx.line(wx,wy+wh*0.4,wx+ww,wy+wh*0.4); } // draw cage if (rand.random() < 0.4){ // cage is slightly larger than window wcx := wx - 5; wcw := ww + 10; wcy := wy - 5; wch := wh + 10; // draw outer frame gx.no_fill(); gx.stroke_weight(2); gx.stroke(rand.random(0.3,0.6)); gx.rect(wcx,wcy,wcw,wch); // draw vertical bars for (i := 0; i < wcw; i += 10){ gx.line(wcx+i,wcy,wcx+i,wcy+wch); } // draw bottom edge (3D illusion) gx.line(wcx,wcy+wch-5,wcx+wcw,wcy+wch-5); // chance for horizontal beam if (rand.random()<0.5){ pct := rand.random(0.2,0.8); gx.line(wcx,wcy+wch*pct,wcx+wcw,wcy+wch*pct); } // either top edge, or give it a canopy if (rand.random()<0.5){ gx.line(wcx,wcy+5,wcx+wcw,wcy+5); }else{ gx.fill(rand.random(0.2,0.5)); gx.no_stroke(); gx.rect(wcx-2,wcy-2,wcw+4,8); } } // draw AC lsp := ch-(wy+wh); // left-over space if (rand.random()<0.65 && lsp > 40){ acx := rand.random(0.,cw-40); acy := ch-lsp+5 // draw 3d box gx.no_stroke(); gx.fill(0.6); gx.rect(acx,acy,40,35); gx.fill(rand.random(0.85,0.95)); gx.rect(acx,acy,40,25); // fan variant if (rand.random()<0.3){ gx.fill(0.5); gx.rect(acx+2,acy+2,22,22) } // draw logo if (rand.random()<0.4){ gx.fill(...rand.random({0.1,0.1,0.1},{0.9,0.9,0.9})); gx.rect(acx+28,acy+3,7,5); } // draw fan gx.fill(0.3); gx.circle(acx+13,acy+12,20); gx.stroke(0.2,0.1,0.0); // draw brackets/support gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(acx+4,acy+40); gx.vertex(acx+4,acy+30); gx.vertex(acx+36,acy+30); gx.vertex(acx+36,acy+40); gx.end_shape(); } } x := 0; y := 0; while (x < W){ y = 0; cw = rand.random(80,120); ch = rand.random(98,102); cc = rand.random({0.8,0.7,0.6},{0.9,0.8,0.7}) wx = rand.random(cw*0.1,cw*0.2); ww = cw-wx*2; wy = rand.random(ch*0.1,ch*0.3); wh = rand.random(ch*0.5,ch*0.7)-wy; nw = rand.random(2,5) dw = rand.random()<0.8 while (y < H){ gx.push_matrix(); gx.translate(x,y); draw_cell(); gx.pop_matrix(); y += ch; } x += cw; } while (1){ gx.poll(); }

The code for drawing the cages are pretty straightforward — we make extensive use of gx.line. A for loop is used to arrange the individual bars in a horizontal array evenly spaced.

Next, let's add the colorful hanging clothes, which are an iconic sight in the region where I grew up.

include "std/gx" include "std/rand" include "std/math" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); cw : f32; // cell width ch : f32; // cell height cc : vec[f32,3]; // cell color wx : f32; // window x wy : f32; // window y ww : f32; // window width wh : f32; // window height nw : i32; // number of window panes dw : i32; // horizontal window divider? func draw_cell(){ gx.no_stroke(); bc := cc+rand.random({0.05,0.05,0.05}); gx.fill(...bc); gx.rect(0,0,cw,ch); // draw window outline gx.stroke(rand.random(0.7,1.0)); gx.stroke_weight(5); gx.rect(wx,wy,ww,wh); gx.stroke_weight(3); // draw individual panes for (i := 0; i < nw; i++){ bc := rand.random(0.5,0.6); if (rand.random()<0.2){ bc *= 0.6; } gx.fill(...(bc+rand.random({0.1,0.1,0.1}))); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw beam if (dw){ gx.line(wx,wy+wh*0.4,wx+ww,wy+wh*0.4); } // draw cage if (rand.random() < 0.4){ // cage is slightly larger than window wcx := wx - 5; wcw := ww + 10; wcy := wy - 5; wch := wh + 10; // draw outer frame gx.no_fill(); gx.stroke_weight(2); gx.stroke(rand.random(0.3,0.6)); gx.rect(wcx,wcy,wcw,wch); // draw vertical bars for (i := 0; i < wcw; i += 10){ gx.line(wcx+i,wcy,wcx+i,wcy+wch); } // draw bottom edge (3D illusion) gx.line(wcx,wcy+wch-5,wcx+wcw,wcy+wch-5); // chance for horizontal beam if (rand.random()<0.5){ pct := rand.random(0.2,0.8); gx.line(wcx,wcy+wch*pct,wcx+wcw,wcy+wch*pct); } // either top edge, or give it a canopy if (rand.random()<0.5){ gx.line(wcx,wcy+5,wcx+wcw,wcy+5); }else{ gx.fill(rand.random(0.2,0.5)); gx.no_stroke(); gx.rect(wcx-2,wcy-2,wcw+4,8); } } // draw AC lsp := ch-(wy+wh); // left-over space if (rand.random()<0.65 && lsp > 40){ acx := rand.random(0.,cw-40); acy := ch-lsp+5 // draw 3d box gx.no_stroke(); gx.fill(0.6); gx.rect(acx,acy,40,35); gx.fill(rand.random(0.85,0.95)); gx.rect(acx,acy,40,25); // fan variant if (rand.random()<0.3){ gx.fill(0.5); gx.rect(acx+2,acy+2,22,22) } // draw logo if (rand.random()<0.4){ gx.fill(...rand.random({0.1,0.1,0.1},{0.9,0.9,0.9})); gx.rect(acx+28,acy+3,7,5); } // draw fan gx.fill(0.3); gx.circle(acx+13,acy+12,20); gx.stroke(0.2,0.1,0.0); // draw brackets/support gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(acx+4,acy+40); gx.vertex(acx+4,acy+30); gx.vertex(acx+36,acy+30); gx.vertex(acx+36,acy+40); gx.end_shape(); } // hanging clothes if (rand.random() < 0.5){ // the frame gx.stroke(0.3,0.2,0.1); gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(wx,wy+wh+5); gx.vertex(wx,wy+wh-10); gx.vertex(wx+ww,wy+wh-10); gx.vertex(wx+ww,wy+wh+5); gx.end_shape(); // multiple pieces of clothing n := math.floor(rand.random(7.)); for (i := 1; i < n; i++){ // the pole gx.stroke(0.4,0.3,0.2); x0 := wx+i/n*ww+rand.random(-5,5) x1 := wx+i/n*ww+rand.random(-5,5) gx.line(x0,wy+wh-15,x1,wy+wh+5); // random irregularly shaped clothes gx.no_stroke(); m : = math.floor(rand.random(1,4)); for (j := 0; j < m; j++){ if (rand.random()<0.5){ // white-ish clothes gx.fill(...rand.random({0.8,0.8,0.8},{1.0,1.0,1.0})); }else{ // vibrant clothes gx.fill(...rand.random({0.2,0.2,0.2},{0.8,0.8,0.8})); } x := math.lerp(x1,x0,j/m); y := wy+wh+5-10*j/m; gx.begin_shape(); gx.vertex(x-5,y-rand.random(0,10)); gx.vertex(x+5,y-rand.random(0,10)); gx.vertex(x+5,y+rand.random(10,30)); gx.vertex(x,y+rand.random(10,30)); gx.vertex(x-5,y+rand.random(10,30)); gx.end_shape(); } } } } x := 0; y := 0; while (x < W){ y = 0; cw = rand.random(80,120); ch = rand.random(98,102); cc = rand.random({0.8,0.7,0.6},{0.9,0.8,0.7}) wx = rand.random(cw*0.1,cw*0.2); ww = cw-wx*2; wy = rand.random(ch*0.1,ch*0.3); wh = rand.random(ch*0.5,ch*0.7)-wy; nw = rand.random(2,5) dw = rand.random()<0.8 while (y < H){ gx.push_matrix(); gx.translate(x,y); draw_cell(); gx.pop_matrix(); y += ch; } x += cw; } while (1){ gx.poll(); }

The clothes are basically distorted pentagons on a slanted line. We randomly pick between two coloring schemes: white-ish color, or full random color, which seems to be a good enough model of the real world. We use begin_shape again to draw the inverted pentagon. We use a pentagon because it is complex enough to represent “draped” shape or “pants” shape, but not too complex that it starts introducing degeneracies. There're multiple poles per frame, and multiple clothes per pole, controlled again by nested for loops.

Now let's add some organic stuff to the scene, to imbue it with liveliness. We're going to add some plants to the windowsills. First, we're going to forget about the buildings for a moment, and focus on just getting the plants right. We use this technique a lot when generating self-contained sub-component of a larger system. We scale everything up 2x and layout a bunch of plants in a nice grid, so we can see all the varieties.

include "std/gx" include "std/rand" include "std/math" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); gx.scale(2); for (i:=0; i<6; i++){ for (j:=0; j<6; j++){ // plant position px := j * 50 + 25; py := i * 50 + 25; gx.no_stroke(); // plant color pc := rand.random({0.1,0.3,0.0},{0.2,0.4,0.1}) // flowering chance fr := rand.random()*2-1.0; // flower color fc := rand.random({0.7,0.2,0.2},{0.9,0.7,0.5}) // draw leaves for (i := 0; i < 10; i++){ // draw leaf, a rotated square gx.fill(...(pc+rand.random({0.1,0.1,0.1}))); gx.push_matrix(); gx.translate(px+rand.random(-6.,6.),py+rand.random(-5.,8.)); gx.rotate_deg(rand.random(360.)) gx.rect(-3,-3,6,6); gx.pop_matrix(); // draw pot, sandwiched between leaves if (i == 7){ gx.fill(...rand.random({0.2,0.1,0.1},{0.5,0.2,0.2})); pw := rand.random(10.,20.); gx.rect(px-pw/2,py+5,pw,6); } // draw flower if (rand.random()<fr){ gx.fill(...(fc+rand.random({0.1,0.1,0.1}))) gx.circle(px+rand.random(-6.,6.),py+rand.random(-6.,6.),3); } } } } while (1){ gx.poll(); }

We use a crude but effective technique for the leaves — A bunch of rotated squares randomly stacked on top of each other. Turns out when you have enough squares, they can give you an interesting overall outline without having things that are too sharp or smooth. We give each square a slightly different shade of green, to add depth and realism.

rotate_deg applies rotation to subsequent draw calls, given an angle in degrees. Notice that the order of the transformation matters — intuitively, whichever transformation that is nearer to the draw call gets applied first (try putting translate after rotate_deg and see how it messes things up).

If it is a flowering plant, we'll add a little circle of reddish color for each leaf. Other leaves might naturally occlude the flowers, thanks for our bunch-of-squares model of leaves.

We draw the pot with a brown-ish color, as is common with pots. We sandwich the pot between leaves, so it gets naturally occluded by some leaves but not all of them, again for depth and realism.

Now let's insert the plants into the scene. We want to draw the plant between the pane background color and the pane frame, so we had to break up the pane-drawing code a little.

include "std/gx" include "std/rand" include "std/math" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); cw : f32; // cell width ch : f32; // cell height cc : vec[f32,3]; // cell color wx : f32; // window x wy : f32; // window y ww : f32; // window width wh : f32; // window height nw : i32; // number of window panes dw : i32; // horizontal window divider? func draw_cell(){ gx.no_stroke(); bc := cc+rand.random({0.05,0.05,0.05}); gx.fill(...bc); gx.rect(0,0,cw,ch); // draw window outline gx.stroke(rand.random(0.7,1.0)); gx.stroke_weight(5); gx.rect(wx,wy,ww,wh); gx.no_stroke(); // draw panes without frame for (i := 0; i < nw; i++){ bc := rand.random(0.5,0.6); if (rand.random()<0.2){ bc *= 0.6; } gx.fill(...(bc+rand.random({0.1,0.1,0.1}))); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw plants np : i32 = rand.random(-12,4); for (k := 0; k < np; k++){ gx.no_stroke(); // plant position px := wx+rand.random(10.,ww-10) py := wy+wh-12; // plant color pc := rand.random({0.1,0.3,0.0},{0.2,0.4,0.1}) // flowering chance fr := rand.random()*2-1.0; // flower color fc := rand.random({0.7,0.2,0.2},{0.9,0.7,0.5}) // draw leaves for (i := 0; i < 10; i++){ // draw leaf, a rotated square gx.fill(...(pc+rand.random({0.1,0.1,0.1}))); gx.push_matrix(); gx.translate(px+rand.random(-6,6),py+rand.random(-5,8)); gx.rotate_deg(rand.random(360.)) gx.rect(-3,-3,6,6); gx.pop_matrix(); // draw pot, sandwiched between leaves if (i == 7){ gx.fill(...rand.random({0.2,0.1,0.1},{0.5,0.2,0.2})); pw := rand.random(10,20); gx.rect(px-pw/2,py+5,pw,6); } // draw flower if (rand.random()<fr){ gx.fill(...(fc+rand.random({0.1,0.1,0.1}))) gx.circle(px+rand.random(-6,6),py+rand.random(-6,6),3); } } } // draw pane frames gx.stroke_weight(3); gx.no_fill(); for (i := 0; i < nw; i++){ gx.stroke(rand.random(0.8,1.0)); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw beam if (dw){ gx.line(wx,wy+wh*0.4,wx+ww,wy+wh*0.4); } // draw cage if (rand.random() < 0.4){ // cage is slightly larger than window wcx := wx - 5; wcw := ww + 10; wcy := wy - 5; wch := wh + 10; // draw outer frame gx.no_fill(); gx.stroke_weight(2); gx.stroke(rand.random(0.3,0.6)); gx.rect(wcx,wcy,wcw,wch); // draw vertical bars for (i := 0; i < wcw; i += 10){ gx.line(wcx+i,wcy,wcx+i,wcy+wch); } // draw bottom edge (3D illusion) gx.line(wcx,wcy+wch-5,wcx+wcw,wcy+wch-5); // chance for horizontal beam if (rand.random()<0.5){ pct := rand.random(0.2,0.8); gx.line(wcx,wcy+wch*pct,wcx+wcw,wcy+wch*pct); } // either top edge, or give it a canopy if (rand.random()<0.5){ gx.line(wcx,wcy+5,wcx+wcw,wcy+5); }else{ gx.fill(rand.random(0.2,0.5)); gx.no_stroke(); gx.rect(wcx-2,wcy-2,wcw+4,8); } } // draw AC lsp := ch-(wy+wh); // left-over space if (rand.random()<0.65 && lsp > 40){ acx := rand.random(0.,cw-40); acy := ch-lsp+5 // draw 3d box gx.no_stroke(); gx.fill(0.6); gx.rect(acx,acy,40,35); gx.fill(rand.random(0.85,0.95)); gx.rect(acx,acy,40,25); // fan variant if (rand.random()<0.3){ gx.fill(0.5); gx.rect(acx+2,acy+2,22,22) } // draw logo if (rand.random()<0.4){ gx.fill(...rand.random({0.1,0.1,0.1},{0.9,0.9,0.9})); gx.rect(acx+28,acy+3,7,5); } // draw fan gx.fill(0.3); gx.circle(acx+13,acy+12,20); gx.stroke(0.2,0.1,0.0); // draw brackets/support gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(acx+4,acy+40); gx.vertex(acx+4,acy+30); gx.vertex(acx+36,acy+30); gx.vertex(acx+36,acy+40); gx.end_shape(); } // hanging clothes if (rand.random() < 0.5){ // the frame gx.stroke(0.3,0.2,0.1); gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(wx,wy+wh+5); gx.vertex(wx,wy+wh-10); gx.vertex(wx+ww,wy+wh-10); gx.vertex(wx+ww,wy+wh+5); gx.end_shape(); // multiple pieces of clothing n := math.floor(rand.random(7.)); for (i := 1; i < n; i++){ // the pole gx.stroke(0.4,0.3,0.2); x0 := wx+i/n*ww+rand.random(-5,5) x1 := wx+i/n*ww+rand.random(-5,5) gx.line(x0,wy+wh-15,x1,wy+wh+5); // random irregularly shaped clothes gx.no_stroke(); m : = math.floor(rand.random(1,4)); for (j := 0; j < m; j++){ if (rand.random()<0.5){ // white-ish clothes gx.fill(...rand.random({0.8,0.8,0.8},{1.0,1.0,1.0})); }else{ // vibrant clothes gx.fill(...rand.random({0.2,0.2,0.2},{0.8,0.8,0.8})); } x := math.lerp(x1,x0,j/m); y := wy+wh+5-10*j/m; gx.begin_shape(); gx.vertex(x-5,y-rand.random(0,10)); gx.vertex(x+5,y-rand.random(0,10)); gx.vertex(x+5,y+rand.random(10,30)); gx.vertex(x,y+rand.random(10,30)); gx.vertex(x-5,y+rand.random(10,30)); gx.end_shape(); } } } } x := 0; y := 0; while (x < W){ y = 0; cw = rand.random(80,120); ch = rand.random(98,102); cc = rand.random({0.8,0.7,0.6},{0.9,0.8,0.7}) wx = rand.random(cw*0.1,cw*0.2); ww = cw-wx*2; wy = rand.random(ch*0.1,ch*0.3); wh = rand.random(ch*0.5,ch*0.7)-wy; nw = rand.random(2,5) dw = rand.random()<0.8 while (y < H){ gx.push_matrix(); gx.translate(x,y); draw_cell(); gx.pop_matrix(); y += ch; } x += cw; } while (1){ gx.poll(); }

As you can see our scene is looking pretty good, if I might say so myself. Time to add some finishing touches and call it a day!

Finishing Touches

Many building facades are made out of some sort of tiles or bricks, instead of just cement. So we want to model that. Also, some buildings have exposed pipes.

include "std/gx" include "std/rand" include "std/math" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); cw : f32; // cell width ch : f32; // cell height cc : vec[f32,3]; // cell color wx : f32; // window x wy : f32; // window y ww : f32; // window width wh : f32; // window height nw : i32; // number of window panes dw : i32; // horizontal window divider? br : i32; // has bricks? pp : i32; // has pipe? func draw_cell(){ gx.no_stroke(); bc := cc+rand.random({0.05,0.05,0.05}); gx.fill(...bc); gx.rect(0,0,cw,ch); // draw bricks (tiles) if (br){ gx.stroke(...(cc-{0.1,0.1,0.1})); gx.stroke_weight(1); // horizontal lines for (i := 0; i < 10; i++){ gx.line(0,i*ch/10,cw,i*ch/10); } // vertical lines for (i := 0; i < 20; i++){ gx.line(i*cw/20,0,i*cw/20,ch); } } // draw window outline gx.stroke(rand.random(0.7,1.0)); gx.stroke_weight(5); gx.rect(wx,wy,ww,wh); gx.no_stroke(); // draw panes without frame for (i := 0; i < nw; i++){ bc := rand.random(0.5,0.6); if (rand.random()<0.2){ bc *= 0.6; } gx.fill(...(bc+rand.random({0.1,0.1,0.1}))); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw plants np : i32 = rand.random(-12,4); for (k := 0; k < np; k++){ gx.no_stroke(); // plant position px := wx+rand.random(10.,ww-10) py := wy+wh-12; // plant color pc := rand.random({0.1,0.3,0.0},{0.2,0.4,0.1}) // flowering chance fr := rand.random()*2-1.0; // flower color fc := rand.random({0.7,0.2,0.2},{0.9,0.7,0.5}) // draw leaves for (i := 0; i < 10; i++){ // draw leaf, a rotated square gx.fill(...(pc+rand.random({0.1,0.1,0.1}))); gx.push_matrix(); gx.translate(px+rand.random(-6,6),py+rand.random(-5,8)); gx.rotate_deg(rand.random(360.)) gx.rect(-3,-3,6,6); gx.pop_matrix(); // draw pot, sandwiched between leaves if (i == 7){ gx.fill(...rand.random({0.2,0.1,0.1},{0.5,0.2,0.2})); pw := rand.random(10,20); gx.rect(px-pw/2,py+5,pw,6); } // draw flower if (rand.random()<fr){ gx.fill(...(fc+rand.random({0.1,0.1,0.1}))) gx.circle(px+rand.random(-6,6),py+rand.random(-6,6),3); } } } // draw pane frames gx.stroke_weight(3); gx.no_fill(); for (i := 0; i < nw; i++){ gx.stroke(rand.random(0.8,1.0)); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw beam if (dw){ gx.line(wx,wy+wh*0.4,wx+ww,wy+wh*0.4); } // draw pipe if (pp && ww < cw*0.7){ // horizontal position ppx := math.lerp(wx+ww,cw,0.6) // light line on dark line // (for 3D illusion) gx.stroke(0.6,0.55,0.5); gx.stroke_weight(5); gx.line(ppx,0,ppx,ch); gx.stroke(0.9); gx.stroke_weight(2.5); gx.line(ppx-0.25,0,ppx-0.25,ch-1); } // draw cage if (rand.random() < 0.4){ // cage is slightly larger than window wcx := wx - 5; wcw := ww + 10; wcy := wy - 5; wch := wh + 10; // draw outer frame gx.no_fill(); gx.stroke_weight(2); gx.stroke(rand.random(0.3,0.6)); gx.rect(wcx,wcy,wcw,wch); // draw vertical bars for (i := 0; i < wcw; i += 10){ gx.line(wcx+i,wcy,wcx+i,wcy+wch); } // draw bottom edge (3D illusion) gx.line(wcx,wcy+wch-5,wcx+wcw,wcy+wch-5); // chance for horizontal beam if (rand.random()<0.5){ pct := rand.random(0.2,0.8); gx.line(wcx,wcy+wch*pct,wcx+wcw,wcy+wch*pct); } // either top edge, or give it a canopy if (rand.random()<0.5){ gx.line(wcx,wcy+5,wcx+wcw,wcy+5); }else{ gx.fill(rand.random(0.2,0.5)); gx.no_stroke(); gx.rect(wcx-2,wcy-2,wcw+4,8); } } // draw AC lsp := ch-(wy+wh); // left-over space if (rand.random()<0.65 && lsp > 40){ acx := rand.random(0.,cw-40); acy := ch-lsp+5 // draw 3d box gx.no_stroke(); gx.fill(0.6); gx.rect(acx,acy,40,35); gx.fill(rand.random(0.85,0.95)); gx.rect(acx,acy,40,25); // fan variant if (rand.random()<0.3){ gx.fill(0.5); gx.rect(acx+2,acy+2,22,22) } // draw logo if (rand.random()<0.4){ gx.fill(...rand.random({0.1,0.1,0.1},{0.9,0.9,0.9})); gx.rect(acx+28,acy+3,7,5); } // draw fan gx.fill(0.3); gx.circle(acx+13,acy+12,20); gx.stroke(0.2,0.1,0.0); // draw brackets/support gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(acx+4,acy+40); gx.vertex(acx+4,acy+30); gx.vertex(acx+36,acy+30); gx.vertex(acx+36,acy+40); gx.end_shape(); } // hanging clothes if (rand.random() < 0.5){ // the frame gx.stroke(0.3,0.2,0.1); gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(wx,wy+wh+5); gx.vertex(wx,wy+wh-10); gx.vertex(wx+ww,wy+wh-10); gx.vertex(wx+ww,wy+wh+5); gx.end_shape(); // multiple pieces of clothing n := math.floor(rand.random(7.)); for (i := 1; i < n; i++){ // the pole gx.stroke(0.4,0.3,0.2); x0 := wx+i/n*ww+rand.random(-5,5) x1 := wx+i/n*ww+rand.random(-5,5) gx.line(x0,wy+wh-15,x1,wy+wh+5); // random irregularly shaped clothes gx.no_stroke(); m : = math.floor(rand.random(1,4)); for (j := 0; j < m; j++){ if (rand.random()<0.5){ // white-ish clothes gx.fill(...rand.random({0.8,0.8,0.8},{1.0,1.0,1.0})); }else{ // vibrant clothes gx.fill(...rand.random({0.2,0.2,0.2},{0.8,0.8,0.8})); } x := math.lerp(x1,x0,j/m); y := wy+wh+5-10*j/m; gx.begin_shape(); gx.vertex(x-5,y-rand.random(0,10)); gx.vertex(x+5,y-rand.random(0,10)); gx.vertex(x+5,y+rand.random(10,30)); gx.vertex(x,y+rand.random(10,30)); gx.vertex(x-5,y+rand.random(10,30)); gx.end_shape(); } } } } x := 0; y := 0; while (x < W){ y = 0; cw = rand.random(80,120); ch = rand.random(98,102); cc = rand.random({0.8,0.7,0.6},{0.9,0.8,0.7}) wx = rand.random(cw*0.1,cw*0.2); ww = cw-wx*2; wy = rand.random(ch*0.1,ch*0.3); wh = rand.random(ch*0.5,ch*0.7)-wy; nw = rand.random(2,5) dw = rand.random()<0.8 br = rand.random()<0.7 pp = rand.random()<0.5 while (y < H){ gx.push_matrix(); gx.translate(x,y); draw_cell(); gx.pop_matrix(); y += ch; } x += cw; } while (1){ gx.poll(); }

Whether or not we have tiles or pipes are each determined by a random chance. The pipe is just two lines, thick dark line and thin light line on top of each other, for a crude illusion of a cylindrical object. The bricks are just a grid, drawn with colors slightly darker color than that of the facade itself.

There we have it! The generative piece is now complete. You can try increasing the width and height of the canvas, to explore the variations. Or if you think of some other elements to add, feel free to try those out too!

Interactivity & Animation

Right now we get one static view and that's it. Let's add a click event listener to refresh/regenerate the piece, so we can have a better look at all the variations. It'll also be more convenient for our users, if they want to pick one they like.

include "std/win" include "std/gx" include "std/rand" include "std/math" W := 600 H := 600 gx.size(W,H); gx.background(0.7,0.8,0.85); func generate(){ cw : f32; // cell width ch : f32; // cell height cc : vec[f32,3]; // cell color wx : f32; // window x wy : f32; // window y ww : f32; // window width wh : f32; // window height nw : i32; // number of window panes dw : i32; // horizontal window divider? br : i32; // has bricks? pp : i32; // has pipe? func draw_cell(){ gx.no_stroke(); bc := cc+rand.random({0.05,0.05,0.05}); gx.fill(...bc); gx.rect(0,0,cw,ch); // draw bricks (tiles) if (br){ gx.stroke(...(cc-{0.1,0.1,0.1})); gx.stroke_weight(1); // horizontal lines for (i := 0; i < 10; i++){ gx.line(0,i*ch/10,cw,i*ch/10); } // vertical lines for (i := 0; i < 20; i++){ gx.line(i*cw/20,0,i*cw/20,ch); } } // draw window outline gx.stroke(rand.random(0.7,1.0)); gx.stroke_weight(5); gx.rect(wx,wy,ww,wh); gx.no_stroke(); // draw panes without frame for (i := 0; i < nw; i++){ bc := rand.random(0.5,0.6); if (rand.random()<0.2){ bc *= 0.6; } gx.fill(...(bc+rand.random({0.1,0.1,0.1}))); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw plants np : i32 = rand.random(-12,4); for (k := 0; k < np; k++){ gx.no_stroke(); // plant position px := wx+rand.random(10.,ww-10) py := wy+wh-12; // plant color pc := rand.random({0.1,0.3,0.0},{0.2,0.4,0.1}) // flowering chance fr := rand.random()*2-1.0; // flower color fc := rand.random({0.7,0.2,0.2},{0.9,0.7,0.5}) // draw leaves for (i := 0; i < 10; i++){ // draw leaf, a rotated square gx.fill(...(pc+rand.random({0.1,0.1,0.1}))); gx.push_matrix(); gx.translate(px+rand.random(-6,6),py+rand.random(-5,8)); gx.rotate_deg(rand.random(360.)) gx.rect(-3,-3,6,6); gx.pop_matrix(); // draw pot, sandwiched between leaves if (i == 7){ gx.fill(...rand.random({0.2,0.1,0.1},{0.5,0.2,0.2})); pw := rand.random(10,20); gx.rect(px-pw/2,py+5,pw,6); } // draw flower if (rand.random()<fr){ gx.fill(...(fc+rand.random({0.1,0.1,0.1}))) gx.circle(px+rand.random(-6,6),py+rand.random(-6,6),3); } } } // draw pane frames gx.stroke_weight(3); gx.no_fill(); for (i := 0; i < nw; i++){ gx.stroke(rand.random(0.8,1.0)); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw beam if (dw){ gx.line(wx,wy+wh*0.4,wx+ww,wy+wh*0.4); } // draw pipe if (pp && ww < cw*0.7){ // horizontal position ppx := math.lerp(wx+ww,cw,0.6) // light line on dark line // (for 3D illusion) gx.stroke(0.6,0.55,0.5); gx.stroke_weight(5); gx.line(ppx,0,ppx,ch); gx.stroke(0.9); gx.stroke_weight(2.5); gx.line(ppx-0.25,0,ppx-0.25,ch-1); } // draw cage if (rand.random() < 0.4){ // cage is slightly larger than window wcx := wx - 5; wcw := ww + 10; wcy := wy - 5; wch := wh + 10; // draw outer frame gx.no_fill(); gx.stroke_weight(2); gx.stroke(rand.random(0.3,0.6)); gx.rect(wcx,wcy,wcw,wch); // draw vertical bars for (i := 0; i < wcw; i += 10){ gx.line(wcx+i,wcy,wcx+i,wcy+wch); } // draw bottom edge (3D illusion) gx.line(wcx,wcy+wch-5,wcx+wcw,wcy+wch-5); // chance for horizontal beam if (rand.random()<0.5){ pct := rand.random(0.2,0.8); gx.line(wcx,wcy+wch*pct,wcx+wcw,wcy+wch*pct); } // either top edge, or give it a canopy if (rand.random()<0.5){ gx.line(wcx,wcy+5,wcx+wcw,wcy+5); }else{ gx.fill(rand.random(0.2,0.5)); gx.no_stroke(); gx.rect(wcx-2,wcy-2,wcw+4,8); } } // draw AC lsp := ch-(wy+wh); // left-over space if (rand.random()<0.65 && lsp > 40){ acx := rand.random(0.,cw-40); acy := ch-lsp+5 // draw 3d box gx.no_stroke(); gx.fill(0.6); gx.rect(acx,acy,40,35); gx.fill(rand.random(0.85,0.95)); gx.rect(acx,acy,40,25); // fan variant if (rand.random()<0.3){ gx.fill(0.5); gx.rect(acx+2,acy+2,22,22) } // draw logo if (rand.random()<0.4){ gx.fill(...rand.random({0.1,0.1,0.1},{0.9,0.9,0.9})); gx.rect(acx+28,acy+3,7,5); } // draw fan gx.fill(0.3); gx.circle(acx+13,acy+12,20); gx.stroke(0.2,0.1,0.0); // draw brackets/support gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(acx+4,acy+40); gx.vertex(acx+4,acy+30); gx.vertex(acx+36,acy+30); gx.vertex(acx+36,acy+40); gx.end_shape(); } // hanging clothes if (rand.random() < 0.5){ // the frame gx.stroke(0.3,0.2,0.1); gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(wx,wy+wh+5); gx.vertex(wx,wy+wh-10); gx.vertex(wx+ww,wy+wh-10); gx.vertex(wx+ww,wy+wh+5); gx.end_shape(); // multiple pieces of clothing n := math.floor(rand.random(7.)); for (i := 1; i < n; i++){ // the pole gx.stroke(0.4,0.3,0.2); x0 := wx+i/n*ww+rand.random(-5,5) x1 := wx+i/n*ww+rand.random(-5,5) gx.line(x0,wy+wh-15,x1,wy+wh+5); // random irregularly shaped clothes gx.no_stroke(); m : = math.floor(rand.random(1,4)); for (j := 0; j < m; j++){ if (rand.random()<0.5){ // white-ish clothes gx.fill(...rand.random({0.8,0.8,0.8},{1.0,1.0,1.0})); }else{ // vibrant clothes gx.fill(...rand.random({0.2,0.2,0.2},{0.8,0.8,0.8})); } x := math.lerp(x1,x0,j/m); y := wy+wh+5-10*j/m; gx.begin_shape(); gx.vertex(x-5,y-rand.random(0,10)); gx.vertex(x+5,y-rand.random(0,10)); gx.vertex(x+5,y+rand.random(10,30)); gx.vertex(x,y+rand.random(10,30)); gx.vertex(x-5,y+rand.random(10,30)); gx.end_shape(); } } } } x := 0; y := 0; while (x < W){ y = 0; cw = rand.random(80,120); ch = rand.random(98,102); cc = rand.random({0.8,0.7,0.6},{0.9,0.8,0.7}) wx = rand.random(cw*0.1,cw*0.2); ww = cw-wx*2; wy = rand.random(ch*0.1,ch*0.3); wh = rand.random(ch*0.5,ch*0.7)-wy; nw = rand.random(2,5) dw = rand.random()<0.8 br = rand.random()<0.7 pp = rand.random()<0.5 while (y < H){ gx.push_matrix(); gx.translate(x,y); draw_cell(); gx.pop_matrix(); y += ch; } x += cw; } gx.fill(0); gx.text("Click Anywhere to Generate New...",0,16); } generate(); while (1){ e := gx.poll(); if (e.type == win.MOUSE_PRESSED){ generate(); } }

We simply wrap our whole program in a function called generate, and call that whenever mouse button is pressed anywhere. We detect the event by looking at the return value of gx.poll(), which refreshes display and grabs user events, as mentioned before. Each poll pops one event, which we check if the type of which is MOUSE_PRESSED upon which we call generate again.

We use gx.text to draw a string in a fixed 8x16px monospace font, given string, x, and baselineY.

Again, we could have organized our code differently, perhaps by having some sort of parameter object or by modularizing. Here we just do barely enough to keep things simple.

Now this clicking thing is a bit boring. What if we have an infinite scroll of buildings? Like it keeps expanding forever and never repeats. It is not so hard to achieve, by having some offline buffers that we swap around.

include "std/gx" include "std/rand" include "std/math" include "std/list" W := 600 H := 600 gx.size(W,H); func generate():i32{ cw : f32; // cell width ch : f32; // cell height cc : vec[f32,3]; // cell color wx : f32; // window x wy : f32; // window y ww : f32; // window width wh : f32; // window height nw : i32; // number of window panes dw : i32; // horizontal window divider? br : i32; // has bricks? pp : i32; // has pipe? func draw_cell(){ gx.no_stroke(); bc := cc+rand.random({0.05,0.05,0.05}); gx.fill(...bc); gx.rect(0,0,cw,ch); // draw bricks (tiles) if (br){ gx.stroke(...(cc-{0.1,0.1,0.1})); gx.stroke_weight(1); // horizontal lines for (i := 0; i < 10; i++){ gx.line(0,i*ch/10,cw,i*ch/10); } // vertical lines for (i := 0; i < 20; i++){ gx.line(i*cw/20,0,i*cw/20,ch); } } // draw window outline gx.stroke(rand.random(0.7,1.0)); gx.stroke_weight(5); gx.rect(wx,wy,ww,wh); gx.no_stroke(); // draw panes without frame for (i := 0; i < nw; i++){ bc := rand.random(0.5,0.6); if (rand.random()<0.2){ bc *= 0.6; } gx.fill(...(bc+rand.random({0.1,0.1,0.1}))); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw plants np : i32 = rand.random(-12,4); for (k := 0; k < np; k++){ gx.no_stroke(); // plant position px := wx+rand.random(10.,ww-10) py := wy+wh-12; // plant color pc := rand.random({0.1,0.3,0.0},{0.2,0.4,0.1}) // flowering chance fr := rand.random()*2-1.0; // flower color fc := rand.random({0.7,0.2,0.2},{0.9,0.7,0.5}) // draw leaves for (i := 0; i < 10; i++){ // draw leaf, a rotated square gx.fill(...(pc+rand.random({0.1,0.1,0.1}))); gx.push_matrix(); gx.translate(px+rand.random(-6,6),py+rand.random(-5,8)); gx.rotate_deg(rand.random(360.)) gx.rect(-3,-3,6,6); gx.pop_matrix(); // draw pot, sandwiched between leaves if (i == 7){ gx.fill(...rand.random({0.2,0.1,0.1},{0.5,0.2,0.2})); pw := rand.random(10,20); gx.rect(px-pw/2,py+5,pw,6); } // draw flower if (rand.random()<fr){ gx.fill(...(fc+rand.random({0.1,0.1,0.1}))) gx.circle(px+rand.random(-6,6),py+rand.random(-6,6),3); } } } // draw pane frames gx.stroke_weight(3); gx.no_fill(); for (i := 0; i < nw; i++){ gx.stroke(rand.random(0.8,1.0)); gx.rect(wx+i*(ww/nw),wy,ww/nw,wh); } // draw beam if (dw){ gx.line(wx,wy+wh*0.4,wx+ww,wy+wh*0.4); } // draw pipe if (pp && ww < cw*0.7){ // horizontal position ppx := math.lerp(wx+ww,cw,0.6) // light line on dark line // (for 3D illusion) gx.stroke(0.6,0.55,0.5); gx.stroke_weight(5); gx.line(ppx,0,ppx,ch); gx.stroke(0.9); gx.stroke_weight(2.5); gx.line(ppx-0.25,0,ppx-0.25,ch-1); } // draw cage if (rand.random() < 0.4){ // cage is slightly larger than window wcx := wx - 5; wcw := ww + 10; wcy := wy - 5; wch := wh + 10; // draw outer frame gx.no_fill(); gx.stroke_weight(2); gx.stroke(rand.random(0.3,0.6)); gx.rect(wcx,wcy,wcw,wch); // draw vertical bars for (i := 0; i < wcw; i += 10){ gx.line(wcx+i,wcy,wcx+i,wcy+wch); } // draw bottom edge (3D illusion) gx.line(wcx,wcy+wch-5,wcx+wcw,wcy+wch-5); // chance for horizontal beam if (rand.random()<0.5){ pct := rand.random(0.2,0.8); gx.line(wcx,wcy+wch*pct,wcx+wcw,wcy+wch*pct); } // either top edge, or give it a canopy if (rand.random()<0.5){ gx.line(wcx,wcy+5,wcx+wcw,wcy+5); }else{ gx.fill(rand.random(0.2,0.5)); gx.no_stroke(); gx.rect(wcx-2,wcy-2,wcw+4,8); } } // draw AC lsp := ch-(wy+wh); // left-over space if (rand.random()<0.65 && lsp > 40){ acx := rand.random(0.,cw-40); acy := ch-lsp+5 // draw 3d box gx.no_stroke(); gx.fill(0.6); gx.rect(acx,acy,40,35); gx.fill(rand.random(0.85,0.95)); gx.rect(acx,acy,40,25); // fan variant if (rand.random()<0.3){ gx.fill(0.5); gx.rect(acx+2,acy+2,22,22) } // draw logo if (rand.random()<0.4){ gx.fill(...rand.random({0.1,0.1,0.1},{0.9,0.9,0.9})); gx.rect(acx+28,acy+3,7,5); } // draw fan gx.fill(0.3); gx.circle(acx+13,acy+12,20); gx.stroke(0.2,0.1,0.0); // draw brackets/support gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(acx+4,acy+40); gx.vertex(acx+4,acy+30); gx.vertex(acx+36,acy+30); gx.vertex(acx+36,acy+40); gx.end_shape(); } // hanging clothes if (rand.random() < 0.5){ // the frame gx.stroke(0.3,0.2,0.1); gx.no_fill(); gx.stroke_weight(3); gx.begin_shape(); gx.vertex(wx,wy+wh+5); gx.vertex(wx,wy+wh-10); gx.vertex(wx+ww,wy+wh-10); gx.vertex(wx+ww,wy+wh+5); gx.end_shape(); // multiple pieces of clothing n := math.floor(rand.random(7.)); for (i := 1; i < n; i++){ // the pole gx.stroke(0.4,0.3,0.2); x0 := wx+i/n*ww+rand.random(-5,5) x1 := wx+i/n*ww+rand.random(-5,5) gx.line(x0,wy+wh-15,x1,wy+wh+5); // random irregularly shaped clothes gx.no_stroke(); m : = math.floor(rand.random(1,4)); for (j := 0; j < m; j++){ if (rand.random()<0.5){ // white-ish clothes gx.fill(...rand.random({0.8,0.8,0.8},{1.0,1.0,1.0})); }else{ // vibrant clothes gx.fill(...rand.random({0.2,0.2,0.2},{0.8,0.8,0.8})); } x := math.lerp(x1,x0,j/m); y := wy+wh+5-10*j/m; gx.begin_shape(); gx.vertex(x-5,y-rand.random(0,10)); gx.vertex(x+5,y-rand.random(0,10)); gx.vertex(x+5,y+rand.random(10,30)); gx.vertex(x,y+rand.random(10,30)); gx.vertex(x-5,y+rand.random(10,30)); gx.end_shape(); } } } } y := 0; cw = rand.random(80,120); ch = rand.random(98,102); cc = rand.random({0.8,0.7,0.6},{0.9,0.8,0.7}) wx = rand.random(cw*0.1,cw*0.2); ww = cw-wx*2; wy = rand.random(ch*0.1,ch*0.3); wh = rand.random(ch*0.5,ch*0.7)-wy; nw = rand.random(2,5) dw = rand.random()<0.8 br = rand.random()<0.7 pp = rand.random()<0.5 while (y < H){ gx.push_matrix(); gx.translate(0,y); draw_cell(); gx.pop_matrix(); y += ch; } return cw; } swaps := list[gx.Graphics]{}; ws := list[f32]{}; dx := 0; for (i:=0; i<8; i++){ swaps.push(gx.create_graphics(120,H)); swaps[i].begin(); ws.push(generate()); swaps[i].end(); } while (1){ gx.background(0.7,0.8,0.85); x := dx; for (i:=0; i<swaps.length(); i++){ swaps[i].draw(x,0); x += ws[i]; } dx = dx-1; if (dx < -ws[0]){ dx += ws[0]; swaps[0].begin(); ws[0] = generate(); swaps[0].end(); swaps.push(swaps[0]); ws.push(ws[0]); swaps.erase(0,1); ws.erase(0,1); } gx.poll(); }

We initialize offscreen rendering buffers (think of it as a canvas or drawing surface that we don't immediately put onto the screen) using gx.create_graphics(w,h). Once we call begin on the graphics, we can draw whatever we like, just like with the main canvas. When end is called on it, we return to drawing on the main canvas. We can blit the offscreen graphics back onto the screen by calling draw(x,y) on the graphics.

A simpler version of what we implemented above: we can just have two such buffers swap (left) and swbp (right), moving at the same time toward the left. When swap completely moves out of view, we fill it with new buildings, and put it to the right of swbp. Now swbp is the new conceptual swap and vice versa, and we keep on doing that.

A small problem with that approach is, if you have a slower computer, you might notice a tiny hiccup once in a while — this is when the new buffer is being generated. We improve it by having smaller slices: one buffer per column of buildings. We keep a list of these, and move the first of the list to the end of the list whenever it scrolls out of view. This implementation does exactly just that!

Now an interesting exercise for the reader would be to make it scroll vertically, or even diagonally!

Conclusion

In this tutorial, we learnt quite a few thing by doing a generative project together. To recap:

Now, you can read the official Dither examples which uses gx for 2D graphics extensively to learn more, check out the reference, or step up the game by working with 3D!