Appearance
User Interface Beta
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
An auto-sized panel re-measures itself when its content changes, so a label that starts empty and later fills in with a wider value makes the panel grow to fit. You can seed a readout with a placeholder and replace it once real data arrives without the text being cut off.
A fixed size is still worth choosing when you want the panel to stay put:
javascript
createPanel(content, { panelSize: [340, 200] });A live readout whose value changes width – a counter gaining digits, a place name, a temperature – will otherwise resize its panel each time, which can read as restless if the panel sits in a fixed spot next to other content. Fixed sizing also gives labels room to grow within a stable outline, and is required if you want the panel to be scrollable.