Anti examples Plain DOM Click anywhere to arm sound
example / plain DOM

No framework, no build, no assets.

Five blocks cover the whole grammar. Every sound is synthesized live from one theme object: nothing is fetched, and no two presses render the same waveform.

01

Bind it and forget it

One call wires every element with a data-anti attribute. For a button whose meaning is in the markup, that is the whole integration: no handlers, no imports at the call site.

The disabled button is silent: nothing happened. A control that refuses out loud is a different case, a locked one.

import Anti from 'anti-tapes'

Anti.bind()   // every [data-anti], wired

<button data-anti="commit">Save</button>
<button data-anti="reject">Cancel</button>
<button data-anti="tick">Next</button>
02

A mechanism gets a pair

A switch is discrete, so it gets the one-shot pair: grab on the press, a verdict on the landing. Keys, checkboxes, and segmented controls take the same shape.

Off

sw.addEventListener('pointerdown', () => {
  Anti.play('grab')          // taken hold of
})

sw.addEventListener('click', () => {
  const on = toggle()
  // It landed. Off is a landing too, and a
  // quieter one: it resolves lower.
  Anti.play('commit', { value: on ? 0.7 : 0.35 })
})
03

A drag is one gesture, so it is one sound

Continuous change is a held drone that glides with the value. Its onset is the grab and its decay is the release: a drag starts with drone.start alone and ends with drone.stop alone. A pluck layered at either end reads as a second event.

Level 0.50

Drag it, then tab to it and use the arrow keys. A keyboard steps rather than drags, so each notch plucks instead of holding a voice.

lvl.addEventListener('pointerdown', () => {
  dragging = true
  Anti.drone.start('level', norm())   // the grab
})

lvl.addEventListener('input', () => {
  if (dragging) {
    Anti.drone.move('level', norm())  // glides
  } else if (performance.now() - last > 40) {
    last = performance.now()          // keyboard: a
    Anti.play('grab', { value: norm() })  // pluck per notch
  }
})

// pointerup, and pointercancel, which is the one
// every implementation forgets and then leaks a
// held voice on.
const end = () => {
  if (!dragging) return
  dragging = false
  Anti.drone.stop('level')            // the release
}
window.addEventListener('pointerup', end)
window.addEventListener('pointercancel', end)
window.addEventListener('blur', end)
04

Loading that resolves when it lands

A progress run arrives exactly at 100%: the lead climbs the ladder, a bass walks the quarters, and a reference tone beats slower as it closes. done resolves it; stop abandons it, the sound of a cancel.

Stateidle

Indeterminate: hold rocks between two rungs with no reference tone, since this state promises no resolution.

Anti.progress.start('upload', 0)

// as the bytes land
Anti.progress.set('upload', 0.4)

// no ETA to give
Anti.progress.hold('upload')

Anti.progress.done('upload')   // resolves
Anti.progress.stop('upload')   // abandons
05

The tier that speaks without being asked

Each notification's shape is its meaning. Success ascends and resolves. Info is a calm consonant ping. Warning oscillates and ends unresolved, on a faint beat. Error falls into a beating cluster, the one place for a charged waveform.

Fire them in a row. The repetition governor thins fast repeats of one event, recovering over about a second of quiet.

Anti.play('success')
Anti.play('info')
Anti.play('warning')
Anti.play('error')

// The sight and the sound name the same tier,
// so they cannot drift apart.
toast(tier, message)