leach.studio

Morning Bells

bells · arpeggiated · bright
↓ source · seed

about this piece

Sparse FM bell plucks over A major pentatonic. Delays and a wash of reverb.

seed
morning-bells-preview
by
stuart
year
2026
tags
bells · arpeggiated · bright
permalink
/a/morning-bells/morning-bells-preview

source

import 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
← leach.studio