Recipes: Small Programs to Copy and Change
Each of these is a small, whole program that does one useful thing. Copy one into a new file, run it, and change it. Every one is here in JavaScript, for pictures.js, and in Python, for Pyctures, and every one is run by a test, so it works as it stands.
They are the same recipes Circuitry's own assistant works from when you ask it for one of these things.
- Sound: 12 recipes.
- Timing: 4 recipes.
- Pictures: 13 recipes.
- Effects: 4 recipes.
- Controls: 3 recipes.
- Games: 1 recipes.
- [Sheet music](#sheet music): 1 recipes.
Sound
Game sounds, tunes and synths. A browser is silent until the first click, tap or key press in the picture.
A kick, a snare and a hat from one chip voice each
For: Chip drums made, not ready-made.
const kick = makeSynth("chip", { decay: 0.2, sustain: 0, macro: [{ wave: "square", pitch: 14 }, { wave: "triangle", pitch: 7 }, { wave: "triangle", pitch: 0 }, { wave: "triangle", pitch: -5 }, { wave: "triangle", pitch: -10, volume: 0.8 }] })
const snare = makeSynth("chip", { decay: 0.17, sustain: 0, macro: [{ wave: "noise", pitch: 24 }, { wave: "square", pitch: 0 }, { wave: "square", pitch: -5, volume: 0.9 }, { wave: "noise", pitch: 20, volume: 0.8 }, { wave: "noise", pitch: 12, volume: 0.4 }] })
const hat = makeSynth("chip", { decay: 0.05, sustain: 0, volume: 0.5, macro: [{ wave: "noise", pitch: 36 }] })
let playing = false
function onMouseDown() {
if (playing) return
playing = true
kick.playNotes("C3 - - - C3 - - - C3 - - - C3 - C3 -", 0.125, { repeat: true })
snare.playNotes("- - - - A3 - - - - - - - A3 - - -", 0.125, { repeat: true })
hat.playNotes("C5 - C5 - C5 - C5 - C5 - C5 - C5 - C5 C5", 0.125, { repeat: true })
}
function draw() {
background("black")
text(playing ? "kick, snare, hat" : "Tap to play", width / 2, height / 2, "white", 28, "centre")
}To make it your own, change: The steps of a macro (wave, pitch in semitones, volume), a fiftieth of a second each; decay for how long the drum rings.
A chip lead that sings: vibrato, a moving pulse, an echo
For: A lead sound for a chiptune melody.
const lead = makeSynth("chipLead", { width: 0.3, pwm: 0.35, pwmRate: 0.7, vibrato: 0.25, vibratoRate: 5.5, vibratoDelay: 0.2, release: 0.08, echo: 0.3 })
function onMouseDown() {
lead.stop()
lead.playNotes("E5 A5 B5 C6:2 B5 A5:3 C5 F5 G5 A5:2 G5 F5:3", 0.18)
}
function draw() {
background("midnightblue")
text("Tap for the tune", width / 2, height / 2, "white", 28, "centre")
}To make it your own, change: width (0.1 thin to 0.5 hollow), pwm (how far the width moves), vibrato and vibratoDelay, echo; and the tune.
A bass whose every note drops into place
For: A punchy bass line.
const bass = makeSynth("chipBass", { pitchSweep: 12, pitchSweepDecay: 0.04, decay: 0.2, sustain: 0.6 })
let line = null
function onMouseDown() {
if (line) line.stop()
line = bass.playNotes("A1 - A2 A1 - A1 A2 - F1 - F2 F1 - F1 G1 G2", 0.14, { repeat: true })
}
function draw() {
background("black")
text(line ? "bass" : "Tap to play", width / 2, height / 2, "white", 28, "centre")
}To make it your own, change: pitchSweep (semitones it starts above the note) and pitchSweepDecay (how fast it falls); the line of notes.
A Commodore 64 chord: one voice stepping through the notes, changing each bar
For: Chords from one voice, changing on the bar.
const CHORDS = ["Am", "F", "C", "G"]
let arp = null, bar = -1
function onMouseDown() {
if (arp) return
setTempo(125) // beat 0 is now
arp = startArp(CHORDS[0]) // it holds until arp.stop()
}
function update(dt) {
if (!arp) return
const now = Math.floor(beat() / 4) // which bar we are in
if (now !== bar) {
bar = now
arp.change(CHORDS[bar % CHORDS.length]) // the same voice moves to the next chord
}
}
function draw() {
background("black")
text(arp ? CHORDS[Math.max(0, bar) % CHORDS.length] : "Tap to play", width / 2, height / 2, "white", 48, "centre")
}To make it your own, change: CHORDS (any chord names: "Am", "F", "Cm7", "G7"), the tempo, and 4, the beats to a bar.
A four-bar drum loop written as letters
For: A beat under a game or a tune.
const kick = makeSynth("kick"), snare = makeSynth("snare"), hat = makeSynth("hat", { volume: 0.5 })
const LOOP = ["k.h.s.h.k.k.s.h.", "k.h.s.h.k.k.s.hh", "k.h.s.h.k.k.s.h.", "k.h.s.hhk.s.ssss"].join("")
const line = (letter, note) => LOOP.split("").map((c) => (c === letter ? note : "-")).join(" ")
let loops = []
function onMouseDown() {
if (loops.length) return
setTempo(120)
loops = [
kick.playNotes(line("k", "C2"), "sixteenth", { repeat: true }),
snare.playNotes(line("s", "A3"), "sixteenth", { repeat: true }),
hat.playNotes(line("h", "C5"), "sixteenth", { repeat: true }),
] // started together, they stay together; loops.forEach((l) => l.stop()) ends them
}
function draw() {
background("black")
text(loops.length ? "bar " + (Math.floor(beat() / 4) % 4 + 1) : "Tap to play", width / 2, height / 2, "white", 28, "centre")
}To make it your own, change: The four bars of LOOP (k kick, s snare, h hat, a letter a sixteenth) and the tempo.
A tune the program works out: note, rest and loops
For: Music made by loops and rules, not typed out.
const lead = makeSynth("pluck"), bass = makeSynth("bass"), kick = makeSynth("kick"), hat = makeSynth("hat", { volume: 0.4 })
const SCALE = ["A4", "C5", "D5", "E5", "G5", "A5"]
let tune = null, drums = null
function onMouseDown() {
if (tune) return
setTempo(110)
startLines() // every line begins from now
tune = loopNotes(() => { // four bars, round and round
for (let bar = 0; bar < 4; bar++) {
for (let i = 0; i < 6; i++) lead.note(SCALE[(bar * 2 + i * 5) % 6], "eighth")
lead.rest("quarter")
bass.note(["A2", "F2", "C3", "G2"][bar], "whole")
}
})
drums = loopNotes(() => {
kick.note("C2", "quarter")
hat.note("C5", "eighth")
hat.note("C5", "eighth")
}) // tune.stop() and drums.stop() end them
}
function draw() {
background("midnightblue")
text(tune ? "beat " + (Math.floor(beat()) % 4 + 1) : "Tap to play", width / 2, height / 2, "white", 28, "centre")
}To make it your own, change: This tune is the recipe's own and everyone has it: write yours. SCALE and the sums that pick from it, the bass notes, the lengths ("eighth", "quarter", "half", "whole") and the tempo.
The smallest song: one pattern, three channels, played from the scene
For: A song as data in the program (never a file beside it).
const SONG = {
format: "synths-song", version: 1, speed: 6, tempo: 125, channels: 3, order: [0],
patterns: [{ rows: 16, events: [ // a row is a sixteenth note; channels and rows count from 0
{ row: 0, ch: 0, note: "E-5", ins: 1 }, { row: 4, ch: 0, note: "G-5", ins: 1 }, { row: 8, ch: 0, note: "A-5", ins: 1 }, { row: 14, ch: 0, note: "off" },
{ row: 0, ch: 1, note: "A-2", ins: 2 }, { row: 6, ch: 1, note: "A-3", ins: 2 }, { row: 8, ch: 1, note: "E-2", ins: 2 },
{ row: 0, ch: 2, note: "C-3", ins: 3 }, { row: 4, ch: 2, note: "A-3", ins: 4 }, { row: 8, ch: 2, note: "C-3", ins: 3 }, { row: 12, ch: 2, note: "A-3", ins: 4 },
] }],
instruments: [{ name: "lead", patch: "chipLead" }, { name: "bass", patch: "chipBass" }, { name: "kick", patch: "chipKick" }, { name: "snare", patch: "chipSnare" }], // counted from 1
}
const song = playSong(new TextEncoder().encode(JSON.stringify(SONG))) // it waits for the first tap, then goes round
function draw() {
background("black")
const at = song.position
text(at.state === "playing" ? "row " + at.row : "Tap to play", width / 2, height / 2, "white", 28, "centre")
}To make it your own, change: The events (row, ch, note, ins), rows, tempo, and order to play more patterns.
A song made from short text: a bar of tune to a line, drums as letters
For: A tune longer than a few bars, without typing events.
const BAR = 16
const BASS = { Dm: "D2", Bb: "A#1", F: "F2", C: "C2" }
const CHORDS = ["Dm", "Bb", "F", "C"] // a chord a bar
const TUNE = ["D5:2 F5:2 A5:4 G5:2 F5:2 D5:4", "D5:2 F5:2 A#5:4 A5:2 F5:2 D5:4", "C5:2 F5:2 A5:4 C6:2 A5:2 F5:4", "E5:2 G5:2 C6:4 G5:2 E5:2 -:4"]
const DRUMS = "k.h.s.h.k.k.s.h." // a bar: k kick, s snare, h hat
const number = (name) => "C D EF G A B".indexOf(name[0]) + (name[1] === "#" ? 1 : name[1] === "b" ? -1 : 0) + 12 * name.slice(-1) + 1 // "C4" is 49
function pattern(withTune) {
const events = []
CHORDS.forEach((chord, bar) => {
let row = bar * BAR
for (const word of withTune ? TUNE[bar].split(" ") : []) {
const [name, rows = 1] = word.split(":")
events.push(name === "-" ? { row, ch: 0, note: "off" } : { row, ch: 0, note: number(name), ins: 1 })
row += Number(rows)
}
for (let r = 0; r < BAR; r++) {
const at = bar * BAR + r
if (r % 2 === 0) events.push({ row: at, ch: 1, note: number(BASS[chord]) + (r % 4 ? 12 : 0), ins: 2 })
if (DRUMS[r] === "k") events.push({ row: at, ch: 2, note: "C-3", ins: 3 })
if (DRUMS[r] === "s") events.push({ row: at, ch: 2, note: "A-3", ins: 4 })
if (DRUMS[r] === "h") events.push({ row: at, ch: 3, note: "C-5", ins: 5, vol: 40 })
}
})
return { rows: 4 * BAR, events }
}
const synth = (preset, volume) => ({ name: preset, patch: "my " + preset, synth: { preset, set: { volume } } })
const SONG = {
format: "synths-song", version: 1, speed: 6, tempo: 130, channels: 4, order: [0, 1, 1],
patterns: [pattern(false), pattern(true)],
instruments: [synth("chipLead", 0.6), synth("chipBass", 0.7), synth("chipKick", 2), synth("chipSnare", 1.5), synth("chipHat", 1)],
}
const song = playSong(new TextEncoder().encode(JSON.stringify(SONG))) // it waits for the first tap
function draw() {
background("black")
text(song.position.state === "playing" ? "pattern " + song.position.order : "Tap to play", width / 2, height / 2, "white", 28, "centre")
}To make it your own, change: This tune is the recipe's own: write yours. TUNE (note:rows, - a rest, 16 rows to a bar), CHORDS and their bass notes, DRUMS, tempo; add patterns and name them in order.
Game sounds on events: once, at the moment it happens
For: Sound effects in a game.
let y = 0, vy = 0, jumps = 0, onGround = true
function jump() {
if (!onGround) return
vy = -520
onGround = false
playSound("jump") // once, when it happens
}
function onMouseDown() { jump() }
function onKeyDown() { jump() }
function update(dt) {
if (onGround) return
vy += 1400 * dt
y += vy * dt
if (y >= 0) { // the frame it lands, and only that frame
y = 0
onGround = true
jumps += 1
playSound(jumps % 5 === 0 ? "powerup" : "bounce", 0.6)
}
}
function draw() {
background("skyblue")
rect(0, height - 60, width, 60, "seagreen")
circle(width / 2, height - 85 + y, 25, "gold")
text("Tap or press a key to jump: " + jumps, width / 2, 40, "black", 20, "centre")
}To make it your own, change: The sound names (coin jump laser explosion hit shoot powerup blip click zap bounce pop win lose) and the moments. Never play one in draw(), or every frame while something is true.
A sound on/off button: the small speaker in the corner
For: Anything with music: let the user silence it.
soundButton() // a small speaker over the scene: a press, or the M key, mutes and unmutes
let tune = null
function onMouseDown() {
if (!tune) tune = playNotes("C4 E4 G4 C5 G4 E4", 0.2, { repeat: true })
}
function draw() {
background("midnightblue")
text(!tune ? "Tap to play" : soundIsOn() ? "playing" : "muted", width / 2, height / 2, "white", 28, "centre")
}To make it your own, change: Where it sits (soundButton("bottom left")) and what plays. It asks nothing and M does the same. soundIsOn() reads it, for a word on screen or to skip work while silent.
A demo that asks first: "This demo has sound", with or without
For: A demo to publish or show: ask before any sound.
askAboutSound() // the card, before anything is heard: "Play with sound" or "Play without sound"
let tune = null, t = 0
function update(dt) {
t += dt // nothing here runs until the person has answered the card
if (!tune && soundIsOn()) { // they chose sound, or turned the speaker on later
tune = playNotes("E4 G4 A4 - A4:2 C5 A4", "eighth", { repeat: true })
}
}
function draw() {
background("midnightblue")
circle(width / 2 + 150 * Math.cos(t * 2), height / 2 + 100 * Math.sin(t * 3), 30, "gold")
}To make it your own, change: What plays, and the words: askAboutSound("Music and effects") sets the title. One line at the top is all a demo needs: it puts the speaker button up too. KNOW: the scene waits behind the card, and only a real click or key answers it, so a run with tap: true cannot: you will see the card in doc.screenshot and no sound state; tell the user to choose. In a recording (Record Video) no card is shown and the sound is on from the first frame.
Timing
Things that happen after a while, again and again, or in time with music.
A timeline of parts keyed to where the song is
For: A demo or music video: parts that change on the bar.
const ROWS = 64 // rows in a pattern: four bars
const TIMELINE = [[0, oneStart, oneDraw], [1, twoStart, twoDraw]] // from which place in the song's order; start; draw
function makeSong() { // a placeholder tune: a bass, and a kick and snare from the second pattern
const patterns = [0, 1].map((busy) => {
const events = []
for (let row = 0; row < ROWS; row++) {
if (row % 2 === 0) events.push({ row, ch: 0, note: [34, 30, 37, 32][Math.floor(row / 16)] + (row % 4 ? 12 : 0), ins: 1 })
if (busy && row % 8 === 0) events.push({ row, ch: 1, note: "C-3", ins: 2 })
if (busy && row % 8 === 4) events.push({ row, ch: 1, note: "A-3", ins: 3 })
}
return { rows: ROWS, events }
})
return { format: "synths-song", version: 1, speed: 6, tempo: 125, channels: 2, order: [0, 1], patterns, instruments: [{ name: "bass", patch: "chipBass" }, { name: "kick", patch: "chipKick" }, { name: "snare", patch: "chipSnare" }] }
}
const song = playSong(new TextEncoder().encode(JSON.stringify(makeSong())))
let rows = 0, part = null, count = 0
function update(dt) {
const at = song.position
if (at.state !== "playing") return
const heard = at.order * ROWS + at.row + at.tick / at.speed
rows += dt * at.tempo / (2.5 * at.speed) // carry on smoothly between the song's reports
rows = Math.abs(heard - rows) > 2 ? heard : rows + (heard - rows) * 0.15 // a jump is taken; a drift is pulled back
const now = TIMELINE.findLast(([from]) => rows / ROWS >= from)
if (now !== part) { part = now; part[1]() } // the cut, on the first row of a pattern
count = (rows - part[0] * ROWS) / 4 // beats since the part began
}
function draw() {
if (part) return part[2]()
background("black")
text("TAP TO START", width / 2, height / 2, "white", 32, "centre")
}
function oneStart() {}
function oneDraw() { background("navy"); circle(width / 2, height / 2, 40 + 30 * (1 - count % 1), "gold") }
function twoStart() {}
function twoDraw() { background("maroon"); rect(width / 2 - 200, height / 2 - 20, 25 * (count % 16), 40, "white") }To make it your own, change: TIMELINE (a line a part: place in the song's order, its start function, its draw function), the parts, and the song.
Do something on every beat and every bar
For: A picture that moves in time with playNotes or a loop.
const COLOURS = ["navy", "darkcyan", "darkgreen", "maroon"]
let started = false, lastBeat = -1, flash = 0, bar = 0
function onMouseDown() {
if (started) return
started = true
setTempo(120) // beat 0 is now
makeSynth("kick").playNotes("C2", "quarter", { repeat: true })
}
function update(dt) {
flash = Math.max(0, flash - dt * 4) // falls away after each beat
if (!started) return
const now = Math.floor(beat()) // beat() counts in beats, with fractions
if (now === lastBeat) return
lastBeat = now
flash = 1 // on every beat
if (now % 4 === 0) bar = now / 4 // on every bar
}
function draw() {
background(COLOURS[bar % 4])
circle(width / 2, height / 2, 50 + 40 * flash, "white")
text(started ? "bar " + (bar + 1) : "Tap to start", width / 2, 50, "white", 24, "centre")
}To make it your own, change: The tempo, 4 (beats to a bar), and what happens on a beat and on a bar.
A flash on every drum hit, read from the song's own rows
For: Pictures that hit exactly with a song's kick and snare.
const ROWS = 64, KICK = 2, SNARE = 3 // rows in a pattern; the instruments' numbers in the song
function makeSong() { // a placeholder tune: a bass, and a kick and snare from the second pattern
const patterns = [0, 1].map((busy) => {
const events = []
for (let row = 0; row < ROWS; row++) {
if (row % 2 === 0) events.push({ row, ch: 0, note: [34, 30, 37, 32][Math.floor(row / 16)] + (row % 4 ? 12 : 0), ins: 1 })
if (busy && row % 8 === 0) events.push({ row, ch: 1, note: "C-3", ins: 2 })
if (busy && row % 8 === 4) events.push({ row, ch: 1, note: "A-3", ins: 3 })
}
return { rows: ROWS, events }
})
return { format: "synths-song", version: 1, speed: 6, tempo: 125, channels: 2, order: [0, 1], patterns, instruments: [{ name: "bass", patch: "chipBass" }, { name: "kick", patch: "chipKick" }, { name: "snare", patch: "chipSnare" }] }
}
const SONG = makeSong()
const hits = [] // for every row of the whole song: which drum is struck there
SONG.order.forEach((p, place) => SONG.patterns[p].events.forEach((e) => {
if (e.ins === KICK || e.ins === SNARE) hits[place * ROWS + e.row] = e.ins
}))
const song = playSong(new TextEncoder().encode(JSON.stringify(SONG)))
let last = -1, kick = 0, snare = 0
function update(dt) {
kick = Math.max(0, kick - dt * 5)
snare = Math.max(0, snare - dt * 5)
const at = song.position
if (at.state !== "playing") return
const row = at.order * ROWS + at.row
if (row < last) last = -1 // the song went round
for (let r = last + 1; r <= row; r++) { // every row gone by since the last frame: none is missed
if (hits[r] === KICK) kick = 1
if (hits[r] === SNARE) snare = 1
}
last = row
}
function draw() {
background(snare > 0.5 ? "white" : "black")
circle(width / 2, height / 2, 60 + 80 * kick, "crimson")
if (song.position.state !== "playing") text("Tap to start", width / 2, 50, "white", 24, "centre")
}To make it your own, change: Which instruments are the kick and the snare (KICK, SNARE: their numbers in your song), and what a hit does.
A title that waits for the tap that turns sound on
For: Anything with music: a browser is silent until the user taps.
let started = false, t = 0
function start() {
if (started) return // a second tap must not start a second tune
started = true
setTempo(120)
playNotes("E4 G4 A4 - A4:2 C5 A4", "eighth", { repeat: true })
}
function onMouseDown() { start() }
function onKeyDown() { start() }
function update(dt) {
if (started) t += dt // the pictures wait for the music
}
function draw() {
background("black")
if (!started) {
text("MY DEMO", width / 2, height / 2 - 20, "gold", 48, "centre")
text("tap, click or press a key to start", width / 2, height / 2 + 30, "white", 18, "centre")
return
}
circle(width / 2 + 150 * Math.cos(t * 2), height / 2 + 100 * Math.sin(t * 3), 30, "gold")
}To make it your own, change: The title, and what start() begins. With a SONG, play it at the top of the file instead and show the title until song.position.state is "playing".
Pictures
Drawing, moving and loading pictures.
A sine-wave scroller in a blocky font of its own
For: Scrolling text, demo style.
const FONT = { // five rows of five dots a letter, each row a number: 16 is the left dot, 1 the right
A: [14, 17, 31, 17, 17], B: [30, 17, 30, 17, 30], C: [15, 16, 16, 16, 15], D: [30, 17, 17, 17, 30], E: [31, 16, 30, 16, 31], F: [31, 16, 30, 16, 16],
G: [15, 16, 19, 17, 15], H: [17, 17, 31, 17, 17], I: [14, 4, 4, 4, 14], J: [1, 1, 1, 17, 14], K: [17, 18, 28, 18, 17], L: [16, 16, 16, 16, 31],
M: [17, 27, 21, 17, 17], N: [17, 25, 21, 19, 17], O: [14, 17, 17, 17, 14], P: [30, 17, 30, 16, 16], Q: [14, 17, 17, 19, 15], R: [30, 17, 30, 18, 17],
S: [15, 16, 14, 1, 30], T: [31, 4, 4, 4, 4], U: [17, 17, 17, 17, 14], V: [17, 17, 17, 10, 4], W: [17, 17, 21, 27, 17], X: [17, 10, 4, 10, 17],
Y: [17, 10, 4, 4, 4], Z: [31, 2, 4, 8, 31], "!": [4, 4, 4, 0, 4], ".": [0, 0, 0, 0, 4],
}
const WORDS = "HELLO FRIENDS! THIS IS A SCROLLER. EVERY LETTER IS FIVE ROWS OF DOTS. "
const COLOURS = ["#fff36b", "#ffb347", "#ff6b6b", "#ff3ea5", "#19c8ff"] // a colour a row, top to bottom
const DOT = 7, WIDE = 6 * DOT // a dot, and a letter with its gap
let t = 0
function update(dt) { t += dt }
function draw() {
background("#0a0a2a")
const moved = t * 160
for (let i = Math.floor(moved / WIDE); i * WIDE < moved + width; i++) { // only the letters on screen
const rows = FONT[WORDS[i % WORDS.length]]
if (!rows) continue // a space
const x = i * WIDE - moved, y = height / 2 + 60 * Math.sin(t * 3 + i * 0.4)
for (let r = 0; r < 5; r++) for (let c = 0; c < 5; c++) {
if (rows[r] & (16 >> c)) rect(x + c * DOT, y + r * DOT, DOT - 1, DOT - 1, COLOURS[r])
}
}
}To make it your own, change: WORDS (capitals, ! and .), DOT (the size of a dot), COLOURS (a colour a row), 160 (its speed) and the wave. In a demo, move it by the song: x from rows, not t.
Copper bars: shaded bars weaving up and down
For: A classic demo background.
const HUES = [0, 40, 120, 170, 200] // a bar each
let t = 0
function update(dt) { t += dt }
function draw() {
background("black")
// Drawn back to front: the bar that is lowest in its swing first, so bars pass in front of each other.
const bars = HUES.map((hue, i) => ({ hue, y: height / 2 + height * 0.3 * Math.sin(t * 1.7 + i * 0.6), depth: Math.cos(t * 1.7 + i * 0.6) }))
bars.sort((a, b) => a.depth - b.depth)
for (const bar of bars) {
for (let line = -9; line <= 9; line++) { // bright in the middle, dark at the edges: a round bar
const light = 60 - Math.abs(line) * 6
rect(0, bar.y + line * 3, width, 3, "hsl(" + bar.hue + ", 100%, " + light + "%)")
}
}
}To make it your own, change: HUES (a bar each), 9 (lines to a bar), and the two sines that move them.
A starfield you fly through
For: Stars rushing past: a demo or a space game background.
const stars = Array.from({ length: 200 }, () => ({ x: random(-1000, 1000), y: random(-1000, 1000), z: random(1, 1000) }))
function update(dt) {
for (const s of stars) {
s.z -= 1000 * 0.6 * dt // towards us
if (s.z < 1) { s.x = random(-1000, 1000); s.y = random(-1000, 1000); s.z = 1000 } // and round again
}
}
function draw() {
background("black")
for (const s of stars) {
const near = 300 / s.z // nearer is bigger and further from the middle
circle(width / 2 + s.x * near, height / 2 + s.y * near, Math.min(4, near), "white")
}
}To make it your own, change: 200 (how many), 0.6 (the speed), and the colour. For stars drifting sideways, move x and leave z alone.
A plasma from plain drawing: a small grid of coloured cells
For: A moving wash of colour with no effects library.
const CELL = 24 // a cell's size: about 40 by 25 of them
let t = 0
function update(dt) { t += dt }
function draw() {
for (let y = 0; y < height; y += CELL) {
for (let x = 0; x < width; x += CELL) {
// Three waves added together make the pattern; the sum picks the colour.
const v = Math.sin(x / 90 + t) + Math.sin(y / 60 - t * 1.3) + Math.sin((x + y) / 120 + t * 0.7)
rect(x, y, CELL, CELL, "hsl(" + Math.floor(v * 60 + t * 40) + ", 90%, 55%)")
}
}
}To make it your own, change: CELL (bigger is cheaper and blockier: keep the grid under about 1,500 cells), the three waves, and the colour sum.
A logo that bounces round the screen and squashes when it lands
For: A logo or a mascot in a demo or a title.
const LOGO = "🚀", SIZE = 90 // or a picture file saved beside the scene: "logo.png"
let x = 100, y = 100, vx = 220, vy = 0, squash = 0
function update(dt) {
vy += 900 * dt // gravity
x += vx * dt
y += vy * dt
squash = Math.max(0, squash - dt * 4)
if (x < SIZE / 2 || x > width - SIZE / 2) { vx = -vx; x = Math.max(SIZE / 2, Math.min(width - SIZE / 2, x)) }
if (y > height - SIZE / 2) { y = height - SIZE / 2; vy = -620; squash = 1 } // it lands: up again, and squashed for a moment
}
function draw() {
background("midnightblue")
ellipse(x, height - 8, SIZE * (0.4 + 0.4 * y / height), 10, "black") // its shadow
picture(LOGO, x, y + SIZE * 0.15 * squash, SIZE * (1 - 0.3 * squash))
}To make it your own, change: LOGO: an emoji, or the name of a picture file the user has saved beside the scene ("logo.png"); its size, speed and gravity.
A spinning wireframe cube, with no 3D library
For: A vector object in a demo: 3D from twelve lines.
const CORNERS = [[-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], [-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1]]
const EDGES = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]]
let t = 0
function update(dt) { t += dt }
function draw() {
background("black")
const a = t * 0.9, b = t * 0.6
const points = CORNERS.map(([x, y, z]) => {
const x1 = x * Math.cos(a) - z * Math.sin(a), z1 = x * Math.sin(a) + z * Math.cos(a) // turned about the upright
const y1 = y * Math.cos(b) - z1 * Math.sin(b), z2 = y * Math.sin(b) + z1 * Math.cos(b) // and tipped
const near = 220 / (4 + z2) // further away is smaller
return [width / 2 + x1 * near, height / 2 + y1 * near]
})
for (const [from, to] of EDGES) line(points[from][0], points[from][1], points[to][0], points[to][1], "cyan", 3)
for (const [x, y] of points) circle(x, y, 5, "white")
}To make it your own, change: CORNERS and EDGES (any shape made of points and lines), the two turning speeds, 220 (its size) and the colour.
An equaliser: bars that jump with the sound
For: Show the music: a visualiser with no effects library.
const BARS = 24
const tops = Array(BARS).fill(0)
let tune = null
function onMouseDown() {
if (!tune) tune = makeSynth("chipLead").playNotes("A4 C5 E5 A5 G5 E5 C5 E5 F4 A4 C5 F5 E5 C5 A4 C5", 0.15, { repeat: true })
}
function update(dt) {
const wave = soundOutput().samples() // what is sounding now, as numbers from -1 to 1
const each = Math.floor(wave.length / BARS)
for (let i = 0; i < BARS; i++) {
let loud = 0
for (let j = 0; j < each; j++) loud = Math.max(loud, Math.abs(wave[i * each + j] || 0))
tops[i] = Math.max(loud, tops[i] - dt * 1.5) // up at once, down slowly
}
}
function draw() {
background("black")
const wide = width / BARS
for (let i = 0; i < BARS; i++) {
const tall = 10 + tops[i] * height * 0.7
rect(i * wide + 2, height - 40 - tall, wide - 4, tall, "hsl(" + (i * 360 / BARS) + ", 90%, 55%)")
}
if (!tune) text("Tap to play", width / 2, 50, "white", 24, "centre")
}To make it your own, change: BARS, the colours, and whose sound: soundOutput() is everything; a synth (lead.samples()) is that synth alone. A song's own channels read 0 for synth instruments: use soundOutput().
A perspective grid floor rolling towards you, with a sun
For: A synthwave horizon for a title or a demo part.
let t = 0
function update(dt) { t += dt }
function draw() {
background("#0b1026")
const horizon = height * 0.55
circle(width / 2, horizon - 20, 90, "#ff5e78") // the sun
rect(0, horizon, width, height - horizon, "#060a1a")
for (let i = -12; i <= 12; i++) { // lines running away, all to one point on the horizon
line(width / 2 + i * 12, horizon, width / 2 + i * 160, height, "#ff3ea5", 2)
}
for (let i = 0; i < 12; i++) { // lines across: far ones crowd together, and they roll towards us
const near = ((i + t * 1.5) % 12) / 12
const y = horizon + (height - horizon) * near * near
line(0, y, width, y, "#ff3ea5", 1 + 2 * near)
}
}To make it your own, change: The horizon (0.55 of the height), 12 (lines across), the speed (1.5) and the colours.
Screen shake when something hits
For: An explosion, a landing, a drum hit: the whole picture jolts.
let shake = 0, sx = 0, sy = 0
function onMouseDown() {
shake = 1 // set it when something hits
playSound("explosion", 0.6)
}
function update(dt) {
shake = Math.max(0, shake - dt * 6) // it dies away
sx = random(-18, 18) * shake // a new jolt every frame
sy = random(-18, 18) * shake
}
function draw() {
background("black")
rect(80 + sx, 120 + sy, 240, 160, "steelblue") // the world is drawn moved by the shake
circle(520 + sx, 300 + sy, 70, "tomato")
text("Tap to shake", width / 2, 40, "white", 22, "centre") // the words stay still
}To make it your own, change: What sets shake (here a tap), 18 (how far), 6 (how fast it dies). Add sx and sy to everything that should move; leave the score still.
A rotozoom: a chessboard that turns and zooms
For: A turning, zooming pattern that fills the screen.
const CELL = 16, TILE = 160 // a screen cell, and a square of the board
let t = 0
function update(dt) { t += dt }
function draw() {
const angle = t * 0.7, zoom = 1.8 + 0.8 * Math.sin(t * 0.9) // both swing: that is the whole effect
const c = Math.cos(angle) * zoom, s = Math.sin(angle) * zoom
for (let y = 0; y < height; y += CELL) {
for (let x = 0; x < width; x += CELL) {
// Where this cell of the screen falls on the board, once the board is turned and zoomed about the middle.
const dx = x - width / 2, dy = y - height / 2
const u = dx * c - dy * s + t * 60, v = dx * s + dy * c
const dark = (Math.floor(u / TILE) + Math.floor(v / TILE)) % 2 === 0
rect(x, y, CELL, CELL, dark ? "#12306b" : "#ff5e78")
}
}
}To make it your own, change: CELL (bigger is cheaper), the tile (here two colours of a chessboard: any rule of u and v), and how the angle and the zoom swing.
A tunnel of rings you fall down
For: Flying down a twisting tunnel.
const RINGS = 28
let t = 0
function update(dt) { t += dt }
function draw() {
background("#05010f")
for (let i = RINGS; i >= 1; i--) { // the far ones first, so near rings are drawn over them
const z = i - (t * 1.2) % 1 // how far away: every ring comes one place nearer, round and round
const bend = z * 0.25 + t // the tunnel bends more the further you look down it
const sway = 1 - 1 / z // the nearest ring is round the middle; far ones follow the bend
const x = width / 2 + Math.sin(bend) * 140 * sway, y = height / 2 + Math.cos(bend * 0.7) * 90 * sway
const hue = (Math.floor(i + t * 1.2) * 25) % 360
circle(x, y, 520 / z, null, { outline: "hsl(" + hue + ", 90%, " + Math.max(15, 70 - z * 2) + "%)", thickness: Math.max(1, 14 / z) })
}
}To make it your own, change: RINGS, the speed (1.2), how far the tunnel bends (the two sines), and the colours. Squares turn it into a corridor: polygon with 4 sides and an angle.
A home-computer boot screen: a typed LOAD and loading stripes
For: An opening for a demo: an old computer loads it.
const LINES = [" **** COMMODORE 64 BASIC V2 ****", " 64K RAM SYSTEM 38911 BASIC BYTES FREE", "", "READY."]
const TYPED = 'LOAD"*",8,1'
const AFTER = ["", "SEARCHING FOR *", "LOADING", "READY.", "RUN"]
const STRIPES = ["#000000", "#ffffff", "#883932", "#67b6bd", "#c75c7a", "#55a049", "#2f4a9c", "#bfce72", "#8b5429", "#b86962"]
let t = 0
function update(dt) { t = (t + dt) % 12 } // it starts over, for looking at: a demo cuts away instead
function draw() {
const edge = Math.min(width, height) * 0.09, size = Math.min(26, (width - 2 * edge) / 26)
background("#7d95e8") // the pale border
if (t > 5.5 && t < 9) { // loading: the border is thin stripes of colour, new every frame
for (let y = 0; y < height; y += 6) rect(0, y, width, 6, STRIPES[random(0, STRIPES.length - 1)])
}
rect(edge, edge, width - 2 * edge, height - 2 * edge, "#2742a8") // the screen
const typed = TYPED.slice(0, Math.max(0, Math.floor((t - 2) * 6))) // six letters a second, from the second second
const lines = LINES.concat(t > 2 ? [typed] : [], AFTER.slice(0, Math.max(0, Math.floor((t - 4) / 1.2))))
lines.forEach((words, i) => text(words, edge + size, edge + size * (2 + i * 1.3), "#7d95e8", size, "left"))
if (Math.floor(t * 3) % 2 === 0) { // the cursor blinks at the end of the last line
const last = lines[lines.length - 1]
rect(edge + size * (1 + last.length * 0.6), edge + size * (1.2 + (lines.length - 1) * 1.3), size * 0.6, size, "#7d95e8")
}
}To make it your own, change: LINES (what the computer says), TYPED (what is typed), the two blues, and how long each stage lasts. Cut to the first part when t passes 10.
A credits crawl: lines rising and shrinking into the distance
For: Credits or a story at the end of a demo.
const CREDITS = ["A DEMO", "", "CODE", "YOUR NAME", "", "MUSIC", "A CHIPTUNE IN A MINOR", "", "MADE IN", "CIRCUITRY STUDIO", "", "THANKS FOR WATCHING"]
let t = 0
function update(dt) { t += dt }
function draw() {
background("black")
for (let i = 0; i < 60; i++) circle((i * 197) % width, (i * 89) % height, 1 + i % 2, "#555") // stars that stay still
const top = height * 0.25 // the point the words shrink towards
CREDITS.forEach((words, i) => {
const far = (t * 0.09 - i * 0.07) % (CREDITS.length * 0.07 + 1) // 0 at the bottom edge, 1 at the top; then round again
if (far < 0 || far > 1) return
const near = (1 - far) * (1 - far) // squared: the words slow and crowd as they go away
text(words, width / 2, top + (height - top) * near, "hsl(48, 100%, " + (25 + 45 * near) + "%)", 8 + 34 * near, "centre")
})
}To make it your own, change: CREDITS (short lines, capitals read best), 0.09 (the speed), the colour, and 0.25 (how far up the vanishing point is).
Effects
Glow, trails, a kaleidoscope and the rest, done by the graphics card to whatever the scene draws. See Effects.
A plasma folded by a kaleidoscope
For: A whole moving background from three lines.
const back = plasma("fire")
const fold = kaleidoscope(8)
glow(0.6)
function update(dt) {
fold.angle += 20 * dt // settings are properties: move them any time
}
function draw() {
background() // wipe with NO colour: the backdrop shows where the canvas is clear
}To make it your own, change: The palette ("fire" "ocean" "neon" "candy" "forest" "ice"), 8 (the folds), and how fast it turns.
A tunnel of colour from five stars and trails
For: A tunnel you fall through, drawn by an effect.
trails(2.5, { zoom: 1.9, spin: 50, hue: 140 })
glow(0.5, { radius: 30 })
let t = 0
function update(dt) { t += dt }
function draw() {
background("black") // trails want a dark background and something moving, with a colour to change
for (let i = 0; i < 5; i++) {
const a = t * 2 + i * Math.PI * 2 / 5
star(width / 2 + 60 * Math.cos(a), height / 2 + 60 * Math.sin(a), 10, 5, "gold")
}
}To make it your own, change: 2.5 (how long a trail lasts), 1.9 (how fast it grows), 50 (how fast it turns), 140 (how fast the colour moves).
An old television over the whole scene
For: A retro look for a game or a demo, in one line.
look("arcade") // several effects at once: scanlines, a curved glass, a glow
let t = 0
function update(dt) { t += dt }
function draw() {
background("navy")
circle(width / 2 + 150 * Math.cos(t), height / 2 + 100 * Math.sin(t * 1.3), 40, "gold")
text("INSERT COIN", width / 2, 60, "white", 32, "centre")
}To make it your own, change: The look: "arcade" "vhs" "gameboy" "c64" "oldfilm" "terminal"; or crt() alone.
A glow that pumps with the bass
For: Effects that move to the music.
const shine = glow(0.4, { radius: 40 })
react(shine, "strength", "bass", { from: 0.4, to: 1.5 }) // the setting follows the bass of whatever is playing
let tune = null
function onMouseDown() {
if (!tune) tune = makeSynth("bass").playNotes("A1 - A1 - - A1 - - F1 - F1 - - G1 - -", 0.15, { repeat: true })
}
function draw() {
background("black")
circle(width / 2, height / 2, 80, "deeppink")
if (!tune) text("Tap to play", width / 2, 50, "white", 24, "centre")
}To make it your own, change: Which effect and setting, what it follows ("bass" "mids" "highs" "loudness" "pulse"), and the two ends of its travel.
Controls
Dials, buttons and keys on the scene.
A synth panel you can play: dials, a wave switch, a scope and a keyboard
For: A synth with controls, in ten lines.
const lead = makeSynth("lead")
group("sound", () => {
rotary("wave", lead, "wave") // a switch with a place for each wave
dial("cutoff", lead, "cutoff") // a control tied to a setting takes its range and unit from it
dial("resonance", lead, "resonance")
})
group("shape", () => {
slider("attack", lead, "attack")
slider("release", lead, "release")
dial("echo", lead, "echo")
})
scope(lead) // its wave, live
keyboard(lead, { octave: 4 }) // keys to click or touch, and the computer's keys
function draw() {
background("#1b1b26")
}To make it your own, change: The synth ("lead", "pad", "pluck", "chipLead"), which settings get a dial (any synth setting), and the octave. NEVER draw a control yourself.
A joystick and a fire button for a phone
For: A game that must be playable on a touch screen.
const stick = joystick()
const fire = actionButton("A")
let x = 300, y = 200
const shots = []
function update(dt) {
x = Math.max(20, Math.min(width - 20, x + stick.x * 300 * dt)) // each from -1 to 1, and 0 when let go
y = Math.max(20, Math.min(height - 20, y + stick.y * 300 * dt))
if (fire.pressed) { // true once for each press
shots.push({ x, y })
playSound("laser", 0.5)
}
for (const s of shots) s.y -= 600 * dt
while (shots.length && shots[0].y < 0) shots.shift()
}
function draw() {
background("midnightblue")
for (const s of shots) rect(s.x - 2, s.y - 12, 4, 12, "yellow")
triangle(x, y - 20, x - 16, y + 16, x + 16, y + 16, "white")
}To make it your own, change: The speed (300), what fire does, and the ship. dpad() gives arrows instead of a stick.
A drum machine in one control
For: A beat the user can edit by tapping steps.
const drums = drumSeq("beat", ["kick", "snare", "hat", "clap"], makeSynth("kick"), { pattern: "backbeat", tempo: 110 })
function draw() {
background("#1b1b26")
}To make it your own, change: The drums' names, the starting pattern ("backbeat") and the tempo.
Games
The parts most games are made of.
A whole small game with sound effects and a tune: dodge the rocks
For: A complete game with music to build on.
const lead = makeSynth("chipLead", { volume: 0.5 }), bass = makeSynth("chipBass", { volume: 0.6 })
let rocks = [], score = 0, best = 0, started = false, next = 0
function start() {
if (started) return // the first tap or key turns the sound on and starts the tune, once
started = true
lead.playNotes("D5 - F5 A5 - F5 D5 - C5 - E5 G5 - E5 C5 -", 0.16, { repeat: true })
bass.playNotes("D2 - D3 - D2 - D3 - C2 - C3 - C2 - C3 -", 0.16, { repeat: true })
}
function onMouseDown() { start() }
function onKeyDown() { start() }
function update(dt) {
if (!started) return
next -= dt
if (next <= 0) { // a new rock, sooner as the score grows
rocks.push({ x: random(20, width - 20), y: -20, speed: random(150, 250) + score * 4 })
next = Math.max(0.25, 0.9 - score * 0.02)
}
for (const rock of rocks) rock.y += rock.speed * dt
const shipY = height - 50
if (rocks.some((rock) => Math.hypot(rock.x - mouse.x, rock.y - shipY) < 34)) { // hit: once, then start over
playSound("explosion")
best = Math.max(best, score)
rocks = []
score = 0
}
const passed = rocks.filter((rock) => rock.y > height + 20).length
if (passed) { // dodged: a point each, and one sound for the frame
score += passed
playSound(score % 10 === 0 ? "powerup" : "blip", 0.4)
rocks = rocks.filter((rock) => rock.y <= height + 20)
}
}
function draw() {
background("#0b1026")
for (const rock of rocks) circle(rock.x, rock.y, 18, "sienna")
triangle(mouse.x, height - 70, mouse.x - 16, height - 34, mouse.x + 16, height - 34, "white")
text(started ? "score " + score + " best " + best : "Move to dodge. Tap to start", width / 2, 36, "white", 22, "centre")
}To make it your own, change: The game itself (what falls, what a hit does, how it speeds up) and BOTH tunes: these notes are the recipe's own and everyone has them. Keep: the tune started once from the first tap, each sound played once at the moment it happens.
Sheet music
Music written out as notes on a staff, and lit as it plays.
A tune written out as sheet music, lit note by note as it plays
For: Show a tune as notes on a staff and follow it as it plays.
const page = showNotes("E4 G4 A4 - A4:2 C5 B4 A4 G4:2 E4:2", "quarter", { title: "A small tune" })
layout({ at: "bottom" }) // the button goes at the bottom, clear of the music at the top
button("play", () => (page.playing ? page.stop() : page.play())) // play() waits for the sound to be on
function draw() {
background("midnightblue") // nothing of the music is drawn here: the page draws itself over the scene
}To make it your own, change: The tune (the words of playNotes: - a rest, :2 two steps, C4+E4 a chord), the step ("eighth", "quarter"), the title; { key, time, clef } only to change what it works out for itself. KNOW: a staff has ONE rhythm, so two tunes at once want two pages, or a song with a channel each. The page draws itself over the scene: put nothing of it in draw(). With controls on the scene, layout at the bottom keeps them clear of the music at the top. page.play() waits for the sound to be on (the user's first tap). Sounding notes light; two notes joined by a curved line (a tie) are ONE note, held: both light together and one sound is heard. To play along: keyboard(page) and page.playAlong("wait") (Python page.play_along("wait")), which waits at each note for the right key.