Controls reference
The full Hypercolor SDK controls API: shorthand inference, every factory, groups, the speed magic, palettes, presets, and build-time meta.
Controls are what a user adjusts on a live effect: sliders, dropdowns, color pickers, toggles, font menus, sensor pickers, viewport rectangles. The SDK gives you a shorthand for quick declarations and explicit factories for anything that needs a tooltip, a group, a custom default, or a non-numeric type.
Every factory returns a ControlSpec that both canvas() and effect() consume. The SDK builds the UI, serializes the declarations into HTML <meta> tags at build time, and resolves the live values before handing them to your render function. You declare controls once; the studio panel, the daemon UI, the validator, and the runtime all read from that single declaration.
For the authoring path these controls plug into, see TypeScript canvas effects. For where they show up to a user, see Effects and controls in the studio.
πControl types at a glance
There are nine control types. The discriminated union lives in controls/specs.ts; this table is its public face. Pick the factory in the right-hand column, or reach for the inferred shorthand where one exists.
| Type | Factory | Runtime value | Inferred shorthand |
|---|---|---|---|
| Number slider | num | number | [min, max, default] |
| Combobox | combo, paletteControl, font | string (or PaletteFn when tagged) | readonly string[] |
| Toggle | toggle | boolean | boolean |
| Color | color | '#rrggbb' | '#rrggbb' |
| Hue | hue | number (degrees) | none |
| Text | text | string | non-# string |
| Sensor | sensor | sensor label string | none |
| Rectangle | rect | { x, y, width, height } | none |
| Asset | asset | media reference string | none |
Hue, sensor, rect, and asset have no literal shape, so they exist only as factories. Everything else has both a shorthand and a factory.
πShorthand inference
For quick drafts, declare a control by value shape alone and let the SDK infer the type. Inference is shape-based: the SDK looks at the literal you wrote and picks a control type.
| Value shape | Inferred control | Example |
|---|---|---|
[min, max, default] | Number slider | speed: [1, 10, 5] |
[min, max, default, step] | Number slider with a step | density: [0, 1000, 200, 10] |
readonly string[] | Combobox (first is default) | palette: ['Fire', 'Ice'] |
boolean | Toggle | mirror: false |
'#rrggbb' | Color picker | tint: '#80ffea' |
'plain string' | Text field | label: 'HELLO' |
number (bare) | Number slider, range 0-100 | opacity: 80 |
Two details from the inference rules are worth pinning down. A bare number such as opacity: 80 becomes a slider with range 0-100 whose default is the value you wrote. A string is a color only when it starts with #; every other string becomes a text field.
Inferred controls derive their label from the key by splitting camelCase into words: trailLength becomes "Trail Length", edgeGlow becomes "Edge Glow". Anything the inference table canβt classify (an empty array, a null, an object) throws a build error naming the offending key, so a bad shorthand fails loud instead of rendering a broken control:
Cannot infer control type for "weird". Expected [min, max, default],
string[], boolean, '#hex', string, or number. Got: {}Shorthand is great for throwaway effects and prototypes. Upgrade to factories the moment you need a tooltip, a group, a non-first default, or any type that has no literal shape: hue, sensor, asset, rect.
πFactory functions
Import the factories you need from hypercolor:
import {
asset,
color,
combo,
font,
hue,
num,
paletteControl,
rect,
sensor,
text,
toggle,
} from "hypercolor";πnum(label, [min, max], default, options?)
Number slider.
density: num("Particle Density", [10, 1000], 200, {
step: 10,
tooltip: "How many particles live in the field",
group: "Simulation",
});Options:
stepβ slider incrementtooltipβ hover tooltip textgroupβ UI grouping labelnormalizeβ'speed','percentage', or'none'. Applies an internal normalization before the value reaches your render function or the shader uniform.uniformβ override the auto-derivediSpeed-style GLSL uniform name
πcombo(label, values, options?)
Dropdown selector. The runtime value is the selected string.
shape: combo("Shape", ["Circle", "Square", "Hexagon"], {
default: "Hexagon",
group: "Geometry",
});Options: default (otherwise the first value wins), tooltip, group, uniform.
The palette function comes from paletteControl, not the key name. A plain combo('Palette', ['A', 'B', 'C']), or the shorthand palette: ['A', 'B', 'C'], stays a raw string at runtime. The automatic PaletteFn (canvas) and integer index (shaders) come from the dedicated paletteControl() factory, which tags the spec with meta.palette = true. The key name palette is not magic. If you have a plain combobox and want the function, call createPaletteFn(name) inside your draw. See Palettes.
πpaletteControl(label, values, options?)
Palette picker. Returns a combobox spec tagged with meta.palette = true. That flag is what drives both the studioβs palette-picker UI and the palette-function conversion at runtime, described under Palette controls. Same option set as combo(): default, tooltip, group, uniform.
palette: paletteControl("Palette", ["Aurora", "Fire", "Ocean"], {
group: "Color",
});πcolor(label, default, options?)
Color picker, produces a hex string.
tint: color("Tint", "#80ffea", { group: "Color" });Canvas effects receive '#rrggbb' strings. Shader effects receive a vec3 uniform with components in the 0.0β1.0 range.
πtoggle(label, default, options?)
Boolean toggle.
mirror: toggle("Mirror Mode", false, {
tooltip: "Reflect the effect horizontally",
});In shaders, booleans become integer uniforms (0 or 1).
πhue(label, [min, max], default, options?)
Hue-angle slider, typically [0, 360]. Semantically a num, but the UI draws a hue-gradient track so the value reads as a position on the color wheel.
baseHue: hue("Base Hue", [0, 360], 270, { group: "Color" });πtext(label, default, options?)
Single-line text input. Useful for display faces, labels, and any effect that renders typed content.
message: text("Display Text", "HYPERCOLOR", { group: "Content" });πfont(label, defaultFamily, options?)
Font family picker. This is syntactic sugar over combo() β it produces a combobox spec whose values are font family names. The face runtime loads the selected family before the first render, so the glyphs are ready by frame one. Capture mode disables remote font loading.
family: font("Family", "JetBrains Mono", { group: "Typography" });If you omit families, the SDK uses a curated, LED-legible default list: JetBrains Mono, Inter, Orbitron, Audiowide, Bebas Neue, DM Sans, Exo 2, Roboto Condensed, Rajdhani, Space Mono, Space Grotesk, and Sora. When you pass your own families list and the defaultFamily is not already in it, the factory prepends the default automatically so the menu always opens on a valid selection.
Options:
familiesβ override the curated list with your own familiestooltip,group
πsensor(label, default, options?)
Sensor picker. The user chooses from the live system sensors the daemon exposes (CPU temperature, GPU load, network throughput, and so on). The runtime value is the sensor label string, not a reading β pass it to the engineβs sensor API to get the current value each frame.
import { face, sensor } from "hypercolor";
export default face("Temperature", {
target: sensor("Sensor", "cpu_temp"),
});Inside the render function, resolve the label to a live reading. getSensorValue hands back a { value, min, max, unit } object (or null when the sensor is unavailable); the live number is reading.value:
const label = controls.target as string;
const reading = engine.getSensorValue(label);
const current = reading?.value ?? 0;Sensor controls are the backbone of data-driven display faces: a gauge that tracks gpu_load, a bar that breathes with cpu_temp. Options: tooltip, group.
πasset(label, mediaKind?, options?)
User media picker. The user supplies an image, video, or Lottie animation, and the control hands your effect a reference to it. The mediaKind argument scopes what the picker accepts.
backdrop: asset("Backdrop", "image", { group: "Content" });mediaKind is one of 'any' (the default), 'image', 'video', or 'lottie'. Options: default (a starting asset reference, an empty string when omitted), tooltip, group. Asset controls are the one control type exempt from the shader uniform-binding check, since user media has no scalar uniform form.
πrect(label, default, options?)
Interactive viewport rectangle. The user drags a rectangle over a live preview, and the value is { x, y, width, height } in normalized [0, 1] coordinates β resolution-independent, like every spatial value in Hypercolor.
viewport: rect(
"Viewport",
{ x: 0.1, y: 0.1, width: 0.8, height: 0.8 },
{
aspectLock: 16 / 9,
preview: "screen",
},
);Options:
aspectLockβ lock the aspect ratio while the user dragspreviewβ'screen','web', or'canvas', picking the backdrop the picker draws the rectangle overtooltip,group
πGroups
Grouping keeps the panel readable once an effect carries more than three or four controls. The group option on any factory files the control under a collapsible header:
{
bloom: num('Bloom', [0, 100], 62, { group: 'Color' }),
nucleus: num('Nucleus', [0, 100], 55, { group: 'Color' }),
palette: paletteControl('Palette', ['Aurora', 'Fire', 'Ocean'], { group: 'Color' }),
rotation: num('Rotation', [-10, 10], 4, { group: 'Motion' }),
speed: num('Speed', [1, 10], 5, { group: 'Motion' }),
petals: combo('Petals', ['6', '8', '12'], { group: 'Shape' }),
}Group names are arbitrary strings. Pick them from the domain of the effect (Color, Motion, Shape) rather than generic labels like βSettingsβ.
πReading values
Controls reach your render function as a Record<string, unknown>. Cast each to its expected type:
const speed = controls.speed as number;
const palette = controls.palette as string;
const mirror = controls.mirror as boolean;
const viewport = controls.viewport as {
x: number;
y: number;
width: number;
height: number;
};The runtime re-polls control values during playback (every 0.1s, or immediately when the panel marks them dirty), so a slider drag updates the next frame without a reload. Most values arrive raw; the exceptions are a control keyed speed and any paletteControl, described next.
πAuto-normalized speed
One control name triggers automatic normalization. The SDKβs MAGIC_NAMES table holds exactly one entry: speed. When a control is keyed speed and you havenβt set an explicit normalize option, its value runs through normalizeSpeed() β max(0.2, (speed / 5) ** 1.5), a multiplier in the 0.2β3.0 range β before your function or the shader sees it, so a slider value of 5 maps to a 1.0 time multiplier. In shaders it surfaces as uniform float iSpeed. No other key is normalized automatically.
To opt out, either rename the key (for example speedMult) and normalize the value yourself, or set an explicit normalize: 'none' on the factory. The normalize option on num also lets you apply 'speed' or 'percentage' normalization to a differently-named control on purpose. Normalization keys off the resolved hint, which is your explicit option first and the MAGIC_NAMES lookup only as a fallback.
'percentage' normalization maps a 0β200 slider to a 0β2 multiplier via normalizePercentage() (max(0.01, value / 100)), so 100 reads as 1.0. It is the right hint for βintensityβ or βscaleβ controls you want centered on unity.
πPalette controls
Palette conversion is a separate, opt-in mechanism that has nothing to do with the controlβs key name. A combobox carries the palette behavior only when its spec is tagged meta.palette = true, which is what the paletteControl() factory does. For a tagged palette control, the runtime replaces the selected string with a PaletteFn in canvas effects and an integer index (uniform int iPalette) in shaders.
| Mechanism | Canvas behavior | Shader behavior |
|---|---|---|
speed key (no explicit normalize) | Normalized through normalizeSpeed() | Normalized, exposed as uniform float iSpeed |
paletteControl() (meta.palette) | Replaced with a PaletteFn | Replaced with index, exposed as uniform int iPalette |
A plain combo(), the shorthand palette: ['A', 'B', 'C'], or any other untagged combobox leaves the value as a plain string. Recover a palette function with createPaletteFn(name) in your draw. See Palettes for the registry and the Oklab sampling internals.
Uniform names for shader effects derive from the control key as i + PascalCase: trailLength becomes iTrailLength, edgeGlow becomes iEdgeGlow. Override with the uniform option. See GLSL shader effects for the full uniform contract, including the build-time check that every non-asset control has a matching i<Key> uniform.
πPresets
Presets live in the options object passed to canvas() / effect() / face(), not in the controls map. Each preset is a named bundle of control values:
export default canvas("Prism Choir", controls, draw, {
presets: [
{
name: "Rose Window",
description: "Twelve petals, slow rotation, generous bloom",
controls: {
bloom: 70,
nucleus: 55,
palette: "SilkCircuit",
petals: "12",
rotation: 3,
},
},
{
name: "Hex Choir",
description: "Six-petal mode with a steady hexagonal pulse",
controls: {
bloom: 55,
nucleus: 60,
palette: "Aurora",
petals: "6",
rotation: 2,
},
},
],
});Aim to set every control in every preset. Partial presets are allowed, but users expect a preset to fully reset the effect, so leaving controls unset leads to surprising carry-over from whatever was selected before. Preset names render as buttons in the studio and the daemon UI.
πBuild-time metadata
On build, the SDK serializes every control declaration into HTML <meta> tags that the daemon parses directly. You never hand-write these, but reading the output helps when a control isnβt showing up the way you expect:
<meta property="speed" label="Speed" type="number" min="1" max="10" default="5" group="Motion" />
<meta property="palette" label="Palette" type="combobox" values="Aurora,Fire,Ocean" default="Aurora" group="Color" />
<meta property="backdrop" label="Backdrop" type="asset" default="" group="Content" media-kind="image" />
<meta preset="Rose Window" preset-description="Twelve petals" preset-controls='{"bloom":70,"palette":"SilkCircuit"}' />Each control becomes one <meta property=β¦ type=β¦ /> tag carrying whatever attributes it declared: min/max/step for numbers, values for comboboxes, aspectLock/preview for rectangles, media-kind for assets, plus the shared label, default, tooltip, and group. A rect default serializes as the comma-joined "x,y,width,height" string. Each preset becomes one <meta preset=β¦ preset-controls='{json}' /> tag.
On the daemon side these tags deserialize into the effectβs controls array and presets, which is what the REST API hands back. GET /api/v1/effects/{id} returns those declarations, and the live control values for the active effect are exposed through the effects domain alongside a controls-version token you can echo for optimistic updates. See the REST reference for the full effect surface.
Run the validator on the built artifact before installing. It parses these tags and flags missing fields, duplicate control ids, invalid ranges, malformed preset JSON, unknown control types, and unknown media kinds, before the daemon ever loads the artifact. (The shader uniform-binding check is a separate, earlier gate: the build step itself fails if a non-asset control has no matching i<Key> uniform, so a built artifact has already cleared that one.)
bunx hypercolor validate dist/*.htmlSee Dev workflow for the build, validate, and install loop and every CLI flag.