UltraVisiondocs

Changing it

Library controls

The Adjust panel is not written per template. Each motion declares the controls it has, and the panel renders that declaration — which is why every family shows exactly its own settings and no dead ones, and why adding a control is a one-line change with no UI to touch.

Controls are declared in two places in this app, and they are edited in different files for different reasons. This page is the motion side. The studio side is Mockup controls.

The vocabulary is closed

A template may only use these 9 types. The limit is the point: a motion cannot invent a widget the panel does not know how to draw, so every motion stays fully editable, thumbnailable and exportable by the same code.

The controls below are live, and they are the editor’s own — the same component the Adjust panel mounts, not a drawing of it. Drag them. The readout beside each one is the value your transform would read that instant, which is the whole contract between the panel and the motion.

sliderA number in a range.
12°

The whole track is the control — press anywhere on it and drag. Hold Shift while dragging for a grid ten times finer than the declared step. Click the value to type an exact one; double-click the track to snap back to the declared default. Focus it and the arrow keys nudge by a step, or by a tenth of one with Shift held.

the transform readsvalues.tilt = 12
sliderA range that spans zero.
0%

The same type, declared with a negative minimum, and it reads differently for it: the fill starts at ZERO and runs out to the value, so right of centre is positive, left is negative, and zero is empty. Filling from the left edge instead made a neutral control look half-on — a bend of 0 showed a half-full track.

the transform readsvalues.bend = 0
toggleTwo states, both visible.

Use it when the two options are opposites and the reader should see both at once. It is a segmented pair rather than a checkbox because "Forward / Reverse" has no natural off state.

the transform readsvalues.direction = 'Forward'
pillsOne of a few, all visible.

The same shape as the toggle, extended. Good up to about four options — past that the row gets cramped and a select reads better. A mode pill is usually what visibleWhen keys off, so the panel can hide settings the current mode does not use.

the transform readsvalues.mode = 'Rotate'
selectOne of many, folded away.

A dropdown, for lists too long to lay out. Same data as pills — an options array and a string default — so switching between the two is a one-word edit if a list grows.

the transform readsvalues.blend = 'Normal'
directionA direction, picked spatially.

A 3×3 pad of edges and corners. It exists because "left / top-right / bottom" as a text list makes the reader translate words into geometry — here the control has the same shape as the thing it describes. Families that push content in from an edge use this.

the transform readsvalues.from = 'left'
colorA colour, as a hex string.
#111827

The swatch opens the platform picker and the value is stored as a hex string. Worth noting: this is for colour a MOTION owns. The interface’s own colours never come from a control — they live in the token sheet.

the transform readsvalues.tint = '#111827'
xypadTwo numbers that belong together.
XY
XY

Drag the dot; the value is { x, y }. Reach for it when the two numbers are one gesture — a position, a nudge — rather than two independent settings. Two sliders would be more precise and much worse to aim.

the transform readsvalues.offset = { x: 0, y: 0 }
uploadA file or a URL.

Gives the motion its own image slot, separate from the card list — a watermark, a badge, a mask. The value is a reference, so it survives a reload with the rest of the project.

the transform readsvalues.logo = ''
textA string.

For motions that draw type. Plain and deliberately unstyled: the template decides how the string is rendered, the control only collects it.

the transform readsvalues.caption = 'UltraVision Studio'

The slider has more in it than a track

It carries most of the controls in the app, so it earned some depth. None of this is configured per template — declare type: 'slider' and you get all of it.

GestureDoes
Drag anywhere on the trackSets the value. The whole 34px row is the control, not a thin rail with a knob to hit.
Hold Shift while draggingFine adjustment — the grid becomes a tenth of the declared step, so a step-1 slider lands on 12.4.
Click the valueTypes an exact number. It snaps to the same grid a drag uses, so the readout can never show 19 while storing 18.5.
Arrow keys, track focusedNudges by one step; with Shift, by a tenth. Enter opens the typed field.
Arrow keys while typingNudges from what is typed, not from what is stored — and Shift means the finer grid here too.
Double-click the trackBack to the control’s declared default.

The fine adjustment is why a control can declare a coarse step without becoming imprecise. A rotation in whole degrees is the right default for dragging, and Shift is there for the one shot that needs 12.4.

Two details that look cosmetic and are not. A drag belongs to the track that was pressed: without that, a pointer held down and swept across a panel rewrote every slider it crossed. And the typed field is a text input with a decimal input mode rather than a number input, because a number input refuses to report a selection — so the first keystroke landed beside the old value instead of replacing it, and 340 followed by 5 became 3405.

What one declaration looks like

Every control above is one object in the template’s controls array. The full set of fields:

{
  key: 'tilt',            // unique within the template; transform reads values.tilt
  label: 'Tilt',          // what the panel shows
  type: 'slider',
  min: 0, max: 45, step: 1,
  default: 12,            // also the value a template switch resets to
  section: 'Motion',      // Layout | Motion | Depth | Finish — groups the panel
  unit: '°',              // ° % px × s — drawn next to the number
  precision: 0,           // decimals shown
  description: 'How far each card leans out of the plane.',
  visibleWhen: { key: 'mode', equals: 'perspective' },
  advanced: true,         // folded away until Advanced is opened
}

section is what groups the panel, so a new control lands in the right block without any layout work. visibleWhen is how a family hides settings that do not apply to the mode it is in — cheaper and clearer than shipping two near-identical templates. advanced keeps the long tail out of the way without hiding it.

Adding or changing one

  • Add the entry to the template’s controls array.
  • Read it in the transform as values.yourKey.
  • That is all. The panel row, the default, the reset-on-switch, the undo step, the thumbnail and the export all follow from the declaration.

Changing a default changes what a fresh pick of that template looks like. Changing a key does something else entirely: keys are what saved projects reference, so renaming one silently drops that value from every scene already on disk. Labels are free to change; keys and template ids are not.

A new preset is a new set of defaults

Most “new motions” are not new motions. A preset is the same transform shipped with different declared defaults, built by a small helper that patches them — so a family of six presets is one piece of code and six value bundles. Because picking a template does a full reset from the declaration, the preset look comes out for free.

Two traps if you write a preset by hand rather than through the helper: a preset of a 3D family that loses its 3D pose function renders flat while its base renders in perspective, and a preset of a lattice family that loses its layer-count function falls back to reading a card-count control those families do not have — six cards where a wall was expected. Both are silent.

Mockup controls →