# Cereb - Complete Documentation for AI > Lightweight reactive stream library for user input modeling and orchestration. > From low-level events (touch, pointer, keyboard, wheel) to high-level gestures (pan, pinch)—all through composable stream pipelines. --- ## Overview Cereb provides a unified abstraction for handling pointer/touch/mouse events through an Observable-based pattern, enabling composable gesture recognition pipelines. ### Key Benefits - **Unified Input**: Mouse, touch, pen normalized into single API - **Composable**: Build pipelines with operators like filter, map, throttle - **Lightweight**: ~1.7KB gzipped for core + pan gesture (77% smaller than Hammer.js) - **Type-safe**: Full TypeScript support with inference ### Package Structure | Package | NPM | Description | |---------|-----|-------------| | `cereb` | cereb | Core: Stream primitives, operators, and gestures (pan, pinch, tap) | ### Installation ```bash npm install cereb ``` --- ## Core Concepts ### Signal An immutable data object representing a discrete event. ```typescript interface Signal { readonly kind: K; // Type discriminator (e.g., "single-pointer", "pan") readonly value: V; // Event payload readonly deviceId: string; // Unique device identifier readonly createdAt: number; // Timestamp (performance.now()) } ``` Signals are readonly by design—prevents side-effects, enables safe composition. ### Stream An observable sequence of Signals with built-in flow control. ```typescript interface Stream { on(observer: (value: T) => void): Unsubscribe; pipe(...operators: Operator[]): Stream; block(): void; unblock(): void; readonly isBlocked: boolean; } ``` - **Lazy**: No work until `on()` is called - **Unicast by default**: Each subscription creates its own source. Use `share()` for multicast. ### Operator A function that transforms one Stream into another. ```typescript type Operator = (source: Stream) => Stream; ``` --- ## Creating Custom Operators ### Basic Structure ```typescript import { createStream } from "cereb"; import type { Signal, Operator } from "cereb"; function myOperator(): Operator { return (source) => createStream((observer) => { return source.on({ next(signal) { observer.next(signal); }, error: observer.error?.bind(observer), complete: observer.complete?.bind(observer), }); }); } ``` ### Side-Effect Operators (like spy) ```typescript function log(label: string): Operator { return (source) => createStream((observer) => { return source.on({ next(signal) { console.log(label, signal.value); observer.next(signal); }, error: observer.error?.bind(observer), complete: observer.complete?.bind(observer), }); }); } ``` ### Value-Extending Operators (like extend) ```typescript function addTimestamp(): Operator { return (source) => createStream((observer) => { return source.on({ next(signal) { (signal.value as any).timestamp = Date.now(); observer.next(signal); }, error: observer.error?.bind(observer), complete: observer.complete?.bind(observer), }); }); } ``` ### Flow-Control Operators (like when, merge) ```typescript function gate( controller: Stream> ): Operator { return (source) => createStream((observer) => { let isActive = false; const controllerUnsub = controller.on({ next(signal) { isActive = signal.value.active; }, }); const sourceUnsub = source.on({ next(signal) { if (isActive) observer.next(signal); }, error: observer.error?.bind(observer), complete: observer.complete?.bind(observer), }); return () => { controllerUnsub(); sourceUnsub(); }; }); } ``` --- ## Stream API Reference ### singlePointer Unified pointer stream. Normalizes mouse, touch, pen into single interface. Tracks only primary pointer. ```typescript function singlePointer(target: EventTarget, options?: SinglePointerOptions): Stream ``` **Signal Value:** - `phase`: "start" | "move" | "end" | "cancel" - `x`, `y`: clientX/Y - `pageX`, `pageY`: pageX/Y - `pointerType`: "mouse" | "touch" | "pen" | "unknown" - `button`: "none" | "primary" | "secondary" | ... - `pressure`: 0.0-1.0 ```typescript import { singlePointer } from "cereb"; singlePointer(element).on((signal) => { const { phase, x, y, pointerType } = signal.value; }); ``` ### multiPointers Multi-touch stream. Tracks multiple simultaneous pointers. ```typescript function multiPointers(target: EventTarget, options?: { maxPointers?: number }): Stream ``` **Signal Value:** - `phase`: "idle" | "active" | "ended" - `pointers`: readonly PointerInfo[] (each has id, phase, x, y, etc.) - `count`: number of active pointers ```typescript import { multiPointers } from "cereb"; multiPointers(element, { maxPointers: 2 }).on((signal) => { if (signal.value.count === 2) { const [p1, p2] = signal.value.pointers; } }); ``` ### pan Pan gesture recognition with velocity, direction, distance tracking. ```typescript function pan(target: EventTarget, options?: { threshold?: number; direction?: "horizontal" | "vertical" | "all" }): Stream ``` **Signal Value:** - `phase`: "start" | "move" | "end" | "cancel" - `delta`: [number, number] - displacement from start [deltaX, deltaY] (px) - `velocity`: [number, number] - [velocityX, velocityY] (px/ms) - `cursor`: [number, number] - current position (client coordinates) - `pageCursor`: [number, number] - current position (page coordinates) - `distance`: cumulative distance (px) - `direction`: "up" | "down" | "left" | "right" | "none" ```typescript import { pan } from "cereb"; pan(element).on((signal) => { const { phase, delta } = signal.value; const [deltaX, deltaY] = delta; if (phase === "move") { element.style.transform = `translate(${deltaX}px, ${deltaY}px)`; } }); ``` **axisLock operator:** ```typescript import { pan } from "cereb"; import { axisLock } from "cereb/operators"; pan(element).pipe(axisLock()).on(handlePan); ``` ### pinch Two-finger pinch gesture with distance, ratio, velocity, center tracking. ```typescript function pinch(target: EventTarget, options?: { threshold?: number }): Stream ``` **Signal Value:** - `phase`: "start" | "change" | "end" | "cancel" - `initialDistance`: distance at start (px) - `distance`: current distance (px) - `ratio`: distance / initialDistance - `deltaDistance`: change since last event (px) - `velocity`: px/ms - `center`: [number, number] - center between pointers (client coordinates) - `pageCenter`: [number, number] - center between pointers (page coordinates) ```typescript import { pinch } from "cereb"; pinch(element).on((signal) => { const { phase, ratio, center } = signal.value; const [centerX, centerY] = center; if (phase === "change") { element.style.transform = `scale(${ratio})`; } }); ``` ### tap Tap gesture recognition with multi-tap support. ```typescript function tap(target: EventTarget, options?: { movementThreshold?: number; durationThreshold?: number; chainMovementThreshold?: number; chainIntervalThreshold?: number; }): Stream ``` **Signal Value:** - `phase`: "start" | "end" | "cancel" - `cursor`: [number, number] - tap position (client coordinates) - `pageCursor`: [number, number] - tap position (page coordinates) - `tapCount`: number - consecutive tap count (1=single, 2=double, etc.) - `duration`: number - how long the pointer was pressed (ms) - `pointerType`: "mouse" | "touch" | "pen" | "unknown" ```typescript import { tap } from "cereb"; tap(element).on((signal) => { const { tapCount, cursor } = signal.value; const [x, y] = cursor; if (tapCount === 2) { console.log(`Double tap at (${x}, ${y})`); } }); ``` ### keyboard Both keydown and keyup events. ```typescript function keyboard(target: EventTarget, options?: { code?: KeyCode | KeyCode[]; modifiers?: ModifierKey[]; preventDefault?: boolean; allowRepeat?: boolean; }): Stream ``` **Signal Value:** - `phase`: "down" | "up" - `key`: logical key value - `code`: physical key code (e.g., "KeyA", "Equal") - `repeat`: boolean - `altKey`, `ctrlKey`, `metaKey`, `shiftKey`: boolean - `originalEvent`: KeyboardEvent ```typescript import { keyboard } from "cereb"; keyboard(window, { code: ["Equal", "Minus"] }).on((signal) => { // + or - pressed }); ``` ### keydown Only keydown events. Same options and signal as keyboard. ```typescript import { keydown } from "cereb"; keydown(window, { code: "Escape" }).on(handleEscape); ``` ### keyheld Track if a key is held down. Emits only on state change. ```typescript function keyheld(target: EventTarget, options: { code: KeyCode }): Stream ``` **Signal Value:** - `held`: boolean ```typescript import { keyheld } from "cereb"; keyheld(window, { code: "Space" }).on((signal) => { if (signal.value.held) { console.log("Space held"); } }); ``` ### wheel Wheel/scroll events. ```typescript function wheel(target: EventTarget, options?: { passive?: boolean; modifiers?: ModifierKey[]; preventDefault?: boolean; }): Stream ``` **Signal Value:** - `deltaX`, `deltaY`, `deltaZ` - `deltaMode`: "pixel" | "line" | "page" - `x`, `y`, `pageX`, `pageY` - `altKey`, `ctrlKey`, `metaKey`, `shiftKey` - `originalEvent`: WheelEvent ```typescript import { wheel } from "cereb"; wheel(element, { passive: false }).on((signal) => { const { deltaY, x, y } = signal.value; }); ``` ### dom Low-level DOM event wrapper with full TypeScript inference. ```typescript function dom(target: EventTarget, eventName: string, options?: AddEventListenerOptions): Stream> ``` ```typescript import { dom } from "cereb"; dom(window, "resize").on((signal) => { // signal.value is UIEvent }); dom(element, "click").on((signal) => { // signal.value is MouseEvent }); ``` **Helper functions:** - `pointerEvents(target)` - pointerdown/move/up/cancel merged - `mouseEvents(target)` - mousedown/move/up merged - `touchEvents(target)` - touchstart/move/end/cancel merged --- ## Operator API Reference All operators imported from `cereb/operators`. ### filter Filter signals by predicate. ```typescript function filter(predicate: (signal: T) => boolean): Operator ``` ```typescript pan(element) .pipe(filter((s) => s.value.phase !== "cancel")) .on(handlePan); ``` ### map Transform each signal. ```typescript function map(transform: (signal: T) => R): Operator ``` ### extend Add properties to signal value. Most common way to extend signals. ```typescript function extend(extender: (signal: T) => A): Operator> ``` ```typescript wheel(element) .pipe(extend((s) => ({ ratio: Math.exp(-s.value.deltaY * 0.005) }))) .on((s) => setScale(scale * s.value.ratio)); ``` ### session / singlePointerSession / multiPointersSession Filter signals to active sessions only. ```typescript function session(options: { start: (s: T) => boolean; end: (s: T) => boolean }): Operator function singlePointerSession(): Operator function multiPointersSession(requiredCount: number): Operator ``` ```typescript singlePointer(element) .pipe(singlePointerSession()) .on(draw); // Only from start to end/cancel ``` ### offset Add element-relative coordinates (offsetX, offsetY). ```typescript function offset>(options: { target: Element; recalculate$?: Stream; }): Operator ``` ```typescript singlePointer(canvas) .pipe(offset({ target: canvas })) .on((s) => draw(s.value.offsetX, s.value.offsetY)); ``` ### zoom Convert ratio input to frame-by-frame scale delta. Consumer accumulates and clamps. ```typescript function zoom>( options?: ZoomOptions ): Operator ``` **Output:** - `scale`: Frame-by-frame scale delta (not absolute scale) - `deltaScale`: Same as scale (deprecated) ```typescript let scale = 1.0; const MIN_SCALE = 0.5, MAX_SCALE = 3.0; pinch(element) .pipe(zoom()) .on((s) => { scale += s.value.scale; scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale)); element.style.transform = `scale(${scale})`; }); ``` ### rotate3d Convert 2D pan to 3D rotation delta. Horizontal pan → Y-axis rotation, vertical pan → X-axis rotation. ```typescript function rotate3d>(options?: { sensitivityX?: number; // default 1.0 sensitivityY?: number; // default 1.0 invertX?: boolean; // default false invertY?: boolean; // default false }): Operator ``` **Output:** - `rotation`: Frame-by-frame rotation delta in radians `[rx, ry, rz]` ```typescript let rotation = [0, 0, 0]; pan(element) .pipe(rotate3d({ sensitivityX: 0.5, sensitivityY: 0.5 })) .on((s) => { const [drx, dry, drz] = s.value.rotation; rotation[0] += drx; rotation[1] += dry; element.style.transform = `rotateX(${rotation[0]}rad) rotateY(${rotation[1]}rad)`; }); ``` ### translate Convert pan delta to 2D translation coordinates. ```typescript function translate>(options?: { baseTranslate?: [number, number] | (() => [number, number]); // default [0, 0] sensitivity?: number; // default 1.0 }): Operator ``` ```typescript pan(element) .pipe(translate()) .on((s) => { const [x, y] = s.value.translate; element.style.transform = `translate(${x}px, ${y}px)`; }); ``` ### when Gate signals based on another stream's state. ```typescript function when(gate: Stream>): Operator ``` ```typescript const zoomMode$ = keyheld(window, { code: "KeyZ" }) .pipe(extend((s) => ({ opened: s.value.held }))); wheel(element) .pipe(when(zoomMode$)) .on(handleZoomWheel); ``` ### throttle / throttleLast Rate limiting. ```typescript function throttle(ms: number): Operator // Emit first, ignore rest function throttleLast(ms: number): Operator // Collect, emit last ``` ```typescript singlePointer(element) .pipe(throttle(16)) // ~60fps .on(updatePosition); ``` ### debounce Wait for silence before emitting. ```typescript function debounce(ms: number): Operator ``` ```typescript keyboard(window) .pipe(debounce(300)) .on(() => performSearch()); ``` ### spy / tap Execute side effect without modifying stream. ```typescript function spy(fn: (signal: T) => void): Operator ``` ```typescript wheel(element, { passive: false }) .pipe(spy((s) => s.value.originalEvent.preventDefault())) .on(handleWheel); ``` ### share / shareReplay Multicast to multiple subscribers. ```typescript function share(): Operator function shareReplay(bufferSize?: number): Operator ``` ```typescript const keyboard$ = keyboard(window).pipe(share()); keyboard$.on(handler1); keyboard$.on(handler2); // Same underlying listener ``` ### merge / mergeWith Combine multiple streams. ```typescript function merge(...sources: Stream[]): Stream function mergeWith(other: Stream): Operator ``` ```typescript merge(pinchZoom$, wheelZoom$, keyboardZoom$).on(applyScale); ``` ### compose Combine operators into reusable pipeline. ```typescript function compose(op1: Operator, op2: Operator): Operator ``` ```typescript const drawingPipeline = compose( singlePointerSession(), offset({ target: canvas }) ); singlePointer(canvas).pipe(drawingPipeline).on(draw); ``` ### reduce Accumulate state across signals. ```typescript function reduce( reducer: (acc: A, signal: T) => A, seed?: A ): Operator> ``` ```typescript singlePointer(element) .pipe(reduce((acc, s) => ({ count: acc.count + 1 }), { count: 0 })) .on((s) => console.log(s.value.count)); ``` --- ## Common Patterns ### Multi-input Zoom ```typescript import { wheel, keyheld, keydown, pinch } from "cereb"; import { when, extend, spy, zoom } from "cereb/operators"; let scale = 1; const MIN_SCALE = 0.5, MAX_SCALE = 3.0; const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); const zoomMode$ = keyheld(window, { code: "KeyZ" }) .pipe(extend((s) => ({ opened: s.value.held }))); // Pinch zoom - uses delta-based zoom operator pinch(element) .pipe(zoom()) .on((s) => { scale = clamp(scale + s.value.scale, MIN_SCALE, MAX_SCALE); element.style.transform = `scale(${scale})`; }); // Z + wheel zoom - compute scale directly wheel(element, { passive: false }) .pipe( when(zoomMode$), spy((s) => s.value.originalEvent.preventDefault()) ) .on((s) => { const multiplier = Math.exp(-s.value.deltaY * 0.005); scale = clamp(scale * multiplier, MIN_SCALE, MAX_SCALE); element.style.transform = `scale(${scale})`; }); // Z + +/- keyboard zoom - compute scale directly keydown(window, { code: ["Equal", "Minus"] }) .pipe(when(zoomMode$)) .on((s) => { const multiplier = s.value.code === "Equal" ? 1.2 : 1 / 1.2; scale = clamp(scale * multiplier, MIN_SCALE, MAX_SCALE); element.style.transform = `scale(${scale})`; }); ``` ### Drawing Application ```typescript import { singlePointer } from "cereb"; import { singlePointerSession, offset } from "cereb/operators"; singlePointer(canvas) .pipe( singlePointerSession(), offset({ target: canvas }) ) .on((signal) => { const { phase, offsetX, offsetY } = signal.value; if (phase === "start") { ctx.beginPath(); ctx.moveTo(offsetX, offsetY); } else if (phase === "move") { ctx.lineTo(offsetX, offsetY); ctx.stroke(); } }); ``` ### Drag with Constraints ```typescript import { pan } from "cereb"; import { offset, filter } from "cereb/operators"; pan(container) .pipe( offset({ target: container }), filter((s) => s.value.offsetX >= 0 && s.value.offsetX <= maxX) ) .on((signal) => { draggable.style.left = `${signal.value.offsetX}px`; }); ``` --- ## Links - GitHub: https://github.com/devphilip21/cereb - Documentation: https://cereb.dev - npm: https://www.npmjs.com/package/cereb