Documentation menu

Architecture

This page describes the Pue JS runtime from the moment a scroll event is observed to the moment an emission reaches the audio output. Understanding the pipeline is not required to use the library, but it will help you make informed tuning decisions.

TypeScript
// The emission pipeline, conceptuallyscroll → sample → kinematics → tonnage → odor → voice → destination

Emitters

An emitter is the unit of composition in Pue JS. It owns:

  • a binding to one scroll container (window or an Element),
  • an AudioContext, either private or shared through the audioContext option,
  • a resolved acoustic profile,
  • a lifecycle state.

Multiple emitters can coexist on a page — for instance, one per scrollable panel. When they share an AudioContext, each odor is decoded once and their output is mixed by the browser.

Lifecycle

StateMeaning
idleCreated or stopped. No listeners are attached.
suspendedObserving, but the AudioContext is waiting for a user gesture.
runningObserving and emitting.
destroyedAll listeners and audio nodes released. Further calls throw E_DESTROYED.

Transitions are reported through the statechange event.

Scroll sampling

Pue JS never performs work inside the scroll event itself. A passive listener flags the container as dirty; the position is read once per animation frame. The frame loop stops by itself 150 ms after the last movement.

Velocity

Instantaneous velocity is expressed in pixels per second and smoothed with a low-pass filter to remove trackpad jitter. Scrolls below the threshold (80 px/s by default) are considered incidental and produce no emission. Frames without movement never emit.

Throttling

The throttle option controls how frequently emissions may be triggered:

StrategyBehaviour
"velocity"The minimum interval shortens from 260 ms to 60 ms as tonnage increases. Default.
"distance"One emission every half viewport height scrolled.
numberA fixed minimum interval, in milliseconds.

Tonnage

Tonnage is the perceived intensity of an emission, normalized between 0 and 1. Velocity is first normalized between the threshold and 3000 px/s, then passed through a tonnage curve. Tonnage drives gain and odor selection.

Three built-in curves are provided — "linear", "logarithmic" (default) and "exponential". The logarithmic curve matches the human perception of loudness and keeps slow reading discreet while preserving headroom for aggressive flicks.

Custom curves are plain functions of the velocity:

TypeScript
import { createEmitter, defineTonnageCurve } from "@puejs/core"; // Silent below 200 px/s, saturates at 3000 px/sconst gated = defineTonnageCurve((velocity) => {  if (velocity < 200) return 0;  return Math.min(1, (velocity - 200) / 2800);}); createEmitter(window, { tonnage: gated });

Acoustic profiles

An acoustic profile is the full set of parameters that shape an emission: odors, pitch range, resonance, duration, volume and tonnage curve. The presets export provides four calibrated profiles:

PresetCharacter
presets.organicBalanced, warm, moderate resonance. The default.
presets.crispShort, bright transients for dense, content-heavy layouts.
presets.deepLow pitch, long body. Suited to immersive storytelling.
presets.discreetReduced gain and duration for productivity interfaces.

Options passed alongside preset are merged over it, so a preset can be refined without being redefined.

Odors

Odors are the samples an emitter plays. Built-in odors live in their own ES modules under @puejs/core/odors and inline their audio, so there is nothing to host.

TypeScript
import { createEmitter } from "@puejs/core";import { staccato, sustained } from "@puejs/core/odors"; createEmitter(window, { odors: [staccato, sustained] });

When several odors are available, the runtime selects one per emission based on tonnage. Where ranges overlap, the choice is drawn from the emitter's seeded random source.

OdorSize (gzip)DurationTonnage rangeProfileIn presets
staccato2.1 kB0.31 s0 – 0.6Short and dry.Yes
sustained1.7 kB0.21 s0.4 – 1Rounder, with more body.Yes
soprano2.3 kB0.37 s0.3 – 0.8High-pitched, airy release.No
pesante4.6 kB0.94 s0.5 – 1Heavy and dense, with a viscous texture.No
sforzando4.9 kB1.01 s0.7 – 1Sudden, hard accent.No
fortissimo10.9 kB2.52 s0.85 – 1Long and powerful. Reserved for the fastest flicks.No
cathedral9.3 kB2.15 s0.9 – 1Loud, with a long reverberant decay.No

Every odor is trimmed and peak-normalized at build time, so emissions start sounding immediately.

soprano, pesante, sforzando, fortissimo and cathedral are expressive odors: they are not referenced by any preset, so they only reach your bundle when you import them.

TypeScript
import { createEmitter } from "@puejs/core";import { cathedral, fortissimo, pesante, sforzando, soprano, staccato, sustained } from "@puejs/core/odors"; createEmitter(window, {  odors: [staccato, sustained, soprano, pesante, sforzando, fortissimo, cathedral],  // fortissimo lasts 2.5 s: raise the duration so it is not cut short.  duration: 2600,});

duration caps every emission. Short odors end naturally before the cap; long ones are faded out when they reach it.

Your own samples can be used with defineOdor.

Voices

Each emission is a short Web Audio graph: the sample's playback rate sets the pitch, a peaking filter around 180 Hz adds resonance, and a gain node applies the tonnage-scaled volume with a 60 ms fade-out ending at duration. At most six voices play at once; the oldest is stopped when the limit is reached.

Scheduling

Motion is detected and the emission scheduled within the same animation frame. Playback starts 5 ms later on the Web Audio clock, which absorbs scheduling jitter without being perceptible. Each emit event exposes the measured latency between detection and scheduling, so you can verify it in your own telemetry.