Experimenting with Drawing Combinators

I’m working on a functional TypeScript library for drawing to a canvas inspired by parser combinators. Let’s try it out by making some generative art! If you haven’t heard of creative coding, this talk by Tim Holman is a great intro.

First we need to setup a project. We will make an index.html with a canvas, a main.ts that renders our sketch to the canvas, and a sketch.ts that defines our sketch.

// main.ts

import { render } from './drawing-combinators';
import sketch from './sketch';

const canvas = document.getElementById('canvas') as HTMLCanvasElement;
render(canvas, sketch);
// sketch.ts

import * as D from './drawing-combinators';

export default [D.fillColor('white', D.fillRect(0, 0, 100, 100)), D.strokeCircle(50, 50, 50)];

The render function expects a 100x100 by sketch. It then scales to fit the canvas and renders the sketch.

The above code results in a circle on a white background.

Now we can edit sketch.ts to draw whatever we want. Let’s start with a classic, 10 PRINT.

import * as D from './drawing-combinators';

const range = (amount: number) => [...Array(amount).keys()];

const line = (size: number) =>
	Math.random() < 0.5 ? D.strokeLine(0, 0, size, size) : D.strokeLine(0, size, size, 0);

const grid = (size: number) =>
	range(size).map((x) =>
		range(size).map((y) => D.translate((x * 100) / size, (y * 100) / size, line(100 / size)))
	);

export default [D.fillColor('white', D.fillRect(0, 0, 100, 100)), grid(20)];

The code isn’t as concise as it is on the Commodore 64, but this will allow for some fun additions. Let’s make the lines horizontal and vertical.

const line = (size: number) =>
	Math.random() < 0.5
		? D.strokeLine(0, size / 2, size, size / 2)
		: D.strokeLine(size / 2, 0, size / 2, size);

What if all 4 types of lines are possible?

const randomChoice = <T>(choices: T[]) => choices[Math.floor(Math.random() * choices.length)];

const line = (size: number) =>
	randomChoice([
		D.strokeLine(0, 0, size, size),
		D.strokeLine(0, size, size, 0),
		D.strokeLine(0, size / 2, size, size / 2),
		D.strokeLine(size / 2, 0, size / 2, size)
	]);

What if we clip it to a circle?

export default [
	D.fillColor('white', D.fillRect(0, 0, 100, 100)),
	D.clipCircle(50, 50, 40, grid(20))
];

That’s pretty cool! Now let’s change it up entirely and try something recursive.

import * as D from './drawing-combinators';

const centeredSquare = (x: number, y: number, size: number) =>
	D.translate(x - size / 2, y - size / 2, D.strokeRect(0, 0, size, size));

const DIST_MULTIPLIER = 0.027;
const ANGLE_CHANGE_SPEED = 0.4;
const SIZE_CHANGE_SPEED = 0.8;

const spiral = (x: number, y: number, size: number, angle: number) =>
	size < 40
		? [
				D.translate(x, y, D.rotateAround(angle, size / 2, size / 2, centeredSquare(0, 0, size))),
				...spiral(
					x + Math.cos(angle) * DIST_MULTIPLIER * size ** 2,
					y + Math.sin(angle) * DIST_MULTIPLIER * size ** 2,
					size + SIZE_CHANGE_SPEED,
					angle + ANGLE_CHANGE_SPEED
				)
		  ]
		: [];

export default [
	D.fillColor('white', D.fillRect(0, 0, 100, 100)),
	D.strokeWidth(1 / 3, spiral(50, 50, 1, 0))
];

Nice! I think this was a successful experiment. Creative coding is always fun, but this library made it more fun. Changing requirements on the fly is common when creative coding, so functional programming is a great fit. Our code becomes composable and reusable by default.