API
Every entry point's exports, from the types. Generated from the source on each build.
@haxiomic/devui
apiVersion
the extension API's version: it changes only when an extension written for it would break
const apiVersion: 1
type AxisName
type AxisName = '+x' | '-x' | '+y' | '-y' | '+z' | '-z'
build
const build: { commit: string; date: string; } | null
interface ChangeEvent
path: stringcontrol: Control<any>value: unknownsource: ChangeSourcetransient: boolean: true while a gesture is still in progress
interface ChangeListenOptions
external: boolean | undefined: also run for the app's own writes to the property, which polling notices (default false)
type ChangeSource
Who caused a change. Only user-ish sources become persisted overrides or undo entries. external is the app's own write, seen by polling; compare is an A/B preview, timeline a timeline playing and driver an audio band or other driver moving it: shown, never saved or recorded in history.
type ChangeSource = 'user' | 'code' | 'external' | 'load' | 'preset' | 'undo' | 'compare' | 'timeline' | 'driver'
interface Codec
Native value ⇄ canonical value. Writes mutate in place where the native is an object.
N: anyC: anyid: stringread(native: N, spec: Spec): Cwrite(native: N, canonical: C, spec: Spec): N: returns the native value to store; return the same object after mutating it in place- ``: literal for "copy as code"
`: the native value as plain JSON that, assigned back field by field (or replacing a primitive), recreates it. Saved asn` so production builds can apply values without codecs. Omit for primitives and plain arrays/objects: the default handles them.- ``: the spec inferred from the value no longer fits it (a THREE.Euler's order changed): infer it again (checked when polled)
interface Command
Something the palette, menus and hotkeys can run.
id: stringlabel: stringgroup: string | undefined: shown next to the label in the palette, e.g. a folder path or an extension namekey: string | undefined: hotkey: 'r', 'shift+r', 'alt+r', 'mod+k' (⌘ on macOS, Ctrl elsewhere)keywords: string | undefined: extra words to match in the paletterun(gui: GUI): void- ``: hidden from the palette, and its hotkey inactive, while this returns false
class Compare
a: Record<string, Json> | null: the A state: values under its folder, what it has no value for there at its code default; null means the code defaultslabel: stringshowing: booleanmark(label?: string): void: make the current values AmarkPreset(scope: string, name: string): void: compare against a preset insteadclear(): void: back to comparing with the code defaultshold(): void: show A until release()release(): void: back to the values from before hold()toggle(): void
class Control
C: anykind: 'control'type: ValueType<C>codec: Codec<any, C>view: stringspec: Specexplain: stringtarget: objectkey: stringhintsGiven: Hints: user hints as given (re-resolution merges into these)codeRange: { min?: number; max?: number; }: its range as its code gives it (a range set from the panel is inspec)codeDefault: Ccached: Cdrift: { stored: Json; code: Json; } | null: stored override was saved against a different code defaulttexture: TextureViewState | undefined: texture controls onlyviewKey: string | undefined: Where how its values are shown is kept, when that belongs to what it shows rather than to the row (a capture's picture row shows one target, then another): Layout.views under this key, else the row's own (Layout.textures, by its path)captureSeq: number: manual texture capture requestsdriver: { label: string; title?: string; } | null: what drives it from outside (a MIDI knob, an audio band), shown on its rowvalue: any: the bound native value, e.g.material.roughnessreadonly: booleansettable: boolean: A value someone sets: not an action, a texture or read-only. Copy as code covers every one, whatever itspersist; that decides only what's saved, restored and kept in presetsget(): C: last known canonical valueread(): C: read the canonical value from the target nowset(value: C, source?: ChangeSource, opts?: { transient?: boolean; }): thissetDriver(driver: { label: string; title?: string; } | null, by?: string): this: Show what drives it (a MIDI knob, an audio band) on its row; null clears it. Each driver has its own slot (by), so audio and MIDI don't clear each other's; the row shows the latest.reset(source?: ChangeSource): thisisModified: boolean: the user changed this (it has an override); code driving the value doesn't countonChange(cb: ChangeCallback, opts?: ChangeListenOptions): this: Runs when the value changes: in the panel (a remote one too), fromset(), a preset, undo, A/B compare, a timeline, a driver (an audio band, MIDI) or loaded values. A saved value restored when the control was added runs it once too, so state built from it matches. The app's own writes to the property don't run it, as in lil-gui: to change a value from code and run its handlers, callset().{ external: true }includes the app's writes (event.source === 'external') as polling notices them: while the row is on screen (or a remote panel is connected), never withlisten: false; so not every write, and late for one made while it was hidden. The same function given twice is one handler.onFinishChange(cb: ChangeCallback, opts?: ChangeListenOptions): this: Runs when a change is finished: a drag released, a field committed, a driver letting go, a one-off change; and once for a restored value. AsonChange, it skips the app's own writes unless given{ external: true }. Work that's slow to start (a load, a rebuild) belongs here.hints(h: Hints): this: update hints at runtime (e.g. a new range); re-runs inferencesetRange(range: { min?: number; max?: number; } | null): this: its range (its slider's ends) as a person sets it in the panel, saved with the values;null: its code's againpress(): this: buttons: run the actioncapture(): this: texture controls with capture: 'manual': capture the current contents now
createGUI()
a new, independent GUI with the std extension registered
function createGUI(options?: GUIOptions): GUI
curve()
a curve: curve([[0, 0], [0.5, 1], [1, 0]]), curve(easing('easeInOutCubic'))
function curve(keys?: readonly KeyInput[], extrapolate?: Extrapolate): Curve
class Curve
keys: Key[]extrapolate: Extrapolateversion: number: bumps on every change: compare it to know when to rebuild a LUT or a texturelastX: number: the last x sampled, so the editor can show where an animation isset(keys: readonly KeyInput[], extrapolate?: Extrapolate): this: replace the keys (and what happens outside them); the panel calls this as you editsubscribe(fn: (c: Curve) => void): () => undefinedsample(x: number): number: the value at x (a time, or 0–1)lut(n?: number, out?: Float32Array<ArrayBuffer>): Float32Array<ArrayBuffer>: n samples across the keys' domain, for a lookup texture (outis reused when given)cubicBezier(): string: for a single eased segment from (0,0) to (1,1): its CSScubic-bezier(…)
interface CurveValue
keys: Key[]extrapolate: Extrapolate
defineExtension()
An extension, checked and stamped with the API version it was written for. Register it with gui.use(ext) (or createGUI({ extensions: [ext] })).
function defineExtension<E extends Omit<Extension, 'apiVersion'>>(def: E): E & { apiVersion: number; }
interface Drops
what a folder is, dropped into lists, and what it takes (see Hints.drops)
kind: string | undefinedaccepts: readonly string[] | undefined
type Ease
type Ease = 'linear' | 'in' | 'out' | 'inOut'
easing()
an easing as two bezier keys, from (0,0) to (1,1)
function easing(name: EasingName | readonly [number, number, number, number]): Key[]
type EasingName
type EasingName = keyof typeof easings
easings
CSS-style easings as cubic-bezier control points (easings.net's fits)
const easings: { readonly linear: readonly [0, 0, 1, 1]; readonly ease: readonly [0.25, 0.1, 0.25, 1]; readonly easeIn: readonly [0.42, 0, 1, 1]; readonly easeOut: readonly [0, 0, 0.58, 1]; readonly easeInOut: readonly [0.42, 0, 0.58,…
type EulerOrder
type EulerOrder = 'XYZ' | 'YXZ' | 'ZXY' | 'ZYX' | 'YZX' | 'XZY'
interface Extension
name: stringapiVersion: number | undefined: the extension API it was written for (defineExtension sets it)requires: string[] | undefinedcommands: Command[] | undefined`: add readouts to the Performance folder whengui.perf()` is on; return a cleanuptypes: ValueType<any>[] | undefinedmatchers: Matcher[] | undefinedinspectors: Inspector<any>[] | undefinedtextureSources: TextureSourceResolver<any>[] | undefinedlifetimes: LifetimeHook[] | undefinedviews: (RowView<any> | LazyRowView)[] | undefined: row views, by id (a control'sview), drawn by the extension- ``: items for a row's menu (right-click): learn a MIDI knob, drive it with audio…
- ``
type Extrapolate
what happens outside the keys: hold the end values, loop, go back and forth, or keep the slope
type Extrapolate = 'hold' | 'loop' | 'pingpong' | 'continue'
class Folder
kind: 'folder'children: AnyNode[]collapsed: booleandrops: Drops | undefined: what it is when its window's dragged into lists, and what it takes (itsdropshint; seedropsOf)window: 'list' | 'tool': how its window's sized: as tall as its rows (a list), or the size it's given (a tool; itswindowhint)windowSize: { w: number; h: number; } | undefined: the size its window opens at, dragged out of the panel (itswindowSizehint; seetornSize)materialised: boolean: the folder's contents exist (an inspector's folder is filled when first opened)defer(fill: (folder: this) => void, opts?: { searchable?: boolean; }): this: Fill this folder when it's first opened instead of now (now if it's open already):f.folder('objects', { collapsed: true }).defer((objs) => …).searchable: a list of things (a scene's objects) that search finds by name, so it's filled when you search.inspect(obj: object, opts?: InspectOptions): this: Fill this folder from an object: its extension's inspector (a material, a scene…) or its inferable own properties, leaving outexclude. As withadd(obj), a collapsed folder fills when first opened. gui.folder('Body', { collapsed: true }).inspect(material, { exclude: ['emissive', 'uniforms'] });materialise(): this: run a deferred inspector nowadd<T extends object, K extends KeyOf<T>>(target: T, key: K, hints?: Hints): Control · add(target: object, hints?: Hints & InspectOptions): Folder: bind one property (an array's element by its index); type, range and view are inferred a whole object: an extension inspector, or every inferable own property (excludeleaves keys out)params<S extends Record<string, unknown>>(schema: S): Params<S>: declare parameters; returns a live, typed object the UI writes intofolder(path: string, hints?: Hints): Folder: 'A/B' creates nested folders; idempotentbutton(label: string, fn: () => void, hints?: Hints): Controlmonitor<T extends object, K extends KeyOf<T>>(target: T, key: K, hints: Hints & PlotOptions & { view: 'graph'; }): Plot · monitor<T extends object, K extends KeyOf<T>>(target: T, key: K, hints?: Hints & PlotOptions & { view?: undefined; }): Control · monitor<T extends object, K extends KeyOf<T>>(target: T, key: K, hints: Hints & PlotOptions): Control | Plot: read-only, polled;view: 'graph'plots it over time (a Plot), any other view is a Control a view known only at run time: eitherplot(label: string, opts?: PlotOptions): Plottexture(label: string, source: unknown, hints?: Hints & Partial<TextureViewState>): Control: a texture view;sourceis anything an extension can resolve (raw WebGL texture, canvas, three.js texture…)find(path: string): AnyNode | undefined: find a descendant by relative path (filling deferred inspector folders on the way)open(): thisclose(): thissetCollapsed(collapsed: boolean): thiscontrols(): Generator<Control>: every control below this folder (deferred inspector folders aren't filled for this)materialiseAll(): this: fill every deferred inspector folder below this onereset(): this: reset every control below this folder to its code default (one undo step)toCode(opts?: { changedOnly?: boolean; }): string: Every value below this folder, as a pasteable object literal (filling it, if it's an inspector's not opened yet).changedOnly: only the values you changed, to paste over what made them (a material's 40-odd properties)presets: { save: (name?: string) => import("./presets").Preset; apply: (name: string, opts?: { duration?: number; ease?: Ease; }) => Promise<void>; blend: (a: string, b: string, t: number) => void; list: () => string[]; get: (na…: named snapshots of every value below this folder: save, apply (optionally animated), blend
interface Glass
How see-through the panel, its windows and menus are, on any surface: opacity, how much of its own colour the panel keeps (1: opaque; menus and fields keep more), and blur, how far the page behind is blurred (px)
opacity: numberblur: number
interface GPUTextureSource
what you pass to view a WebGPU texture: the texture and its device (a GPUTexture can't tell you)
device: GPUDevicetexture: GPUTexturekind: TextureKind | undefined: a 2D texture with 6 layers can be a cube: say so (default: '2d-array' for layers, '3d' for 3D)label: string | undefinedtopDown: boolean | undefined: row 0 is the image's top (default true, WebGPU's convention; false for GL-style data)isDepth: boolean | undefined: a depth view (default: depth formats are)axis: 'x' | 'y' | 'z' | undefinedcolorSpace: 'linear' | 'srgb' | undefined: its values: 'linear' (default), or 'srgb' encoded for the screen already (a canvas's): shown as they are
interface Group
id: stringtitle: stringitems: string[]: node paths, in display ordercollapsed: boolean | undefined
gui
the default GUI; its panel mounts the first time something is added
const gui: GUI
class GUI
extensions: ExtensionHosthistory: Historyregistry: Map<string, AnyNode>options: GUIOptionsoverrides: Map<string, ValueEntry>: user overrides by path: value and the code default it was made againstranges: Map<string, ValueRange>: ranges set from the panel, by path: each control's slider range over its code's (saved with the values)file: FileLink | null: the linked values file (see linkFile)insets: Insets: What windows docked full length against each edge of the browser window cover, in px, so an app can keep its subject clear of them (see theinsetsevent). Nothing is resized for you.layout: Layout: pins, groups, order, windows, collapsed folders, texture-view settings (persisted apart from values)presetStore: Presets: named snapshots of values;folder.presetsis the per-folder APItimelines: Map<string, Timeline>: timelines by name (see timeline())compare: Compare: hold to see state A (marked values, a preset, or the code defaults)visibleControls: Set<Control<any>>: controls currently on screen; only these are polledvisiblePlots: Set<Plot>: plots currently on screen (a frame scope records while its plot shows)sampledPlots: Set<Plot>structureVersion: numberpersistId: string | nullvisible: booleanhotkey: string | null: the key that shows and hides the panel: thehotkeyoption's, H by default, null for noneconfigure(opts: Pick<GUIOptions, 'autoShow' | 'hotkey' | 'persist'>): this: ChangeautoShow,hotkeyorpersistafter construction (the defaultgui's, say).persistapplies only before the GUI's first use: it reads what's saved then.perf(opts?: PerfOptions): Perf: A Performance folder: frame time, FPS, 1% lows and long frames; CPU time of named sections (perf.begin(name)/end(name)), and their GPU time given a WebGL2 context; memory. Extensions add their own readouts. Calling it again returns the same one (aglgiven later is used if none was).commands: Map<string, Command>command(cmd: Command): () => void: register a command for the palette and hotkeys; returns a function that removes itallCommands(): Command[]: registered commands, plus every button, texture inspector and preset, that are available now: what the palette and keys findrunCommand(cmd: string | Command): boolean: run a command by id (or the command itself); returns false if it isn't availableuse(ext: Extension): thison<K extends keyof GUIEvents>(name: K, fn: Listener<GUIEvents[K]>): () => undefinedresolve(path: string): AnyNode | undefined: a node by full path, filling deferred inspector folders along the wayacceptDrift(ctrl: Control): void: keep the user's value but stop flagging that the code default changedtick(controls?: boolean): void: called by the UI every animation frame while visible: samples graph monitors, and polls on-screen controls unlesscontrolsis false (the UI polls them up to 30 times a second)openWindow(path: string, opts?: OpenWindowOptions): import("./layout").WindowState: Show a folder, a group (by id) or a texture in its own window, docked to an edge of the browser window, in one of its corners, or beside another window. Returns the window's layout entry. gui.openWindow('Performance', { corner: 'top-left', size: { w: 260, h: 280 } });update(): void: call after your app renders: lets texture views copy from the frame you just drewtimeline(opts?: TimelineOptions): Timeline: A timeline: control values keyed over time, played by its own clock, your frame loop (update(dt)) or your own time (sample(t)). One per name;'main'by default. Its keys are saved with the values.persist(id: string): this: autosave values and layout underid(in localStorage, unless a store is given: core/storage.ts), and load what's theresetRange(path: string, range: ValueRange | null, save?: boolean): void: Set a control's range (its slider's ends; seeNumberSpec.min) over its code's, ornullfor its code's again. Saved with the values: autosaved, copied, written to a linked file.save(): ValuesDoc: current overrides as a document (only values that differ from code defaults)load(doc: ValuesDoc | string, opts?: { save?: boolean; restore?: boolean; }): this: apply a saved document; controls it doesn't mention return to their code defaultsclearSaved(): void: forget local saved values (current values stay until reload or reset); a linked file's staydownload(filename?: string): voidupload(): Promise<void>: pick a JSON file and load itflushSave(): void: write pending autosave immediatelylinkFile(path: string, opts?: LinkOptions): Promise<FileLink>: Keep values in a project file: read at startup, written through on every finished change while writable (dev server or File System Access), and applied by production builds when bundled asinitial. Local tweaks sit on top of the file's values.unlinkFile(): Promise<void>: Stop using the linked file: it's no longer read or written, and a remembered handle is forgotten. Values stay as they are (the file's become local values).theme: string | Theme: the panel's colour theme: a built-in's name ('devui' is the default) or your owncustomThemes: Theme[]: themes given as objects (tosetThemeor in the options), which the panel's theme picker lists toosetTheme(theme: string | Theme, opts?: { remember?: boolean; }): this: Change the panel's colours: a built-in theme's name ('nord', 'gruvbox', 'github-light'…) or your own.rememberkeeps it as this browser's choice for every project (the panel's theme picker does this); otherwise the next page load starts from the options again.surface: Surface: how the panel's surfaces are drawn (Surface): the theme picker's, as kept in this browser, else the options'setSurface(surface: Surface, opts?: { remember?: boolean; }): this: Draw the panel's surfaces another way, in any theme: 'flat', 'soft' or 'inset' (see Surface).rememberkeeps it as this browser's choice for every project, as the theme picker doesglass: Glass: how see-through the panel is (Glass), on any surface: the theme picker's, as kept in this browser, else the options'translucent: boolean: the panel's surfaces see-through, the page behind blurred: its glass less than opaquesetGlass(glass: Partial<Glass>, opts?: { remember?: boolean; }): this: How see-through the panel, its windows and menus are, in any theme and on any surface (data, such as textures, plots and colours, stays opaque): its opacity and blur, each kept between glassLimits.rememberkeeps it as this browser's choice for every project, as the theme picker doessetTranslucent(on: boolean, opts?: { remember?: boolean; }): this: see-through, as the default glass (its blur as it is), or opaque: setGlass's opacity aloneresetTheme(): this: forget the theme, surface and glass picked in a panel: back to the code's (or the defaults)show(visible?: boolean): this: mount (lazily loading the UI) and show the panel;show(false)hides it, as hide() doeshide(opts?: { remember?: boolean; }): this: hide the panel;remember: falsedoesn't keep it hidden on the next load (the panel popped out, say)toggle(): thisready(): Promise<void>: resolves once the UI is mounteddispose(): void
interface GUIEvents
change: ChangeEventfinish: ChangeEventstructure: voidpress: Control<any>capture: Control<any>frame: void: the app called update() after renderinglayout: voidvisibility: booleancommands: void: the set of registered commands changedcommand: Command: a command ran (palette, hotkey or code)presets: voidtimelines: void: a timeline was created (each timeline tells its own subscribers about its keys)compare: boolean: A/B compare started (true) or ended (false), or A changedfile: FileLink | null: the linked values file's status changedinsets: Insets: what windows docked full length against the browser window's edges cover changedtheme: string | Theme: the panel's theme changedreveal: AnyNode: a node to bring into view where it shows: its folders opened, scrolled to and flashed (gui.perf()'s frame scope, from the palette)
interface GUIOptions
persist: string | false | { id?: string; values?: boolean; layout?: boolean; } | undefined: storage id for autosaved values and layout; defaultlocation.pathname; false disables. As an object,values: falsesaves no values but those a folder or control opts in withpersist: true, andlayout: falsesaves no layout (open folders, windows, the panel's place).title: string | undefinedautoShow: boolean | undefined: show the panel the first time something is added (default true in a browser)container: HTMLElement | undefined: element the panel host is appended to (default document.body)hotkey: string | null | undefined: toggle hotkey (default 'h'); null disablesside: 'left' | 'right' | undefinedextensions: Extension[] | undefinedcompareKey: string | null | undefined: hold this key to compare with A (default '`'); null disableskeys: 'window' | 'panel' | undefined: Where the panel's keys work ('/' search, '?' shortcuts, the compare key): anywhere in the window but a text field ('window', the default), or only while working in the panel ('panel'), on a page whose keys are its own (devui's Chrome extension's)gesture: Partial<import("../ui/primitives/scrub").GestureConfig> | undefined: scrub gesture constants: thresholds, edge lock, long-press time, gearing…hdr: 'auto' | 'force' | 'off' | undefined: HDR in the panel (the inspector's HDR view, HDR colours' swatches): on HDR displays ('auto', default), always ('force', for testing) or neverhdrCompositorTrigger: boolean | undefinedtheme: string | Theme | undefined: the panel's colours: a built-in theme's name ('nord', 'gruvbox', 'github-light'…) or your own (see Theme). A theme picked in the panel winssurface: Surface | undefined: How the panel's surfaces are drawn, in any theme: 'flat' (the default: tones apart), 'soft' (depth: each folder a card raised on a soft shadow) or 'inset' (each folder a tray set into the panel, its controls raised in it). A surface picked in the panel's theme picker winstranslucent: boolean | Partial<Glass> | undefined: The panel, its windows and menus see-through, the page behind them blurred, in any theme and on any surface (default false): true for the default glass, or how opaque the panel is (opacity, 0 to 1, 1 opaque) and how far the page behind is blurred (blur, px). The browser redraws the blur each frame the page behind changes: leave it off when profiling. A choice made in the panel's theme picker winslabelWidth: string | number | undefined: the label column's width: a CSS length or percentage ('45%', '12em'), or px as a number (default 38%)
interface Hints
Everything a caller can say about a control. Inference fills in what's missing.
id: string | undefined: identity within the folder; defaults to label, then keylabel: string | undefineddescription: string | undefinedtags: string[] | undefinedorder: number | undefinedview: string | undefinedreadonly: boolean | undefinedhidden: boolean | undefineddisabled: boolean | undefinedlisten: boolean | undefined: poll for external changes while visible (default true)persist: boolean | undefined: include in saves, value files and presets; on a folder, the default for the controls under it. Unset, a control takes the nearest folder's, else the GUI'spersist.values(default true for inputs). Copy as code covers a value either wayunit: string | undefined: display unit; 'rad' displays degrees and stores radiansoptions: readonly unknown[] | Record<string, unknown> | undefinedcolor: boolean | ColorHints | undefinedramp: RampHints | undefinedcurve: CurveHints | undefinedorientation: OrientationHints | undefinedmultiline: boolean | undefinedcollapsed: boolean | undefined: collapsed state for foldersdrops: Drops | undefined: On a folder: dragging windows into lists.kindis what it is when its window is dragged;accepts, the kinds of folder that can be dropped into it (its own folders of its kind always can). Unset, a folder is what its folder is, and takes what its folder takes: at the top, a'folder'that takes folders. A window dragged over a list that doesn't take it just moves. A tool is a kind no list accepts ({ kind: 'tool', accepts: [] }): its window goes into no list, its own folder's included, and only its own folders go back into it.window: 'list' | 'tool' | undefined: On a folder: how its window's sized.'list'(the default): as tall as its rows, the height it's given at most.'tool': the size it's given, whatever it shows, its content scrolling inside (a view that changes as you work in it, as the frame capture's does, shouldn't move the window)windowSize: { w: number; h: number; } | undefined: On a folder: the size its window opens at, dragged out of the panel (default 300 × 280; kept within the page). A view with a lot to show (the frame scope's lanes) asks for roomkey: string | undefined: keyboard shortcut for buttons, e.g. 'r' or 'shift+r'
interface Insets
px that windows docked full length against each edge of the browser window cover
top: numberright: numberbottom: numberleft: number
interface InspectOptions
filling a folder from an object (add(obj, opts), folder.inspect(obj, opts))
exclude: string[] | undefined: keys (or paths within, such as 'uniforms/diffuse') to leave out
interface Inspector
Lays out a whole known object, e.g. gui.add(material), through the public API only.
T: anyid: stringpriority: number | undefinedtest(value: unknown): value is T- ``
inspect(value: T, folder: Folder): voidlazy: boolean | undefined: a collapsed folder runs inspect() when first opened (default true): big scenes stay cheap
type Interp
Curves: keyframes of a value over a domain (0–1, or time), for mapping and animation. A live value like a ramp: the panel edits it in place, and your code samples it (sample, lut). Works the same in production builds (the noop build exports it too). Each segment runs from a key to the next with the left key's interpolation: - smooth: automatic tangents that never overshoot (monotone cubic) - bezier: the key's own handles (out, and the next key's in) - linear, or step (hold until the next key)
type Interp = 'smooth' | 'bezier' | 'linear' | 'step'
type Json
type Json = null | boolean | number | string | Json[] | { [key: string]: Json }
interface Key
x: numbery: numberinterp: Interpin: [number, number] | undefined: handle offsets from the key, for bezier:inpoints back (dx ≤ 0),outforward (dx ≥ 0)out: [number, number] | undefinedbroken: boolean | undefined: the handles move apart (otherwise they stay in line)
interface KeyClip
copied keys, times from the first
tracks: { path: string; kind: TrackKind; ch: KeyJSON[][]; }[]
type KeyInput
[x, y], [x, y, interp], or a full key
type KeyInput = [x: number, y: number, interp?: Interp] | Key
type KeyOf
a property of T to bind: one of its keys, or an index when T is an array
type KeyOf = (keyof T & string) | (T extends readonly unknown[] ? number : never)
type KeySelection
keys by track: path → key times
type KeySelection = Record<string, number[]>
class Layout
version: numbercollapsed: Record<string, boolean>textures: Record<string, Partial<TextureViewState>>views: Record<string, Partial<TextureViewState>>: how values are shown, kept for what a view shows rather than for its row: a capture's target, by name and format (Control.viewKey)panel: PanelPlace | undefinedminimized: boolean | undefinedstack: string[]: window ids (andmain, the panel), back to frontvisible: boolean | undefinedorder: Record<string, string[]>: user order of child names, per folder pathmoves: Record<string, string>: folders shown inside another folder (path → the folder they're shown in; '' is the top level)groups: Group[]windows: WindowState[]recent: string[]: recently used palette entries (command ids,node:<path>), newest firstlinkedFile: string | undefined: a values file linked from the UI (relinked on the next visit)revision: number: bumps on every change (UI subscriptions)resolveNode: ((path: string) => AnyNode | undefined) | undefined: nodes by path, for folders moved into another (set by the GUI)load(doc: Partial<LayoutDoc>): void: replace the state with a saved document (v1 documents have no order/groups/windows)toJSON(): LayoutDocpinned: Groupgroup(id: string): Group | undefinedisPinned(path: string): booleanpin(path: string): booleanunpin(path: string): booleangroupsWith(path: string): Group[]: groups that show this pathcreateGroup(title: string, items?: string[]): GrouprenameGroup(id: string, title: string): voiddeleteGroup(id: string): void: the pinned group can be emptied, not deletedsetGroupCollapsed(id: string, collapsed: boolean): voidaddToGroup(id: string, path: string, index?: number): booleanremoveFromGroup(id: string, path: string): booleanmoveInGroup(id: string, path: string, index: number): boolean: move an item toindex(in the list as it is before the move)moveGroup(id: string, index: number): voidsorted(folder: Folder): AnyNode[]: Visible children in display order. The user's order (if any) wins for the names it lists; others keep their code position, placed after the nearest preceding listed sibling, so a control added in code appears near where the code put it.moveChild(folder: Folder, node: AnyNode, index: number): void: move a child toindexamong its folder's visible children (index in the current order)resetOrder(folder: Folder): voidshownIn(path: string): string: the folder a node is shown in: where it was moved, else where it livescanMove(path: string, into: string): boolean: Canpath(a folder) be shown ininto(dropped into its list)? Only where it's taken (drops.ts: its own folder, or one that accepts its kind); not in itself, nor in one of its own folders; and not a hidden folder, which no list shows.moveFolder(folder: Folder, node: AnyNode, index: number): boolean: show a folder inintoatindexamong its children there (its own folder: just reorder)moveBack(path: string): void: show a moved folder where it lives againresetMoves(): void: every moved folder back where it liveswindowFor(shows: WindowShows): WindowState | undefinedopenWindow(shows: WindowShows, rect?: WindowRect, raise?: boolean): WindowState: Open a window (or return the already-open one).raise: bring that one to the front, as a texture opened in the inspector again (a picture clicked) must show, where it may be behind another windowsetCompact(id: string, compact: boolean, size?: { w: number; h: number; }): WindowState | undefined: A folder's window in its compact form or its full one, where it is: its place, its holds and its place in the stack kept (gui.perf()'s meters expanded into its folder, and back).size, the form's own (its place kept)windowTitle(w: WindowState): string: what a window's title says: its folder's or texture's path, its group's titleoutIn(id: string): string | undefined: the pop-out a window is drawn in (by the id of the window it was opened for), if it's outlendWindow(id: string): () => void: Draw a window in a browser window of its own (a pop-out) for now: the page leaves it out until the returned function brings it back, with any window opened there meanwhilecloseWindow(id: string, rects?: (id: string) => WindowRect | undefined): void: Close a window. Windows (and the panel) holding it take over its own holds on those sides, so nothing jumps: a side panel held to a bottom window's top ends up held where that window's bottom was.rectsgives where windows are now (the UI knows), for edges that let go.placeOf(id: string): PanelPlace | undefined: where a window (ormain, the panel) is, as keptsetPlace(id: string, place: PanelPlace | undefined): void: set where a window (ormain, the panel) is; undefined goes back to its defaultresetPlaces(): void: every window, and the panel, back to its default placesetWindowRect(id: string, rect: WindowRect, hold?: Holds): voidraiseWindow(id: string): void: bring a window (ormain, the panel) to the front; no-op when it's already theresetPanel(place: PanelPlace | undefined): void: where the panel is; undefined goes back to its defaultnoteRecent(id: string): void: remember a palette pick so it ranks higher next timestale(exists: (path: string) => boolean): { items: { group: string; path: string; }[]; order: string[]; windows: WindowState[]; textures: string[]; collapsed: string[]; moves: string[]; count: number; }: entries that refer to paths that don't exist nowclean(exists: (path: string) => boolean): number: drop entries whose paths are gone; returns how many were removed
interface LayoutDoc
version: number | undefinedcollapsed: Record<string, boolean>textures: Record<string, Partial<TextureViewState>>views: Record<string, Partial<TextureViewState>> | undefined: how values are shown, kept for what a view shows rather than for its row: a capture's target, by name and format (Control.viewKey)width: number | undefined: version 2: the panel's width and side (version 3 keepspanel)side: 'left' | 'right' | undefinedpanel: PanelPlace | undefinedminimized: boolean | undefinedstack: string[] | undefined: window ids (andmain, the panel), back to frontvisible: boolean | undefinedorder: Record<string, string[]> | undefined: user order of child names, per folder pathmoves: Record<string, string> | undefined: folders shown inside another folder (path → the folder they're shown in; '' is the top level)groups: Group[] | undefinedwindows: WindowState[] | undefinedrecent: string[] | undefined: recently used palette entries (command ids,node:<path>), newest firstlinkedFile: string | undefined: a values file linked from the UI (relinked on the next visit)
interface LazyRowView
a row view loaded the first time a row needs it: load: () => import('./my-view')
id: stringtypes: string[] | undefinedwide: boolean | undefinedload(): Promise<RowView | { default: RowView; }>
interface LifetimeHook
Hooks a control's lifetime to an object, e.g. three.js dispose events.
test(value: unknown): booleanonDispose(value: unknown, fn: () => void): () => void
interface MatchContext
value: unknownkey: stringtarget: objecthints: Specresolve(value: unknown, key: string, target: object): Resolution | null: resolve a different value (e.g. the inner value of a wrapper)isTexture(value: unknown): boolean: true if some extension can display this value as a texture
interface Matcher
id: stringpriority: number: higher runs first: project 300, library extensions 200, std 100match(ctx: MatchContext): Resolution | null
interface MountedView
a row view drawn by an extension: what mount returns
C: anyupdate(ctx: ViewContext<C>): void: the value (or the control's spec) changed- ``
interface OpenWindowOptions
dock: 'left' | 'right' | 'top' | 'bottom' | undefined: hold the browser window's edge: full width at the top or bottom, full height at a sidebeside: string | undefined: sit beside another window:'panel', or the path (or group id) it showscorner: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | undefined: sit in a corner of the browser window, held against its two edgessize: { w: number; h: number; } | undefined: its width and content height, px (300 × 280 by default)
interface OptionItem
T: anylabel: stringvalue: T
interface OrientationHints
a rotation shown on the orientation sphere (view 'orientation', and 'direction')
format: 'euler' | 'quaternion' | undefined: how the value holds it: Euler angles [x, y, z] (radians, or degrees with unit 'deg'), or a quaternion [x, y, z, w]; by default by its lengthorder: EulerOrder | undefined: the Euler angles' order, as three.js's (default 'XYZ')forward: AxisName | undefined: the object's own forward axis, which the model's nose (and a direction) follows: '+z' (default, three's objects) or '-z' (cameras, lights)…
type Params
{ curl: { value: 30, min: 0 } } → { curl: number }
type Params = { -readonly [K in keyof S]: S[K] extends { value: infer V } ? (S[K] extends Record<string, unknown> ? V : S[K]) : S[K]; }
class Perf
folder: Folderplot: Plot: frame time, ms (its guide, the display's frame budget)cpuPlot: Plot: CPU time of the timed sections, ms: in all, and each one's where there are two or moregpuPlot: Plot: GPU time of the timed sections, ms: in all, and each one's where there are two or morestats: { fps: number; '1% low': number; 'long frames': number; 'frame ms': number; 'cpu ms': number; 'gpu ms': number; 'heap MB': number; }: what the monitors show; updated a few times a secondmemory: Plot | null: the memory graph, once there's memory to graphdisposed: boolean: afterdispose();gui.perf()then makes a new onegui: GUIcompact(corner?: OpenWindowOptions['corner']): WindowState: Show the compact meters: GPU time, CPU time and memory, each a small graph with its value written on it, in a window with no title bar (drag it by anything but its buttons). They're the folder's window in its compact form, so the folder shows there and nowhere else: where it's open in a window, that window turns into them, where it is; otherwise they open incorner, top-left by default, and the folder leaves the panel until they close.expand(), their expand button or a double-click on them turns them into the folder again.expand(): WindowState: the meters expanded: the folder, in their window where they are (or in a window of its own, if they aren't open)gpuTiming: { readonly testing: boolean; readonly why: string | null; }: whether timers time each pass here, and if not, why (gpu msis then the whole frame's, from a fence, or NaN with no fence: no time never reads as 0)setGL(gl: WebGL2RenderingContext): this: time GPU sections on this context (the first one given wins); a frame scope of it is offered in the folder. Its timing is a chunk of its own, loaded now: GPU time starts a frame or so laterofferScope(target: object): this: Offer a frame scope oftarget(a WebGL2 context, a GPUDevice, or a three.js renderer, which names its targets and draws) in the folder: a frame scope folder, collapsed, attached the first time it's opened (its chunk loads then), and recording only while it's on screen, so folded away or compact it costs nothing.setGLoffers its context's, and three's extension its renderer's. Not where the app has attached one to the context itself: that one stays where the app put it.showScope(): undefined: The frame scope offered, opened (and so attached) and brought into view: the meters expanded into the folder, or the folder opened where it is. Where the app attached one itself, that one.begin(name?: string): void: Start a named section: CPU time always, GPU time when a context is set (a section inside another counts in both). One begun inside another of its name is part of it: three's render() calls itself while it prefilters an environment map.end(name?: string): voidtime<T>(name: string, fn: () => T): TonFrame(fn: () => void): () => undefined: called once per frame, after that frame's samples are taken (extensions use it to publish counters)dispose(): void
interface PerfOptions
gl: WebGL2RenderingContext | undefined: the context to time GPU work on (three's extension sets its renderer's)folder: string | undefined: folder path, default 'Performance'compact: boolean | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | undefined: start as the compact meters (perf.compact()), in this corner of the page (true: top-left), unless the folder or the meters are already open in a window (as the page was left)
PINNED
const PINNED: 'pinned'
class Plot
kind: 'plot'capacity: numberseries: string[]buffers: Float64Array<ArrayBufferLike>[]times: Float64Array<ArrayBufferLike>: when each sample was pushed (performance.now), for "how long ago"log: boolean: draw the y axis on a log scale (positive values only)head: numbercount: numberdataVersion: number: bumps on every push; views redraw when it changesmin: number | undefinedmax: number | undefinedunit: string | undefinedcolors: string[] | undefined: its series' colours: CSS colours, or the theme's by name (--series-3)guide: number | undefined: a dashed line across it at this value (a frame's budget, say)note: string: a line about it, shown by its label (frames a second under a frame time, say):setNoteoverlay: boolean: its label and readout over its graph (PlotOptions.overlay)half: boolean: half a row's width, beside the half-width plots next to it (PlotOptions.half)setNote(note: string): this: its note, shown by its label (a view redraws when it changes)push(value: number | Record<string, number>): thissample(s: number, i: number): number: sample i of series s, 0 = oldest retainedtime(i: number): number: when sample i was pushedlatest(s?: number): numberclear(): this
interface PlotOptions
label: string | undefinedseries: string[] | undefined: series names; more appear automatically when pushing recordshistory: number | undefined: samples kept per seriesmin: number | undefinedmax: number | undefinedunit: string | undefinedcolors: string[] | undefinedid: string | undefinedlog: boolean | undefined: log y axisguide: number | undefined: a dashed line across it at this value, in its range (a frame's budget, say)overlay: boolean | undefined: its label and readout written over its graph, which is all of its row: a plot read at a glancehalf: boolean | undefined: half a row's width: plots side by side with the half-width plots next to them
interface Preset
name: stringscope: string: folder path the preset covers; '' is the whole GUIvalues: Record<string, Json>created: number
class Presets
list: Preset[]load(list: unknown): voidin(scope: string): Preset[]get(scope: string, name: string): Preset | undefinednextName(scope: string, base?: string): string: a name not used in this scope yet: 'Preset 1', 'Preset 2'…save(scope: string, name: string): Preset: snapshot the current values (replacing a preset of the same name)delete(scope: string, name: string): voidrename(scope: string, name: string, to: string): booleananimating: booleancancelAnimation(): voidapply(scope: string, name: string, opts?: { duration?: number; ease?: Ease; }): Promise<void>: set the scope to a preset; animated overdurationseconds. One undo step either wayblend(scope: string, a: string, b: string, t: number, opts?: { transient?: boolean; }): void: set the scope to a mix of two presets (t = 0 is a, 1 is b)
ramp()
a colour ramp: ramp(['#000', '#f80', '#fff']), ramp([[0, 'navy'], [0.7, '#f0f']], 'oklch'), ramp([...], 'oklab', { smooth: true })
function ramp(stops?: readonly StopInput[], mode?: RampMode, options?: RampOptions): Ramp
class Ramp
stops: RampStop[]mode: RampModesmooth: boolean: a smooth curve through the stops, with no corners at them (straight between them when not set)version: number: bumps on every change: compare it to know when to rebuild a LUT or a textureset(stops: readonly RampStop[] | readonly StopInput[], mode?: RampMode, smooth?: boolean): this: replace the stops (and the mode, and whether it's smooth); the panel calls this as you editsubscribe(fn: (r: Ramp) => void): () => undefined: called after every changesample(t: number): RGBA: the colour at t (0–1), linear sRGBhex(t: number): string: the colour at t as '#rrggbb', 8-bit sRGB as a screen shows it (displayable): a colour past sRGB brought in with its hue kept, an HDR one's basecolors(n?: number): string[]: n colours as '#rrggbb' (ashex): evenly from end to end, or each band's for stepslut(n?: number, out?: Float32Array<ArrayBuffer>): Float32Array<ArrayBuffer>: n samples, RGBA linear, for a lookup texture (outis reused when given)gradient(direction?: string): string: a CSSlinear-gradient(…)with alpha, its colours ashexgives them
type RampMode
how colour runs between stops: perceptual (oklab, straight; oklch, round the hue the short way; oklch-long, the long way), light (linear), as stored (srgb), or bands (step)
type RampMode = 'oklab' | 'oklch' | 'oklch-long' | 'linear' | 'srgb' | 'step'
interface RampStop
a stop: its place along the ramp (0–1) and its colour (linear sRGB)
t: numbercolor: RGBA
interface RampValue
a ramp's value, as the panel edits and saves it
stops: RampStop[]mode: RampModesmooth: boolean | undefined: a smooth curve through the stops, with no corners at them (straight between them when not set)
interface RawGPUTexture
A raw WebGPU texture, with the device it belongs to (a GPUTexture can't tell you).
device: GPUDevicetexture: GPUTexture
interface RawWebGLTexture
A raw WebGL2 texture plus the facts GL can't tell us cheaply.
gl: WebGL2RenderingContexttexture: WebGLTexture
interface Resolution
C: anykind: NodeKindHint | undefinedtype: stringcodec: Codec<any, C>view: stringspec: Partial<Spec> | undefinedexplain: string: human-readable reason, shown in the control's debug tooltipbind: { target: object; key: string; } | undefined: rebind to a nested property, e.g. a uniform wrapper's.value
type ResolvedTexture
Something drawable by the texture viewer, resolved from a library object.
type ResolvedTexture = | { kind: 'webgl'; texture: import('../gpu/types').RawWebGLTexture; /** a multi-attachment source (MRT): names to switch between, `attachment` picks one (-1 is depth) */ attachments?: string[]; /** the extension can capture each write to it (`capture: 'on-write'`) */ onWrite?: boolean; } | { kind: 'webgpu'; texture: import('../gpu/types').RawGPUTexture; attachments?: string[]; onWrite?: boolean; …
interface RowView
A row view an extension draws, for controls whose view is its id. It draws into a plain element (the row's value cell, in the panel's shadow root: style it with the panel's CSS variables, --accent, --fg, --row, --row-h…), with any framework or none.
C: anyid: stringtypes: string[] | undefined: the value types it can show (default: any)wide: boolean | undefined: the whole row's width, with no label column (a view that labels itself, like a texture's)mount(el: HTMLElement, ctx: ViewContext<C>): MountedView<C>
interface Spec
Hints after inference, with options normalised.
options: OptionItem<unknown>[] | undefinedcolor: ColorHints | undefined
std
const std: { name: string; types: ValueType<any>[]; matchers: Matcher[]; textureSources: TextureSourceResolver<any>[]; lifetimes: never[]; } & { apiVersion: number; }
type StopInput
a CSS colour, a 0xrrggbb number, linear RGBA, or any of those at a place: [0.3, '#f80']
type StopInput = string | number | RGBA | [t: number, color: string | number | RGBA]
type Surface
How the panel's surfaces are drawn, in any theme: 'flat', tones apart (the default); 'soft', depth: each open folder a card raised on a soft shadow, what shows data set in; or 'inset', soft's inverse: each open folder a tray set into the panel, its controls raised in it. Each can be see-through too (Glass, translucent)
type Surface = 'flat' | 'soft' | 'inset'
type TextureKind
type TextureKind = '2d' | 'cube' | '2d-array' | '3d'
interface TextureSourceResolver
T: anyid: stringpriority: number | undefinedtest(value: unknown): value is Tresolve(value: T, opts?: TextureResolveOptions): ResolvedTexture | null- ``
- ``: for depth: the near/far range it was rendered with, when known
interface TextureViewState
A texture view's settings: a texture control's (Control.texture), kept in the layout.
level: numberlayer: numberexposure: numbertonemap: 'clip' | 'reinhard' | 'aces' | 'agx'channels: 'rgb' | 'rgba' | 'rg' | 'r' | 'g' | 'b' | 'a': rg shows red and green only (velocity, normal xy)overlays: { nanInf: boolean; negative: boolean; clipping: boolean; falseColor: boolean; ids?: boolean; }: ids colours each distinct value (integer ids, material indices)encodeSRGB: booleancolourSpace: ColourSpace | undefined: what its values are, as chosen in the viewer: light (encoded for the screen to show), colour encoded for it already, or numbers that aren't colour (shown as stored, fitted). Unset: as what it holds says (textureDefaults)black: number: value range mapped to black and white before exposurewhite: numberautoRange: boolean: keep black/white fitted to sampled values (depth textures start with this on)fitDrawn: boolean | undefined: a fit leaves out what the texture was cleared to, where that's known (TextureInfo.cleared), to fit what was drawn; default onframeDrawn: boolean | undefined: shown framed on what was drawn, where a fit found where that is (RangeResult.drawn); off: all of it. Default oncapture: 'auto' | 'manual' | 'on-write': capture timing: 'auto' (every frame while visible), 'manual' (a snapshot taken when capture() is called, at that point in the frame), or 'on-write' (each time rendering into the target finishes, for extensions that support it: three.js render targets)write: number | undefined: on-write: which write of a frame to show, 1-based; 0 (default) is the lastwrites: number | undefined: on-write: writes seen in the last frame (filled in by the extension)defaultsFor: string | undefined: what the shown texture holds, that the view's defaults are set for (textureDefaults)numbers: { open: boolean; log: boolean; exact: boolean; view?: { lo: number; hi: number; }; drag?: { lo: number; hi: number; }; } | undefined: the inspector's numbers section: open (it's computed only then), log bins, every texelprobes: { x: number; y: number; level?: number; layer?: number; }[] | undefined: texels pinned in the inspector (up to four), GL rows from the bottom; level and layer default to the shown onescompare: { target: string | null; mode: 'hold' | 'split' | 'diff'; split: number; gain: number; } | undefined: Compare with B:snap:<id>(a snapshot, for this page's life) ortex:<path>(another texture view). Hold flips to B, split wipes at a fraction of the view's width, diff shows |A − B| × gain.layout: 'plain' | 'cross' | 'equirect' | 'strip' | undefined: cube: one face, a cross or an equirect unwrap; any mipmapped texture: every level in a stripaxis: 'x' | 'y' | 'z' | undefined: 3D: the axis slices go along (z by default)depth: { linear: boolean; near?: number; far?: number; perspective?: boolean; } | undefined: depth: show distance instead of the stored value; near/far/perspective default to the resolver'sattachment: number | undefined: a multi-attachment source: which colour attachment, or -1 for depthhdr: boolean | undefined: the inspector shows HDR (a float canvas) with values up toheadroomtimes SDR whiteheadroom: number | undefined
interface Theme
The panel's colours: a few you choose, the rest derived from them (in OKLCH) the way the default theme's are. Any CSS colour works (hex, rgb(), hsl(), oklch()).
name: stringlabel: string | undefined: its name in the theme picker (default: name)light: boolean | undefined: dark text on a light backgroundbackground: string: the panel's background and text, and the accent (focus, selection, slider fills)foreground: stringaccent: stringred: string | undefined: a syntax theme's colours: errors (red), changed values (orange), the x/y/z axes (red, green, blue), plot seriesorange: string | undefinedyellow: string | undefinedgreen: string | undefinedcyan: string | undefinedblue: string | undefinedpurple: string | undefinedpink: string | undefinedtokens: Record<string, string> | undefined: tokens to set exactly once the rest are derived, by name without the dashes:{ row: '#222', 'axis-x': 'tomato' }
class Timeline
name: stringduration: numberloop: booleanrate: number: playback speedplaying: booleanrecording: boolean: record mode: every change you make keys the control at the playheadfps: number: while recording, key times snap to frames of this ratetracks: Map<string, Track>version: number: bumps when keys, settings or the play state change (not every frame's time)gui: GUItime: numbersubscribe(fn: () => void): () => undefinededited: boolean: its keys or settings were changed here (so it's saved even with no keys)play(): thispause(): thistoggle(): thisrecord(on?: boolean): this: record mode on or off (toggles without an argument)seek(t: number): this: move the playhead and set the controls to their values there (the end itself, when looping, is the end)update(dt: number): this: your frame loop: advance bydtseconds while playing, and set the controlssample(t: number): this: your own clock: the controls take their values attapply(paths?: readonly string[]): void: set every keyed control (or those atpaths) to its value at the playhead, except one you're draggingvalueAt(track: Track | string, t: number): unknown: a track's value (canonical) at time tkeyTimes(path: string): number[]: every key time on a track (over its channels), in orderhasKey(path: string, t?: number): booleankey(ctrl: Control | string, t?: number, value?: unknown): this: key a control at t (default: the playhead) with its value now, orvalue(canonical)unkey(ctrl: Control | string, t?: number): this: remove a control's keys at t (default: the playhead); its track goes with its last keyremoveKeys(sel: KeySelection): this: remove keys (path → times); a track with none left goesremoveTrack(path: string): thismoveKeys(sel: KeySelection, dt: number): this: move keys (path → times) by dt, together, kept inside 0…duration; keys they land on are replaceddragKeys(sel: KeySelection): (dt: number) => void: A drag of keys (path → times): call the result with how far they've moved in all. Each move starts again from where the keys were, so a key the drag passes over is only replaced if the drag ends on it. The keys move together, kept inside 0…duration.interpOf(sel: KeySelection): Interp | null: the interpolation from these keys (path → times): one, or null when they differ (or there are none)setInterp(sel: KeySelection, interp: Interp): this: the interpolation from these keys (path → times) to the nextcopyKeys(sel: KeySelection): KeyClip: keys (path → times) to paste elsewhere: their times from the firstpasteKeys(clip: KeyClip, t?: number): this: paste keys at t (default: the playhead), onto the tracks they came fromedit(paths: string[], fn: () => void): void: Change tracks as one undo step:fnedits them, and undo puts back what they were. The controls take their values at the playhead after.toJSON(): TimelineJSONload(j: Partial<TimelineJSON> | null | undefined): thisconfigure(opts: { duration?: number; loop?: boolean; }): this: change the length or looping (an undo step of its own isn't kept for these)dispose(): void
interface TimelineJSON
duration: numberloop: booleantracks: Record<string, TrackJSON>
interface TimelineOptions
name: string | undefined: default 'main'duration: number | undefined: seconds (default 10)loop: boolean | undefined: go back to the start at the end (default true)autoplay: boolean | undefined: start playing (default false)
interface Track
path: stringkind: TrackKindchannels: Curve[]: one per component; their keys share times unless edited apart
interface TrackJSON
kind: TrackKindch: KeyJSON[][]shape: Json | undefined: for production builds: the native literal is this shape, with the channels as its numbers in orderframes: [number, Json][] | undefined: for production builds without a shape: the native literal at each key timecolor: { linear: boolean; scale: number; alphaScale?: number; } | undefined: for production builds, a colour's: how its literal's numbers hold it, linear or sRGB-encoded, and their scale (1, or 255 for bytes) and alpha's. Told by its control, as a literal alone can't tell it: keyed at pure colours, both encodings give the same numbers at every key, and a fade from alpha 0 says nothing of alpha's scale at its first
type TrackKind
type TrackKind = 'number' | 'vector' | 'color' | 'boolean'
interface ValuesDoc
devui: 1values: Record<string, ValueEntry>:vcanonical value,dthe code default it was made against,na plain native literalremoved: string[] | undefined: local storage only: paths reset here that the linked file still setspresets: Preset[] | undefined: saved with the values when there are anytimelines: Record<string, TimelineJSON> | undefined: timelines' keys, by nameranges: Record<string, ValueRange> | undefined: ranges set from the panel, by path
interface ValueType
Canonical semantics of a value type, shared by every codec of that type.
C: anyid: stringequals(a: C, b: C): booleanclone(c: C): CtoJSON(c: C, spec: Spec): JsonfromJSON(json: Json, spec: Spec, like: C): C | undefined- ``
interface ViewContext
what a row view is given, and how it changes the value
C: anycontrol: Control<C>: the control: its label, spec (hints), path, type…value: C: its value now (canonical: what its type and codec make of the native value)readonly: booleanset(value: C, opts?: { transient?: boolean; }): void: a change you make: an undo step of its own, or part of a gesture between begin() and end()begin(): void: a drag starts: the changes until end() are one undo stepend(cancel?: boolean): void: the drag ends;cancelputs the value backdrag(e: PointerEvent, drag: ViewDrag): void: A drag from a pointerdown, as the panel's own values drag: it starts once the pointer moves (a press alone is a click), the pointer locks at the window's edge (the cursor wraps round, so it never runs out of room), Shift is faster and Alt slower, Esc puts the value back, and the HUD by the pointer showsreadout. It's one undo step (begin and end are done for you).
interface ViewDrag
a view's drag (ViewContext.drag)
move(m: { dx: number; dy: number; snap: boolean; }): void: the travel since the press, px (each step scaled by the gear);snapwhile Ctrl (⌘) is held- ``: after it: kept, or put back (Esc)
- ``: the HUD's text while dragging, read each frame (the value, say)
interface WindowRect
x: numbery: numberw: number: outer widthh: number: height of the resizable content
type WindowShows
what a window shows: a folder, a group, a texture's inspector, a control's editor (a curve's) or a timeline. One window each: a folder's, in whatever form (WindowState.compact), is the one place it shows outside the panel's lists
type WindowShows = { folder: string } | { group: string } | { texture: string } | { editor: string } | { timeline: string }
interface WindowState
id: stringshows: WindowShowsrect: WindowRect | undefined: absent until the window is first moved or resized; for held edges, where it was lasthold: Partial<Record<Side, Hold>> | undefined: held edges (absent: free)compact: true | undefined: a folder's window in its compact form:gui.perf()'s meters (core/perf.ts), drawn so whatever else is true (empty until its perf is there)
@haxiomic/devui/three
colorCodec
THREE.Color in the working colour space ⇄ canonical linear sRGB
const colorCodec: Codec<THREE.Color, RGBA>
curveTexture()
function curveTexture(curve: Curve, size?: number): THREE.DataTexture
eulerCodec
const eulerCodec: Codec<THREE.Euler, number[]>
quaternionCodec
quaternions are edited as Euler angles
const quaternionCodec: Codec<THREE.Quaternion, number[]>
rampTexture()
function rampTexture(ramp: Ramp, size?: number): THREE.DataTexture
threeExtension()
function threeExtension(options?: ThreeExtensionOptions): Extension
interface ThreeExtensionOptions
renderer: Renderer | undefined: needed to view textures: devui reads them in the renderer's own context (WebGL2, or WebGPU)
interface WebGPURendererLike
three's WebGPURenderer (from 'three/webgpu'), as far as devui needs it. Its backend is WebGPU, or WebGL2 where there's no WebGPU; devui views textures in whichever it is.
isWebGPURenderer: truebackend: { isWebGPUBackend?: boolean; isWebGLBackend?: boolean; device?: GPUDevice | null; gl?: WebGL2RenderingContext; get(o: object): { texture?: GPUTexture; textureGPU?: WebGLTexture; }; }initTexture(t: THREE.Texture): voidrender(scene: THREE.Object3D, camera: THREE.Camera): unknownsetRenderTarget(target: THREE.RenderTarget | null, ...rest: number[]): voidgetRenderTarget(): THREE.RenderTarget | nullinfo: { autoReset: boolean; render: { calls?: number; drawCalls?: number; triangles: number; points: number; lines: number; }; memory: { textures: number; geometries: number; }; programs?: unknown[] | null; }
@haxiomic/devui/frames
bytesLabel()
bytes: B, then KB, MB and GB (of 1024), a tenth shown below 10 of each
function bytesLabel(n: number): string
Clear
const Clear: { readonly Color: 1; readonly Depth: 2; readonly Stencil: 4; }
interface FrameCaptureOptions
which frame a capture takes (FrameScope.capture)
when: 'next' | 'work' | 'input' | undefined: 'next' (the default): the next frame. 'work': the next that does more than the frames before it (more passes or draws), passing over an on-demand renderer's idle frames: arm it, then interact. 'input': after the next input to the page (a pointer or a key, not devui's own), the first that does work. For a hover that's often the pick's frame (reading what's under the pointer), with the highlight it causes a frame or two later:skiptakes thatwithin: number | undefined: how long 'work' and 'input' wait, ms (default 30 s: time to interact by hand)skip: number | undefined: frames to let go first, of those it would take (for 'work' and 'input', frames that do work)timesSince: number | undefined: Size its passes by the frames after this one (a serial) only: a capture taken after a change, timed as the app runs with it, not as it ran before
interface FrameCounts
what a frame did, summed
draws: number: draw calls (a multi-draw's sub-draws, and a bundle's draws, each count)passes: number: runs of work on one targetcomputePasses: number: compute passes and their dispatches (WebGPU)dispatches: numberclears: numberblits: number: blits and copies into a textureindexed: number: with an index buffer (drawElements*, drawRangeElements, drawIndexed*)instanced: number: with more than one instanceindirect: number: counts on the GPU (WebGPU's draw*Indirect, dispatchWorkgroupsIndirect)multi: number: multi-draw calls (WEBGL_multi_draw)bundles: number: render bundles executed (WebGPU)modes: number[]: draws by primitive mode, indexed by Modetriangles: numberlines: numberpoints: numbervertices: number: vertices processed: count × instancesinstances: numberprograms: number: useProgram/setPipeline to a different onetextureBinds: number: bindTexture (WebGL), setBindGroup (WebGPU)bufferBinds: number: bindBuffer* (WebGL), setVertexBuffer/setIndexBuffer (WebGPU)vaoBinds: numberframebufferBinds: numbersamplerBinds: numberuniforms: number: uniform* callsstateChanges: number: fixed-function state: enable/disable, blend, depth, stencil, cull, masks, viewport, scissor…attribs: number: vertex attribute setup outside a VAO: vertexAttribPointer, enable/disableVertexAttribArray, divisorsuploads: number: bytes uploaded this frame (texture and buffer data)textureUploads: numberbufferUploads: numberreadbacks: number: reads back to the CPU (readPixels, getBufferSubData, mapAsync)syncCalls: number: queries that wait on the GPU process (getError, getParameter, get…Parameter, checkFramebufferStatus…)compiles: number: shader compiles and program links (WebGL), pipelines and shader modules made (WebGPU)submits: number: queue submits (WebGPU)created: number: resources made and deleted this framedeleted: numbercopiesOut: number: the canvas copied out (drawImage into a 2D canvas, createImageBitmap): each waits for its passesoverflow: number: draws past the detailed limit: counted, not detailed
class FrameRecord
one frame, as columns
serial: number: increasing per frame; 0 = never usedstart: number: CPU time, ms (performance.now): its first call and when it closedend: numbergpu: number: GPU ms of the whole frame, the sum of its timed passes (NaN until they're all in, or untimed)gpuSteady: number: alike, of the passes' steady times (passSteady)gpuPending: number: passes still waiting for a GPU timeappTimed: number: passes the app timed itself (its own timestamp writes, or its own timer query open): devui's timing stands aside, as a pass takes one timerslowed: boolean: devui slowed it (a capture's wrappers were on): its times aren't the app's as it runsreadBacks: number[]: Its readbacks to the CPU (readPixels) of what a pass wrote: [frame serial, pass, texture] each, the pass this frame's or an earlier one's (a readback put off a frame, so it doesn't wait for the GPU)copiesOut: number[]: The canvas copied out (drawImage into a 2D canvas, createImageBitmap: a pane of a canvas several share, each to its own): [its pass (the passes before it), the draws before it (their count), x, y, width, height] eachcounts: FrameCountsnDraws: numberdrawPass: Int32Array<ArrayBuffer>drawProgram: Int32Array<ArrayBuffer>drawMode: Uint8Array<ArrayBuffer>drawCount: Int32Array<ArrayBuffer>drawInstances: Int32Array<ArrayBuffer>drawLabel: Int32Array<ArrayBuffer>: a label from the recorder's string table (0 = none): an object, a materialdrawInputsAt: Int32Array<ArrayBuffer>: where the draw's inputs start ininputs, and how many (a run of draws reading the same share one list)drawInputsN: Uint8Array<ArrayBuffer>inputs: Int32Array<ArrayBuffer>nInputs: numbernPasses: numberpassTarget: Int32Array<ArrayBuffer>passLayer: Int32Array<ArrayBuffer>: the attachment that defines the pass's lane variant: a cube face, a layer or a mip level, packedpassFlags: Uint8Array<ArrayBuffer>passClear: Uint8Array<ArrayBuffer>passFirst: Int32Array<ArrayBuffer>passDraws: Int32Array<ArrayBuffer>passCpu0: Float64Array<ArrayBuffer>passCpu1: Float64Array<ArrayBuffer>passCpuTypical: Float32Array<ArrayBuffer>: CPU ms as the pass typically takes now, and where it typically starts (ms into the frame): the mean of its last readings (Recorder.cpuIn)passAtTypical: Float32Array<ArrayBuffer>passGpu: Float32Array<ArrayBuffer>: GPU ms; NaN while pending or if untimedpassSteady: Float32Array<ArrayBuffer>: GPU ms as the pass typically takes now: the low quarter of its last readings (Recorder.gpuIn); NaN where it's untimedpassViewW: Int32Array<ArrayBuffer>passViewH: Int32Array<ArrayBuffer>passVertices: Float64Array<ArrayBuffer>: vertices the pass processed, for sharing its GPU time among its drawspassOutsAt: Int32Array<ArrayBuffer>: every texture a pass writes (its colour attachments and depth): where its list starts inouts, and how manypassOutsN: Uint8Array<ArrayBuffer>outs: Int32Array<ArrayBuffer>nOuts: numbernLinks: numberlinkFrom: Int32Array<ArrayBuffer>: writer pass in this frame, or -1 when it was written in an earlier framelinkFromTarget: Int32Array<ArrayBuffer>: the writer's target (for an earlier frame's write, the lane it came from)linkTo: Int32Array<ArrayBuffer>linkTexture: Int32Array<ArrayBuffer>: the texture readreset(serial: number, now: number): voidpassOuts(p: number): number[]: the textures a pass writesaddPass(target: Id, layer: number, flags: number, now: number, viewW: number, viewH: number): numberisApp(p: number): boolean: a pass of the app's: not devui's own work on its context (a texture view's, a readback)isCompute(p: number): boolean: a compute pass (WebGPU's): dispatches, nothing drawnisBlit(p: number): boolean: a blit: a copy between targets, nothing drawnappPasses(): number[]: the app's passes, in order (devui's own left out)passNumber(pass: number): number: a pass's number among the app's, from 1, or 0 for devui's own: theirs (a texture view drawing on the app's context) come and go with what devui shows, so counting them would move the app'sappCpu(): number: the app's CPU ms on the frame: its passes' (devui's own left out)appCpuSteady(): number: the app's CPU ms as its passes typically take now (passCpuSteady): one frame's can't be read finer than the clock's steppassCpuSteady(p: number): number: a pass's CPU ms as it typically takes now (passCpuTypical), else (a frame not through Recorder.cpuIn) its own readingpassAtSteady(p: number): number: where a pass typically starts, ms into the frame (passAtTypical), else where it didappGpu(): number: the app's GPU ms on the frame: its timed passes', NaN when none of them is (no time never reads as 0)appGpuSteady(): number: the app's GPU ms as its passes typically take now (passGpuSteady): what the frame costs the GPU, its readings' waits read throughpassGpuSteady(p: number): number: a pass's GPU ms as it typically takes now (passSteady), else (a frame not through Recorder.gpuIn) its own readingclone(): FrameRecord: a copy of what was recorded (to hold a frame while recording carries on)passesOnly(): FrameRecord: A copy of its passes' targets, flags and draw counts alone: what keys a pass and works out its time over frames (a measurement keeps one of each shape of frame it sees, and an app making targets every frame has a shape a frame). Nothing else of it is keptsharesInputs(i: number): boolean: Draw i's inputs are the draw's before it in its pass, their list shared. Not just where its list starts: a draw that reads nothing starts where the next draw's list doesdrawInputs(i: number): Int32Array<ArrayBuffer>: the inputs of a draw (a view into the shared list)
frames()
the frame scope for a context, device or renderer, in a folder of gui (one per context)
function frames(gui: GUI, target: FrameTarget, opts?: FramesOptions): FrameScope
class FrameScope
recorder: Recorderapi: GLRecorder | GPURecorder: the instrumentation on the context or devicetiming: ScopeTiming: whether passes are timed on the GPU, and if not, why: the context can't, or the app times its own (live)folder: Folderstats: { gpu: number; cpu: number; counts: FrameCounts; memory: Memory; }: what the monitors show, updated a few times a second (WebGPU calls programs pipelines, and binds bind groups)plot: Plotgui: GUIcontext: GPUDevice | WebGL2RenderingContextrenderer: ThreeRendererLike | nulldescribe(): string: its context in a few words, told from a page's others: its API, what draws with it, its canvassound(): Promise<FrameSound>: The frame played as sound (its own chunk, loaded the first time it's asked for). Experimental: the scope has no button for it, and nothing but this starts it.captures(): Promise<CaptureStore>: the captures taken (its chunk loads the first time it's asked for)capture(opts?: FrameCaptureOptions): Promise<Capture>: Capture the next frame: each pass's targets before and after, each draw's state, uniforms, inputs and geometry, and a picture after each draw. Kept incaptures().whenchooses which frame: the next, the next that does work, or the one after the next inputcaptureAndShow(when?: FrameCaptureOptions['when']): Promise<void>: capture the next frame (or aswhensays) and show it in the capture's window (the scope's capture, and the palette's)openCapture(file: Blob | ArrayBuffer | Uint8Array, name?: string): Promise<Capture>: Open a.devui-capturefile (a capture saved from the capture's window, here or on another page), and show it: its frame, draws and pictures, without the app. Its draws are drawn again in this scope's contextname(object: object, label: string): this: name a resource (a texture, framebuffer, program) for lanes and blockshold(): () => void: keep recording while held, even with the folder closed (a scope view holds it while it's on screen)recording: booleantarget(id: Id): Control | null: a lane's target as a texture row in the 'targets' folder (made the first time), or null if it's goneinspect(id: Id): Control<any> | null: open a lane's target in the texture inspectordispose(): void
interface FramesOptions
folder: string | undefined: folder path, default 'Frame'group: boolean | undefined: Share the folder with the GUI's other grouped scopes of its folder name: one shown at a time, the context picked in its bar, the pick kept for the page (devui's Chrome extension groups the scopes it attaches). Default false: each scope a folder of its own ('Frame', 'Frame 2'…), side by side, each recording while it's on screentiming: boolean | undefined: time passes on the GPU where the context can (default true; WebGPU needs the device's 'timestamp-query' feature)history: number | undefined: frames kept, for the scope's persistence, late GPU times and reading back a burst of work (default 64: about a second)
type FrameTarget
what can be attached: a WebGL2 context, a WebGPU device, or a three.js renderer
type FrameTarget = WebGL2RenderingContext | WebGLRenderingContext | GPUDevice | ThreeRendererLike | ThreeWebGPURendererLike
type Id
a resource's id: small integers, from a WeakMap on the API object
type Id = number
interface Memory
GPU memory the recorder has seen allocated, by kind
textures: numbertextureCount: numberbuffers: numberbufferCount: numberrenderbuffers: numberrenderbufferCount: numbercanvas: number: the canvas's drawing buffer (colour, depth, multisampling), estimated
Mode
a draw's primitive mode, as GL numbers it (WebGPU maps onto these)
const Mode: { readonly Points: 0; readonly Lines: 1; readonly LineLoop: 2; readonly LineStrip: 3; readonly Triangles: 4; readonly TriangleStrip: 5; readonly TriangleFan: 6; readonly Dispatch: 7; }
observeThree()
A frame scope on each three.js renderer an app makes, through three's own devtools hook: for an app that bundles its own three (no import map, no global THREE), whose classes can't be reached. three announces each WebGLRenderer as it's made (an observe event on __THREE_DEVTOOLS__), so call this before the app's bundle runs: its renderers then have scopes with three's names, and GPU memory counted from the start. onScope hears of each; the scopes made are in scopes; off() stops. const three = observeThree(gui, { history: 120, onScope: (scope) => console.log(scope.folder.path) });
function observeThree(gui: GUI, opts?: FramesOptions & { onScope?: (scope: FrameScope) => void; }): { scopes: FrameScope[]; off(): void; }
PassFlag
const PassFlag: { readonly Devui: 1; readonly Blit: 2; readonly Compute: 4; readonly CopyOut: 8; readonly Earlier: 16; readonly Split: 32; }
primitives()
primitives a draw makes: triangles, lines or points (none for a dispatch)
function primitives(mode: number, count: number): number
class Recorder
what a frame scope reads: the frames, the resources and their names
ring: FrameRecord[]: frames, oldest overwritten firstresources: Map<number, ResourceInfo>memory: Memorystrings: string[]: labels by id; 0 is no labelcurrent: FrameRecord | null: the frame being recorded (null between tasks)latest: FrameRecord | null: the last closed framelabel: number: the label put on draws from now (an integration's current object)slowing: number: devui's runs slowing the app's frames now (a capture's wrappers): the frames they're in are markedslowedonPassOpen: ((f: FrameRecord, pass: number) => void) | null: called as a pass opens (or carries on afterretime) and closes: the GPU timer begins and ends its queries thereonPassClose: ((f: FrameRecord, pass: number) => void) | nullonFrameClose: ((f: FrameRecord) => void) | null: called as a frame closes, before listeners (GPU results are polled here)onCollected: ((id: Id) => void) | null: an object of the app's collected (its id): the GL or GPU recorder's, to take its bytes out ofmemoryid(o: object | null | undefined): Id: an object's id (a framebuffer, texture, program…), made on first sightkeep(id: Id, o: object): void: keep a way back from an id to its object (weakly: the app can still let it go)object(id: Id): object | undefined: the object an id was kept for, if it's still alivepeek(o: object | null | undefined): Id: the id, without making onestring(s: string): numberfreed(id: Id): void: the app deleted a resource: its record stays for the frames that used it, until they're overwrittenline(id: Id): Id: the line a target belongs to: the first of the targets made to replace each other, else itselflive(id: Id): boolean: a resource the app has and hasn't deletedresource(id: Id, kind: ResourceInfo['kind']): ResourceInfoonFrame(fn: (f: FrameRecord, own: boolean) => void): () => undefined: Callfnas each frame closes;ownwhen that's in the recorder's own task (its posted close), between the app's tasks, rather than in an app's call (a frame the browser drew past)typical(key: string, kind: 'gpu' | 'cpu'): number: A pass's typical time by its key (passKeys), as last read: kept for 600 frames after it was last seen, far longer than the frames themselves (NaN if it wasn't). An on-demand app's full frame comes between idle ones: its passes' times outlast the ring'sonTimed(fn: (f: FrameRecord) => void): () => undefined: callfnas each frame's GPU times are all in (a few frames after it closes)recent(n: number): FrameRecord[]: frames newest first, up ton, closed ones onlyframe(serial: number): FrameRecord | null: the frame by serial, if it's still in the ringopen(): FrameRecord: the frame calls go into now, opening one (and arranging its close) if none ispass(target: Id, layer: number, flags: number, viewW: number, viewH: number, writes: ArrayLike<Id>, nWrites: number): number: The pass work goes into: a new one when the target (its lane: the texture it writes), the layer or level, or devui's part changes.writesare every texture the pass writes (MRT, depth), so passes that read them later link back here.retime(): void: The open pass's GPU timing stopped with the work of the callback it was in (the timer ends its query then): work into the pass from a later callback times it again.endPass(now?: number, late?: boolean): void: Close the open pass (a target change, its encoder's end, or the frame's:late, a task after its work). A pass ends at its last call.draw(program: Id, mode: number, count: number, instances: number, inputs: ArrayLike<number>, nIn: number): void: record a draw in the open pass; inputs are the texture ids it samplesdispatch(program: Id, workgroups: number, inputs: ArrayLike<number>, nIn: number): void: record a compute dispatch in the open pass: a block like a draw's, counted as a dispatch (its count is workgroups)read(tex: Id): void: a blit or copy reads a texture directlycopyOut(canvas: Id, x: number, y: number, w: number, h: number): void: The canvas copied out (drawImage, createImageBitmap): a pass of its own on the canvas's lane, until the copy's call returns (its caller ends it then), timed as any pass is: the copy resolves the canvas and reads it on the GPU. What's drawn next is another pass. Its rectangle is kept, with the draws before itreadBack(tex: Id): void: a texture read back to the CPU (readPixels): the pass that last wrote it, kept on this frameclear(mask: number): voidstamp(): void: the open pass's work so far ends now (a blit or a copy: one call, no draws)openPass: number: the pass work goes into now (-1: none)close(own?: boolean): void: close the frame now (normally a message posted when it opened does this:own, its own task)staleFrame(): void: The open frame, if the browser has drawn since its first pass, is done: close it now. Its close is posted to the next task, which normally runs first; but after a slow frame (a capture's) the browser may draw again before running it, and the two frames would read as one.counts: FrameCounts: counts outside a pass (uploads, binds, readbacks) still belong to a framepassCounts: FrameCounts: what the open pass's work counts towards: the frame's counts are the app's, so devui's own go nowhereresetAll(): void: forget every frame and resource (a lost context)gpuClock: ClockStep: the GPU clock's step, learned from its timestamps (a browser may round WebGPU's: core/steady.ts)cpuIn(f: FrameRecord): void: A frame closed (close calls this): each pass's typical CPU time and start, the mean of its last 15 (SteadyTimes: in any one frame the clock reads a quick pass as 0 or a step, core/clock.ts).gpuIn(f: FrameRecord): void: A frame's GPU times are all in (its timer calls this): the frame's total, and each pass's steady time, the low quarter of its last 15 readings (SteadyTimes: a reading counts the GPU's waits too), or their mean on a coarse clock (gpuClock).timesGiven(f: FrameRecord): void: a frame's GPU times came another way than its own readings (frames/webgl.ts): told to what waits for themdispose(): void
interface ResourceInfo
id: numberkind: 'texture' | 'canvas' | 'renderbuffer' | 'buffer' | 'program' | 'framebuffer'label: string | undefined: a name the app or an integration gave itwidth: number | undefinedheight: number | undefineddepth: number | undefined: depth or layers (6 for a cube)format: string | undefined: the internal format, as text (RGBA16F)textureKind: '2d' | 'cube' | '2d-array' | '3d' | undefined: a texture's kind and mip levels, for viewing itlevels: number | undefinedisDepth: boolean | undefined: a depth format (its values in .r), or an integer one (read through an int sampler)sampler: 'float' | 'int' | 'uint' | undefinedholds: 'data' | undefined: numbers, not colour, by what drew into it (an integration knows: three's shadow maps, its normal passes)target: true | undefined: a texture that's drawn into: attached to a framebuffer (WebGL), made to be rendered to (WebGPU)bytes: number | undefined: estimated GPU memory, bytesdeleted: number | undefined: the frame it was deleted in: its record (its name) stays while frames that used it are in the ringreplaces: number | undefined: A target this one was made to replace, and the reverse: one deleted as this one came, last written at about the same place in the frame, of the same kind and format (a render target resized)replacedBy: number | undefinedline: number | undefined: the first target of the line it replaces (each made to replace the one before): what a lane follows
interface ScopeTiming
whether a scope's passes are timed on the GPU, as it is now
available: booleanwhy: stringtesting: boolean: the timers' self-test isn't in yet (WebGL2, timer-test.ts): times are held back until it isoverFrames: boolean: Its times are worked out over a sweep of frames, one run of passes timed a frame (WebGL2 where a timer query can't time a pass on its own: Chrome on macOS). A sweep takes aboutsweepframessweep: number: frames a sweep takes, where times come over frames (else 0)restart(): void: Time every pass afresh: where times come over frames, from the next frame each pass's time is from readings taken since. Until a pass has been read again it shows the time it had, marked (PassFlag.Earlier). A change finished in the panel does this by itself; call it for one devui can't see (the camera, the app's own state). Elsewhere a pass's time is its last few frames' already
interface ThreeWebGPURendererLike
a three.js WebGPURenderer (after await renderer.init()): its backend's device, or the WebGL2 context it fell back to
backend: { device?: GPUDevice | null; gl?: WebGL2RenderingContext | null; }
watchFrames()
Record a WebGL2 context or a GPUDevice from now, before there's a scope to show it: the scope attached later (frames(gui, context)) knows what it made meanwhile (its textures and buffers, counted in its memory; on WebGPU, made copyable for a capture), as if it had been there from the start. Nothing shows until then. Its draws are recorded only until its first frame that draws or dispatches, which onDraw hears of: a context made only to test for support never does. Returns a stop, which lets go of it if no scope has taken it over.
function watchFrames(context: WebGL2RenderingContext | WebGLRenderingContext | GPUDevice, opts?: Pick<FramesOptions, 'timing' | 'history'> & { onDraw?: () => void; }): () => void
@haxiomic/devui/shader
interface BlockLayout
variable: stringgroup: numberbinding: numbermembers: Member[]size: number: the struct's size, rounded to its alignmentinvalid: string | undefined: a member's type isn't understood, so offsets past it are unknown: nothing is packed
interface CompileError
line: numbermessage: string
detectLang()
GLSL or WGSL, by the look of it
function detectLang(source: string): ShaderLang
layoutBlocks()
each uniform buffer's layout, in source order
function layoutBlocks(uniforms: UniformInfo[]): BlockLayout[]
interface Member
info: UniformInfooffset: numberstride: number | undefined: bytes between array elementscolumn: number | undefined: bytes between a matrix's columns
pack()
a block's values (by uniform name) into its bytes
function pack(block: BlockLayout, value: (name: string) => unknown, out?: ArrayBuffer): ArrayBuffer
parseShader()
function parseShader(source: string, lang?: ShaderLang): ParsedShader
shader()
a shader's uniforms as controls in gui (a GUI or folder)
function shader(gui: FolderLike, source: string, opts?: ShaderOptions): Shader
class Shader
uniforms: Record<string, { value: unknown; }>:{ name: { value } }, as three's ShaderMaterial takes them; the controls edit the valueslang: ShaderLanginfo: UniformInfo[]blocks: BlockLayout[]: WGSL: each uniform buffer's layouterrors: CompileError[]source: stringfolder: FolderLikeupdate(source: string): this: a new source: uniforms still there keep their values; new ones get controls; gone ones gotexture(name: string, source: unknown): this: bind a texture to a sampler uniform, and show it in the folderapply(gl: WebGL2RenderingContext | WebGLRenderingContext, program: WebGLProgram): this: raw WebGL: set the program's uniforms (call with the program in use)pack(variable?: string, out?: ArrayBuffer): ArrayBuffer: WGSL: a uniform buffer's bytes (the first one, or thevar<uniform>named)write(device: { queue: { writeBuffer(buffer: unknown, offset: number, data: ArrayBuffer): void; }; }, buffer: unknown, variable?: string, offset?: number): this: WebGPU: write a uniform buffer's bytesreportErrors(log: string | null | { messages: readonly { type: string; lineNum: number; message: string; }[]; }, source?: string): number: Compile errors from your compile: a GLSL info log (ERROR: 0:12: …) or WebGPU'sgetCompilationInfo(). They show in the folder with their lines (fromsource, if it isn't the one the shader has: a new one that failed to compile); returns how many there were.clearErrors(): thisdispose(): void: remove its controls and folder
type ShaderLang
type ShaderLang = 'glsl' | 'wgsl'
interface ShaderOptions
name: string | undefined: the folder's name (default 'Shader')lang: ShaderLang | undefined: default: by the look of the sourcelineOffset: number | undefined: lines your engine adds before the source (three adds its own prefix), so error lines match yours
interface UniformInfo
name: stringtype: UniformTypearray: number | undefined: an array of this many (not shown as a control)hints: Record<string, unknown>: annotations, as control hintsinitial: number | boolean | number[] | undefinedhidden: booleancolor: booleanline: number: 1-based source lineblock: { struct: string; variable: string; group: number; binding: number; } | undefined: WGSL: the struct it's a member of, and that struct's bindingalign: number | undefined: WGSL:size: number | undefinedopaque: { align: number; size: number; } | undefined: WGSL: a member that takes space but isn't a uniform you edit (a nested struct): its alignment and size; NaN when its type isn't understood, so its block can't be laid out
interface UniformType
a uniform's type: its base, its component count, and for WGSL its size and alignment
base: 'float' | 'int' | 'uint' | 'bool' | 'sampler': float, int, uint or bool (a matrix is float, withmatrix); sampler for texturesn: number: components (vec3: 3, mat4: 16)matrix: [number, number] | undefined: columns × rows for a matrixhalf: boolean | undefined: WGSL f16: 2-byte componentsname: string: the type as written
@haxiomic/devui/audio
audio()
audio levels, spectrum and bands in a folder of gui; bands can drive controls
function audio(gui: GUI, source: Source, opts?: AudioOptions): AudioInput
class AudioInput
context: AudioContextanalyser: AnalyserNodebands: Record<string, [number, number]>values: Record<string, number>: level (RMS) and peak in dB (−60 to 0), and each band, 0–1gui: GUIupdate(): void: read the analyser and drive the bound controls (it runs every frame by itself)bind(node: Control | { kind: string; } | null | undefined, band: string, opts?: BindOptions): () => void: drive a number control (asgui.findgives it) with a band, or 'level' (unbinding whatever drove it before)unbind(control: Control): void: stop driving it: it goes back to where it rests (its value before, or what you set meanwhile)bandOf(control: Control): string | undefined: the band driving a control, if one of this input's doesdata(): { frequency: Uint8Array<ArrayBuffer>; time: Float32Array<ArrayBuffer>; sampleRate: number; }: the spectrum and waveform, for a viewdispose(): void: stop: unbind what it drives, take its rows away, and close its own context (a media element's stays, so it plays on)
interface AudioOptions
name: string | undefined: the folder's name (default 'Audio')bands: Record<string, [number, number]> | undefined: frequency bands in Hz (default low 20–250, mid 250–2000, high 2000–16000)fftSize: number | undefined: analyser size (default 2048)
interface BindOptions
range: [number, number] | undefined: the control's values the band's 0–1 maps to (default: its range as the panel has it, or 0 to twice its value)gain: number | undefined: multiplies the band before mapping (default 1)smoothing: number | undefined: 0 follows instantly, near 1 glides (default 0.6)normalize: boolean | undefined: map the band from the range it has recently moved in, not 0–1 (default true)remember: boolean | undefined: keep this binding across reloads (the row menu's are)
@haxiomic/devui/midi
midi()
MIDI learn for gui's controls (asks the browser for MIDI access)
function midi(gui: GUI, opts?: MidiOptions): Midi
class Midi
mappings: Map<string, Live>learning: Control<any> | null: the control waiting for a knob or keyaccess: MidiAccessLike | nulllog: { messages: string; }: the last messages, newest firstwaiting: boolean: still asking the browser for accessgui: GUIreceive(data: ArrayLike<number>, from?: string): void: a MIDI message (status, data1, data2), as from an inputlearn(control: Control): void: wait for the next knob or key, and map it tocontrol(Esc, or a click anywhere, stops waiting)cancelLearn(): voidmap(control: Control, mapping: MidiMapping): voidforget(control: Control): voidforgetAll(): void
interface MidiAccessLike
what's used of the Web MIDI API
inputs: { forEach(fn: (input: MidiInputLike) => void): void; } | Map<string, MidiInputLike>onstatechange: ((e: unknown) => void) | null | undefined
interface MidiInputLike
name: string | null | undefinedonmidimessage: ((e: { data: ArrayLike<number>; }) => void) | null
interface MidiMapping
kind: 'cc' | 'note'channel: number: 1–16number: number: the CC or note numberrange: [number, number] | undefined: for a number without min and max: what the knob covers
interface MidiOptions
softTakeover: boolean | undefined: a knob takes over a value only once it reaches it (default true)access: MidiAccessLike | Promise<MidiAccessLike> | undefined: the MIDI access to use (default: navigator.requestMIDIAccess(), which asks the browser)
noteName
function noteName(n: number): string
@haxiomic/devui/remote
broadcast()
windows of one origin: the pop-out
function broadcast<Out, In>(name: string): Transport<Out, In>
client()
function client(transport: Transport<ClientMessage, HostMessage>, opts?: ClientOptions): RemotePanel
type ClientMessage
type ClientMessage = | { t: 'hello' } | { t: 'set'; path: string; value: Json; transient: boolean } | { t: 'press'; path: string } | { t: 'begin' } | { t: 'end'; cancel?: boolean } | { t: 'materialise'; path: string } /** still here (every few seconds): an app that stops hearing these shows the panel as gone */ | { t: 'ping' } | { t: 'bye' }
interface ClientOptions
fill: boolean | undefined: the panel fills the window (a pop-out, a phone): default true
type HostMessage
type HostMessage = | { t: 'structure'; title: string; nodes: NodeSnap[] } | { t: 'values'; values: Record<string, Json> } | { t: 'plots'; samples: Record<string, (number | Record<string, number>)[]> } | { t: 'bye' }
type NodeSnap
type NodeSnap = | { k: 'folder'; path: string; label: string; collapsed: boolean; lazy: boolean; order: number } | { k: 'control'; path: string; label: string; type: string; view: string; spec: Json; value: Json; readonly: boolean; hidden: boolean; order: number; description?: string } | { k: 'action'; path: string; label: string; order: number; description?: string; key?: string } | { k: 'plot'; path: string; l…
popOut()
The panel in a window of its own (a second monitor). It's this GUI's own panel, drawn there by the window's own copy of devui's UI, so everything in it works as it does here: texture views and inspectors, the frame scope, captures. This window's panel comes down until that one closes.
function popOut(gui: GUI, features?: string): { window: Window; close(): void; }
popOutWindow()
One of the panel's windows in a browser window of its own, filling it (a capture on a second monitor): id is its id in the layout (gui.layout.windows). It's drawn there as popOut draws the panel, with any window opened from it meanwhile; the panel and the rest stay here, and it comes back when that window closes.
function popOutWindow(gui: GUI, id: string, features?: string): { window: Window; close(): void; }
qrPath()
a QR code as an SVG path (one unit per module), with its size
function qrPath(text: string): { d: string; size: number; }
relay()
Through the dev server (devui's Vite plugin): messages posted to a room reach the others in it, as server-sent events. base is the server's devui endpoint, e.g. http://192.168.1.5:5173/__devui/.
function relay<Out, In>(base: string, room: string): Transport<Out, In>
interface RemotePanel
gui: GUIclose(): void
resumeShare()
this tab was sharing before it reloaded: share the same room again, so the device carries on
function resumeShare(gui: GUI): Promise<Share> | null
serve()
function serve(gui: GUI, transport: Transport<HostMessage, ClientMessage>): Session
interface Session
close(): voidonStatus(fn: (connected: boolean) => void): () => void: the remote said hello, or went
interface Share
url: string: the address to open on the other devicesession: Sessionclose(): void
shareToDevice()
The panel on another device: joins a new room on the dev server's relay and shows a QR code (and the address) to open it with. Needs devui's Vite plugin, and the dev server reachable on your network (vite --host).
function shareToDevice(gui: GUI, opts?: { dialog?: boolean; room?: string; resumed?: boolean; }): Promise<Share>
interface Transport
a way to talk: a BroadcastChannel, or the dev server's relay
Out: anyIn: anysend(message: Out): voidonMessage(fn: (message: In) => void): () => voidclose(): void
@haxiomic/devui/react
DevUIProvider()
use a GUI other than the default gui below this point
function DevUIProvider({ gui, children }: { gui: GUI; children?: ReactNode; }): import("react").FunctionComponentElement<import("react").ProviderProps<GUI | null>>
releaseGraceMs
const releaseGraceMs: 250
useButton()
a button; fn can be a fresh closure each render
function useButton(label: string, fn: () => void, hints?: Hints & Where): Control | null
useControl()
bind one property (type, range and view inferred); null until mounted
function useControl<T extends object, K extends keyof T & string>(target: T | null | undefined, key: K, hints?: Hints & Where): Control | null
useFolder()
the folder at a path (created on first use)
function useFolder(path: string, hints?: Hints): Folder
useGUI()
function useGUI(): GUI
useMonitor()
a read-only readout of a property, polled while on screen; view: 'graph' plots it
function useMonitor<T extends object, K extends keyof T & string>(target: T | null | undefined, key: K, hints?: Hints & PlotOptions & Where): Control | Plot | null
useParams()
Declare parameters in a folder and get the live object back. The same object across renders (and StrictMode's double render); editing the schema under Fast Refresh rebinds it, keeping tweaks. Re-renders the component on change unless rerender: false (then read it in a frame loop).
function useParams<S extends Record<string, unknown>>(folder: string, schema: S, opts?: { rerender?: boolean; }): Params<S>
usePlot()
a live plot: push values from your frame loop with plot?.push(v)
function usePlot(label: string, opts?: PlotOptions & Where): Plot | null
useTexture()
a texture view of anything an extension can resolve (canvas, raw WebGL texture, three.js texture…)
function useTexture(label: string, source: unknown, hints?: Hints & Partial<TextureViewState> & Where): Control | null
useValue()
the current value of a bound property; re-renders when it changes (from the panel or code)
function useValue<T extends object, K extends keyof T & string>(target: T, key: K, hints?: Hints & Where): T[K]
type Where
where a hook's control goes: a folder path ('A/B'), or the root
type Where = { folder?: string }