Sparse FM bell plucks over A major pentatonic. Delays and a wash of reverb.
morning-bells-previewimport type { Patch } from '@/lib/engine/types'
const morningBells: Patch = {
id: 'morning-bells',
title: 'Morning Bells',
author: 'stuart',
year: 2026,
tags: ['bells', 'arpeggiated', 'bright'],
description:
'Sparse FM bell plucks over A major pentatonic. Delays and a wash of reverb.',
audio: ({ tone, rng, initialTick, dispose }) => {
const reverb = new tone.Reverb({ decay: 8, wet: 0.45 }).toDestination()
const delay = new tone.FeedbackDelay({
delayTime: '8n.',
feedback: 0.35,
wet: 0.35,
}).connect(reverb)
const synth = new tone.PolySynth(tone.FMSynth, {
harmonicity: 3,
modulationIndex: 6,
envelope: { attack: 0.01, decay: 0.4, sustain: 0, release: 2 },
}).connect(delay)
synth.volume.value = -18
const notes = ['A4', 'B4', 'C#5', 'E5', 'F#5', 'A5', 'B5', 'E6']
const interval = tone.Time('4n').toSeconds()
let tick = initialTick(interval)
tone.getTransport().scheduleRepeat((time) => {
if (rng.at(tick, 0) > 0.6) {
const note = rng.pick(notes, tick, 1)
synth.triggerAttackRelease(note, '8n', time)
}
tick++
}, interval)
dispose(() => {
synth.dispose()
delay.dispose()
reverb.dispose()
})
},
visual: ({ ctx, rng, t, width, height, mouseX, mouseY, mouseActive, getRms }) => {
// Scattered bright dots like windchimes catching light. Bell hits
// (audio RMS spikes) flash random dots; the cursor reveals a soft glow.
ctx.fillStyle = 'rgba(12, 12, 12, 0.16)'
ctx.fillRect(0, 0, width, height)
const dotCount = 70
const rms = getRms()
const flash = Math.min(1, rms * 8)
for (let i = 0; i < dotCount; i++) {
// Stable position from rng (constant per dot, derived from index)
const x = rng.range(0, width, 0, i + 1)
const y = rng.range(0, height, 0, i + 100)
// Slow individual breathing
const breath = 0.3 + 0.3 * Math.sin(t * (0.4 + (i % 5) * 0.1) + i)
// Audio-reactive: each dot has chance to flash bright on a peak
const flashBoost = rng.at(Math.floor(t * 2), i + 200) > 0.7 ? flash : 0
const a = breath + flashBoost * 0.5
const r = 2 + breath * 2 + flashBoost * 4
ctx.fillStyle = `rgba(255, 240, 200, ${Math.min(1, a * 0.5)})`
ctx.beginPath()
ctx.arc(x, y, r, 0, Math.PI * 2)
ctx.fill()
}
// Cursor reveals a warm halo
if (mouseActive) {
const cx = mouseX * width
const cy = mouseY * height
const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 120)
grad.addColorStop(0, 'rgba(255, 230, 180, 0.18)')
grad.addColorStop(1, 'rgba(255, 230, 180, 0)')
ctx.fillStyle = grad
ctx.fillRect(cx - 120, cy - 120, 240, 240)
}
},
}
export default morningBells