leach.studio

Still Water

ambient · drone
↓ source · seed

about this piece

Slow notes drifting in D Phrygian. Sine-based poly pad through a long reverb.

seed
still-water-preview
by
stuart
year
2026
tags
ambient · drone
permalink
/a/still-water/still-water-preview

source

import type { Patch } from '@/lib/engine/types'

const stillWater: Patch = {
  id: 'still-water',
  title: 'Still Water',
  author: 'stuart',
  year: 2026,
  tags: ['ambient', 'drone'],
  description:
    'Slow notes drifting in D Phrygian. Sine-based poly pad through a long reverb.',

  audio: ({ tone, rng, initialTick, dispose }) => {
    const reverb = new tone.Reverb({ decay: 12, wet: 0.6 }).toDestination()
    const synth = new tone.PolySynth(tone.Synth, {
      oscillator: { type: 'sine' },
      envelope: { attack: 2, decay: 1, sustain: 0.6, release: 6 },
    }).connect(reverb)
    synth.volume.value = -14

    const notes = ['D3', 'Eb3', 'F3', 'G3', 'A3', 'Bb3', 'C4', 'D4', 'F4']
    const interval = tone.Time('2m').toSeconds()

    let tick = initialTick(interval)
    tone.getTransport().scheduleRepeat((time) => {
      const note = rng.pick(notes, tick, 0)
      const dur = rng.range(2, 6, tick, 1)
      synth.triggerAttackRelease(note, dur, time)
      if (rng.at(tick, 2) > 0.55) {
        const harmony = rng.pick(notes, tick, 3)
        synth.triggerAttackRelease(harmony, dur * 0.8, time + 0.3)
      }
      tick++
    }, interval)

    dispose(() => {
      synth.dispose()
      reverb.dispose()
    })
  },

  visual: ({ ctx, t, width, height, getRms, getWaveform, mouseX, mouseY, mouseActive }) => {
    // Still water surface — horizontal lines that ripple with audio,
    // with the cursor causing a localized splash.
    ctx.fillStyle = 'rgba(12, 12, 12, 0.10)'
    ctx.fillRect(0, 0, width, height)

    const wave = getWaveform()
    const rms = getRms()
    const lineCount = 26
    const cursorX = mouseActive ? mouseX * width : -1000
    const cursorY = mouseActive ? mouseY * height : -1000

    for (let i = 0; i < lineCount; i++) {
      const baseY = (height / lineCount) * (i + 0.5)
      const phase = t * (0.05 + i * 0.008) + i
      ctx.strokeStyle = `rgba(120, 160, 200, ${0.06 + (i / lineCount) * 0.16})`
      ctx.lineWidth = 1
      ctx.beginPath()
      const segments = 110
      for (let s = 0; s <= segments; s++) {
        const x = (s / segments) * width
        const sineWave = Math.sin(phase + (s / segments) * 4) * (4 + rms * 30)
        const audioRipple = wave
          ? wave[Math.floor((s / segments) * wave.length)] * 16
          : 0
        let y = baseY + sineWave + audioRipple
        // Cursor splash
        if (mouseActive) {
          const dx = x - cursorX
          const dy = y - cursorY
          const d = Math.hypot(dx, dy)
          if (d < 100) {
            const fall = 1 - d / 100
            y += dy * fall * 0.4
          }
        }
        if (s === 0) ctx.moveTo(x, y)
        else ctx.lineTo(x, y)
      }
      ctx.stroke()
    }
  },
}

export default stillWater
← leach.studio