API Reference
This page documents every public export of Pue JS. All signatures are written in TypeScript; the same API is available from plain JavaScript.
Core
createEmitter()
Creates an emitter bound to a scroll container.
function createEmitter(target: Window | Element, options?: EmitterOptions): Emitter;| Parameter | Type | Description |
|---|---|---|
target | Window | Element | The scroll container to observe. |
options | EmitterOptions | Optional configuration. Merged over the selected preset. |
Returns an Emitter in the idle state. Invalid options throw E_INVALID_OPTION. On the server, or without Web Audio, the returned emitter is inert: its methods resolve immediately and it never emits.
import { createEmitter } from "@puejs/core"; const emitter = createEmitter(document.querySelector("#feed")!, { preset: "crisp" });EmitterOptions
| Option | Type | Default | Description |
|---|---|---|---|
preset | PresetName | AcousticProfile | "organic" | Base acoustic profile. Other options are merged over it. |
odors | Odor[] | preset odors | Samples available to the emitter. See odors. |
pitch | number | PitchRange | preset | Playback rate. A range is sampled per emission. |
resonance | number | preset | Low-frequency body, from 0 to 1. |
duration | number | preset | Maximum emission length in milliseconds, ending with a fade. |
volume | number | preset | Master gain, from 0 to 1. |
tonnage | TonnageCurveName | TonnageCurve | "logarithmic" | Maps velocity to tonnage. |
threshold | number | 80 | Minimum velocity, in px/s, required to emit. |
throttle | "velocity" | "distance" | number | "velocity" | Emission rate strategy, or a fixed interval in milliseconds. |
respectReducedMotion | boolean | true | Stay silent when the user prefers reduced motion. |
autoUnlock | boolean | true | Resume a suspended emitter on the next user gesture. |
audioContext | AudioContext | private context | Share an existing context between emitters. |
Emitter
interface Emitter { readonly state: EmitterState; readonly context: AudioContext | null; readonly options: ResolvedOptions; start(): Promise<void>; stop(): void; update(options: Partial<EmitterOptions>): void; emit(input?: EmissionInput): void; on<E extends keyof EmitterEvents>(event: E, handler: (payload: EmitterEvents[E]) => void): () => void; off<E extends keyof EmitterEvents>(event: E, handler: (payload: EmitterEvents[E]) => void): void; destroy(): void;}start()
Creates the audio context if needed, decodes the odors, starts observing the scroll container and resumes audio. Resolves once the emitter is running, or suspended when no user gesture is available. Calling start() on a started emitter is a no-op.
stop()
Stops observing. In-flight emissions finish naturally. The emitter returns to idle and can be started again.
update()
Merges new options without interrupting observation. Invalid options throw E_INVALID_OPTION and leave the current options untouched. audioContext cannot change after start().
emitter.update({ preset: "deep", volume: 0.5 });emit()
Triggers an emission manually, independently of scroll. Ignored unless the emitter is running.
| Field | Type | Default | Description |
|---|---|---|---|
tonnage | number | 0.5 | Intensity of the emission, from 0 to 1. |
odor | Odor | auto | Force a specific odor instead of automatic selection. |
on() and off()
Subscribes to and unsubscribes from emitter events. on() returns an unsubscribe function.
const unsubscribe = emitter.on("emit", ({ tonnage, odor }) => { console.debug(`emission · ${odor} · tonnage=${tonnage.toFixed(2)}`);});destroy()
Removes every listener, stops every voice and closes the private audio context, if any. Any later call throws E_DESTROYED, except destroy() and off(), which are safe to repeat.
Events
interface EmitterEvents { emit: EmissionEvent; statechange: { previous: EmitterState; current: EmitterState }; unlock: { context: AudioContext }; error: PueError;}EmissionEvent
| Field | Type | Description |
|---|---|---|
tonnage | number | Computed tonnage, from 0 to 1. |
velocity | number | Smoothed scroll velocity at trigger time, in px/s. |
direction | 1 | -1 | 1 when scrolling forward, -1 when scrolling backward. |
pitch | number | Effective playback rate. |
odor | string | Name of the odor that produced the emission. |
latency | number | Time from motion detection to scheduling, in ms. |
timestamp | number | AudioContext.currentTime at which the emission starts. |
Presets
const presets: Readonly<Record<PresetName, AcousticProfile>>;| Name | Pitch | Resonance | Duration | Volume | Odors |
|---|---|---|---|---|---|
organic | 0.90 – 1.10 | 0.40 | 320 ms | 0.8 | staccato, sustained |
crisp | 1.10 – 1.35 | 0.20 | 180 ms | 0.8 | staccato |
deep | 0.60 – 0.80 | 0.70 | 620 ms | 0.8 | sustained |
discreet | 0.95 – 1.05 | 0.25 | 220 ms | 0.4 | staccato |
Presets are frozen objects. Spread them to derive your own:
import { presets, type AcousticProfile } from "@puejs/core"; export const brand: AcousticProfile = { ...presets.organic, resonance: 0.55, duration: 360 };Tonnage curves
defineTonnageCurve()
function defineTonnageCurve(curve: (velocity: number) => number): TonnageCurve;The function receives the smoothed velocity in px/s and must return a value between 0 and 1; other values are clamped. It runs once per emitting frame: keep it pure and allocation-free.
Odors
import { cathedral, fortissimo, pesante, sforzando, soprano, staccato, sustained } from "@puejs/core/odors";| Odor | Tonnage range | Duration | In presets |
|---|---|---|---|
staccato | 0 – 0.6 | 0.31 s | Yes |
sustained | 0.4 – 1 | 0.21 s | Yes |
soprano | 0.3 – 0.8 | 0.37 s | No |
pesante | 0.5 – 1 | 0.94 s | No |
sforzando | 0.7 – 1 | 1.01 s | No |
fortissimo | 0.85 – 1 | 2.52 s | No |
cathedral | 0.9 – 1 | 2.15 s | No |
Odors longer than the emitter's duration are faded out when they reach it.
defineOdor()
Declares an odor from your own sample.
function defineOdor(definition: { name: string; src: string; tonnage?: [number, number] }): Odor;| Field | Type | Description |
|---|---|---|
name | string | Identifier reported in EmissionEvent.odor. |
src | string | URL or base64 data URI of an audio file the browser can decode. |
tonnage | [number, number] | Tonnage range for which the odor is eligible. Default [0, 1]. |
URL odors are fetched and decoded on start().
Utilities
isSupported()
function isSupported(): boolean;Returns true when the Web Audio API is available. Always false on the server.
unlock()
function unlock(context?: AudioContext): Promise<boolean>;Resumes the given audio context — or every context created by Pue JS — from within a user gesture. Resolves with true if playback is now allowed.
Types
type PresetName = "organic" | "crisp" | "deep" | "discreet"; type EmitterState = "idle" | "suspended" | "running" | "destroyed"; type TonnageCurveName = "linear" | "logarithmic" | "exponential"; type ThrottleStrategy = "velocity" | "distance" | number; interface PitchRange { min: number; max: number;} interface AcousticProfile { odors: readonly Odor[]; pitch: number | PitchRange; resonance: number; duration: number; volume: number; tonnage: TonnageCurveName | TonnageCurve;}Errors
Every error thrown or dispatched by Pue JS is a PueError with a stable code.
class PueError extends Error { readonly code: "E_DESTROYED" | "E_INVALID_OPTION" | "E_DECODE";}| Code | When |
|---|---|
E_DESTROYED | A method is called on a destroyed emitter. |
E_INVALID_OPTION | An option is out of range, or immutable after start(). |
E_DECODE | An odor could not be fetched or decoded. Dispatched through the error event; the emitter keeps running with its other odors. |
React adapter
Available from @puejs/react. See Framework Adapters for usage.
usePue()
function usePue<T extends Element = Element>( options?: EmitterOptions | false, config?: { autoStart?: boolean },): { ref: (element: T | null) => void; state: EmitterState; start: () => Promise<void>; stop: () => void; emit: (input?: EmissionInput) => void; emitter: Emitter | null;};Options are compared by value: pitch ranges structurally, odor lists element by element, everything else by identity. Removing an option reverts it to its default. Inline functions, profiles and odor lists built during render are compared by identity and applied on every render: define them outside the component to avoid needless updates. Changing the shared audioContext recreates the emitter.
Without ref, the hook observes window. Once ref has been attached, removing the element disables the emitter until a new element is attached. On the server and while disabled, emitter is null, state is "idle", and start, stop and emit are safe no-ops with stable identities.
PueProvider
| Prop | Type | Description |
|---|---|---|
defaults | EmitterOptions | Merged under the options of every usePue call in the subtree. |
audioContext | AudioContext | Shared by every emitter in the subtree. Never closed. |