Package Exports
- onwave
- onwave/react
Readme
onwave
onwave is to onclick what a wave is to a tap.
Zero-dependency, no-ML wave gesture for the browser. Swipe your hand past the camera — that's the click.
Install
npm install onwaveQuick start
Vanilla JS
import { createWaveDetector } from 'onwave'
const detector = createWaveDetector({
onWave: () => timer.toggle(),
})
// must be called inside a user gesture (e.g. button click)
await detector.start()React
import { useOnWave } from 'onwave/react'
function BrewTimer() {
const { ref, state } = useOnWave(() => timer.toggle())
return <button ref={ref}>Wave to start — {state}</button>
}How it works
onwave compares consecutive camera frames on a small offscreen canvas — no ML, just pixel math. The default algorithm (Eclipse) computes the mean absolute difference (MAD) between frames and its spatial spread across vertical strips of the frame. A hand swiping past the camera produces a sharp, spatially non-uniform MAD spike that neither gradual lighting changes nor AGC adjustments can replicate. When the spike clears both thresholds, the wave callback fires.
Calibration runs for ~700ms on startup to measure the ambient noise floor and set thresholds relative to it, so the detector adapts automatically to different environments (phone on a stand, flat on a table, screen on or dimmed).
API
createWaveDetector(options, algorithm?)
| Option | Type | Default | Description |
|---|---|---|---|
onWave |
() => void |
required | Fires once per wave |
cooldown |
number |
1000 |
ms before next wave accepted |
sampleInterval |
number |
35 |
ms between samples |
facingMode |
'user' | 'environment' |
'user' |
Camera to use |
onError |
(error: Error) => void |
— | Camera permission denied etc. |
onStateChange |
(state: DetectorState) => void |
— | State change callback |
onDiagnostics |
(data: Record<string, number>) => void |
— | Per-tick signal values |
The optional algorithm argument accepts any DetectionAlgorithm. Defaults to createEclipseAlgorithm().
Returns { start, stop, calibrate, state }.
createEclipseAlgorithm(options?) — default
Frame-differencing detector. Fires on any directional swipe through the camera. Works on a stand or flat on a table without per-device tuning.
| Option | Type | Default | Description |
|---|---|---|---|
motionThreshold |
number |
auto | Min frame MAD to qualify as a swipe. Auto-calibrated from ambient noise if not set. |
spatialThreshold |
number |
auto | Min MAD spread across strips. Auto-calibrated if not set. |
canvasSize |
number |
16 |
Canvas width/height in px |
numStrips |
number |
5 |
Vertical strips for spatial spread check |
calibrationFrames |
number |
20 |
Frames sampled at startup to measure noise floor |
createLumaAlgorithm(options?) — alternative
Original brightness-based detector. Fires when average frame luma drops below a calibrated baseline. Simpler but sensitive to AGC and lighting drift.
| Option | Type | Default | Description |
|---|---|---|---|
threshold |
number |
40 |
Luma drop (0–255) to trigger |
sampleSize |
number |
10 |
Canvas size in px |
calibrationFrames |
number |
10 |
Frames to establish baseline |
useOnWave(onWave, options?)
React hook. Starts detector on mount, stops on unmount. Returns { ref, state, calibrate }.
Privacy
onwave processes all camera data locally in the browser. No frames, pixels, or any derived data are ever sent to a server or third party. The camera stream is released immediately when detector.stop() is called.
Browser support
Requires getUserMedia (all modern browsers) and HTTPS in production. On iOS, start() must be called inside a user gesture handler.
License
MIT