code your own t-shirt

Creating designs with HTML canvas

You can have a lot of fun creating designs using JavaScript and HTML canvas, but it can be hard getting started. It’s not technically difficult if you’ve done a little web development, but it takes some time to figure out what you want to draw. This post is for you if you want to try designing images with code, but you’re not sure where to start. The basics of drawing 2D shapes using HTML canvas are explained quite well by MDN’s documentation, and I won’t try to replicate their great work. I also found html5canvastutorials.com while digging around for canvas guides. I’ve included a few basic operations below for reference.

// First we get a reference to the canvas context,
// which lets us draw shapes
const canvas = document.getElementById(canvasId)
const ctx = canvas.getContext('2d')
// We can draw shapes, like a red rectangle
ctx.fillStyle = 'red'
ctx.fillRect(10, 10, 100, 100)
// ... or a blue circle
ctx.fillStyle = 'rgb(0, 0, 255)' // CSS color setting
ctx.beginPath()
ctx.arc(WIDTH / 2, HEIGHT / 2, 300, 0, Math.PI * 2)
ctx.fill()
// We can clear rectangles
ctx.fillStyle = 'green'
ctx.fillRect(200, 200, 100, 100)
ctx.clearRect(200, 200, 50, 50)
// We can draw shapes with 'strokes'
let x0, y0
x0 = 3 * WIDTH / 4
y0 = 3 * HEIGHT / 4
ctx.lineWidth = 30
ctx.strokeStyle = 'orange'
ctx.beginPath()
ctx.moveTo(x0, y0)
ctx.lineTo(x0, y0 + 400)
ctx.lineTo(x0 + 400, y0 + 400)
ctx.closePath()
ctx.stroke()
// .. and we can fill them in
ctx.fillStyle = 'purple'
ctx.beginPath()
ctx.moveTo(x0, y0)
ctx.lineTo(x0, y0 + 400)
ctx.lineTo(x0 + 400, y0 + 400)
ctx.closePath()
ctx.fill()
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Exploring color space

You can do draw interesting stuff just by messing around with colors. The canvas' fillStyle property takes your typical CSS color strings, like rgb(255, 0, 0), and you can change those values incrementally to get a gradient-like effect. Note that if all you want is a gradient, there are simpler methods you can use.

const canvas = document.getElementById(canvasId)
const ctx = canvas.getContext('2d')
const COLS = 20
const SIZE = WIDTH / COLS
const ROWS = Math.floor(HEIGHT / SIZE)
for (let row = 0; row <= ROWS; row++) {
for (let col = 0; col <= COLS; col++) {
ctx.fillStyle = `rgb(${5 * (row + col)}, 0, 255)`
ctx.fillRect(col * SIZE, row * SIZE, SIZE, SIZE)
}
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Using hue / saturation / value

Using RGB to explore color space programmatically has some issues though: it’s hard to avoid hitting unwanted whites, blacks and browns. Using HSL (hue / saturation / value) is much better suited to doing this, because you can change the colors by adjusting the hue parameter, without affecting how dark, light or washed-out the image is.

const canvas = document.getElementById(canvasId)
const ctx = canvas.getContext('2d')
const COLS = 20
const SIZE = WIDTH / COLS
const ROWS = Math.floor(HEIGHT / SIZE)
for (let row = 0; row <= ROWS; row++) {
for (let col = 0; col <= COLS; col++) {
ctx.fillStyle = `hsl(${3* (row + col)}, 100%, 70%)`
ctx.fillRect(col * SIZE, row * SIZE, SIZE, SIZE)
}
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

You can produce some easy, fun designs using just the repeating shapes and color changes we've covered:

const canvas = document.getElementById(canvasId)
const ctx = canvas.getContext('2d')
const PADDING = 30
const COLS = 5
const SIZE = WIDTH / COLS
const ROWS = Math.floor(HEIGHT / SIZE) - 1
const main = () => {
for (let row = 0; row <= ROWS; row++) {
for (let col = 0; col <= COLS; col++) {
ctx.fillStyle = `hsl(${60 - 10 * (row + col)}, 100%, 80%)`
const x = col * SIZE + SIZE / 2
const y = row * SIZE + SIZE / 2
const radius = SIZE / 2 - PADDING
drawPacman(x, y, radius)
}
}
}
const drawPacman = (x, y, radius) => {
// Draw body
ctx.beginPath()
ctx.arc(x, y, radius, Math.PI / 8, 15 * Math.PI / 8)
ctx.lineTo(x, y)
ctx.fill()
// Draw eyes
ctx.fillStyle = 'rgb(60, 60, 60)'
ctx.beginPath()
ctx.arc(x + 40, y - 100, 20, 0, 2 * Math.PI)
ctx.fill()
}
main()
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Using recursion

You can describe structures and patterns using a small amount code if you make use of recursion: a function calling itself. A simple case is “draw a shape, then draw another smaller copy next to it”:

const canvas = document.getElementById(canvasId)
const ctx = canvas.getContext('2d')
const drawPacman = (x, y, radius) => {
ctx.fillStyle = `hsl(${radius / 4 + 300}, 100%, 80%)`
// Draw body
ctx.beginPath()
ctx.arc(x, y, radius, Math.PI / 8, 15 * Math.PI / 8)
ctx.lineTo(x, y)
ctx.fill()
// Draw eyes
ctx.fillStyle = 'rgb(60, 60, 60)'
ctx.beginPath()
ctx.arc(x + radius / 6, y - radius / 2, radius / 15, 0, 2 * Math.PI)
ctx.fill()
// Keep drawing until the pacman is too small.
if (radius >= 60) {
// Draw a baby pacman to the bottom right.
drawPacman(x + 3 * radius / 4, y + radius, 13 * radius / 16)
}
}
// Start drawing
drawPacman(500, 500, 500)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Trees

In addition to legions of pac-men, you can describe detailed and beautiful structures using recursion. Trees (shown below), the Sierpinski triangle, and fractals can all be described in a suprisingly terse manner.

const canvas = document.getElementById(canvasId)
const ctx = canvas.getContext('2d')
const drawBranch = (x, y, length, angle) => {
ctx.strokeStyle = `hsl(${200 + 1000 / Math.sqrt(length)}, 100%, 60%)`
const x1 = x + length * Math.cos(angle)
const y1 = y - length * Math.sin(angle)
ctx.lineWidth = length / 15
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x1, y1)
ctx.stroke()
if (length >= 100) {
const newLength = 3 * length / 4
drawBranch(x1, y1, newLength, angle + Math.PI / 12)
drawBranch(x1, y1, newLength, angle)
drawBranch(x1, y1, newLength, angle - Math.PI / 12)
}
}
// Start drawing
drawBranch(WIDTH / 2, HEIGHT - 500, HEIGHT / 5, Math.PI / 2)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Have fun!

Hopefully I've motivated you to play around with code and draw some things. There's a few things I'd like to cover but have omitted, including drawing using finite automata and going into more detail on fractals.

If you've created a design you like, you can get it printed on a t-shirt here