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.
The built package was not found. Run npm run build (or ./serve.sh, which builds too) and reload.
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>
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 })
})
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.
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)
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.
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
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)