Appearance
User Interface
Build 2D panels – labels, sliders, buttons, stacks – from a script, place them in the scene or pin them to the screen, and drive them from your logic. For text that tracks a variable live, see Live labels below.
Build and place a panel
Compose a view tree with the UI.* builders, wrap it with createPanel(...), and instantiate it with scene.createEntity:
javascript
var panel = createPanel(
UI.vStack({
spacing: 12,
children: [
UI.label({ text: 'Volume' }),
UI.slider({ name: 'vol', value: 0.5, range: [0, 1], sizing: { width: 'fill' } })
]
}),
{ panelSize: [320, 160], material: 'system' }
).name('controls');
var entity = await scene.createEntity(
panel.anchor(Anchor.screenSpace({ alignment: 'bottom', insets: 24 }))
);Builders: UI.label, UI.button, UI.slider, UI.toggle, UI.textField, UI.progressView, UI.spacer, and the layout stacks UI.vStack / UI.hStack / UI.zStack. Every builder takes a name (for lookup), padding, and sizing ('auto' / 'fill' / a number / a '50%' string).
Anchor.screenSpace(...) pins the panel to a screen edge or corner; anchor it in the world instead (e.g. Anchor.position(...)) for a panel that lives in the scene.
Find and update views
Name a view, then reach it later through the object's representation to read or change it:
javascript
var view = entity.representation.findView({ name: 'vol' });
view.setProperty('value', 0.8);setProperty / getProperty accept keys appropriate to the view kind (a label's text, a slider's value, a button's title, …). Setting a key the kind doesn't support is a no-op.
React to input
Subscribe to a view's events:
javascript
entity.representation.findView({ name: 'vol' }).on('valueChanged', function(e) {
console.log('slider is now', e.uiValue);
});
entity.representation.findView({ name: 'save' }).on('tap', function() {
scene.setVariable('saved', true);
});Bind a control to a variable
bind connects a view's value to a variable in both directions – the control writes the variable as the user drags, and the view updates if the variable changes elsewhere:
javascript
entity.representation.findView({ name: 'vol' }).bind('value', 'volume');Pair a bound control with a live label (below) to show its value.
Live labels
Label text supports inline variable references that resolve live – the label re-renders automatically whenever the referenced variable changes. No scene.on('variableChange', …) wiring needed.
javascript
UI.label({ text: 'vol = ${vol}' })${vol} is read from the current segment's variable storage. If vol is unset, the label renders vol = UNDEFINED.
Scopes
| Form | Reads from |
|---|---|
${name} | Segment-scoped variable (default) |
${g:name} | Experience-scoped (global) variable |
Set scope on the variable side via scene.setVariable(name, value, { scope: 'experience' }).
Filters
Pipe the value through a formatter with |filter or |filter(arg):
| Filter | Use | Example output (input 0.6789) |
|---|---|---|
|fixed(N) | Trim a float to N decimals (default 2) | 0.68 |
|percent(N) | Multiply ×100, append % (default 0 decimals) | 68% |
|time | Seconds → m:ss (or h:mm:ss ≥ 1h) | (for 185) 3:05 |
|int | Truncate toward zero | 0 |
|signed(N) | Always-signed, N decimals (default 0) | +1 |
Filters are numeric – applied to a non-numeric variable, they pass the raw value through unchanged. Unknown filter names log a console warning and do the same.
Example: bound slider with live readout
The slider drives the variable; the label reads from it – no subscription code, the label re-renders on every change automatically:
javascript
// Attach to: On Experience Start
scene.setVariable('vol', 0.5);
var panel = createPanel(
UI.vStack({
spacing: 12,
padding: 16,
children: [
UI.label({ text: 'Volume: ${vol|fixed(2)}' }),
UI.slider({ name: 'volume', value: 0.5 })
]
}),
{ panelSize: [400, 160] }
);
var entity = await scene.createEntity(panel);
entity.representation.findView({ name: 'volume' }).bind('value', 'vol');A single label can mix any number of references and plain text; each reference subscribes independently, so any one variable changing re-renders with the current values of all of them:
javascript
UI.label({ text: 'Score: ${g:score|int} , Timer: ${countdown|time}' })Sizing panels with dynamic labels
A label re-measures itself when its value changes, so its own frame grows to fit wider text. But an auto-sized panel does not – it measures its outer bounds once, from the content present when it is first built. If you seed a label empty (or short) and fill it in later with a wider value, the panel stays at its original small size and the text truncates to ….
For a panel whose interpolated content changes width at runtime – a live readout, a counter that gains digits, a place name – give the panel a fixed size rather than relying on auto-sizing:
javascript
createPanel(content, { panelSize: [340, 200] });Fixed panels are unaffected: the labels have room to grow within them. Auto sizing is best kept for panels whose content width is known up front and does not change.