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:
size and background sets up our window and drawing context;
rect, circle, line draws primitives;
begin_shape, vertex, end_shape draws arbitrary polyline/polygons;
fill, no_fill, stroke, no_stroke styles subsequent draw calls;
push_matrix, pop_matrix saves and restores transformation states;
translate and rotate_deg apply transformations;
create_graphics, begin, end, draw deals with offline rendering buffers;
text for simple text
rand.random generates random numbers and vectors.
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!