lens-studio-2d-ui
Lens Studio 2D UI — Reference Guide
Lens Studio's 2D UI layer uses ScreenTransform components to position elements in screen space. All screen-space elements are children of a Full Frame Region or Safe Render Region at the top of the scene hierarchy.
ScreenTransform — Coordinate System
ScreenTransform uses a normalised coordinate system:
- Origin
(0, 0)= centre of the parent region (1, 1)= top-right corner(-1, -1)= bottom-left corner
const st = this.sceneObject.getComponent('Component.ScreenTransform')
Anchors
Anchors pin an element to a region of its parent. Both anchors.setMin and anchors.setMax use a [0, 1] space (not the ScreenTransform [-1, 1] space):
// Fill the entire parent
st.anchors.setMin(new vec2(0, 0))
st.anchors.setMax(new vec2(1, 1))
// Pin to the top-right corner only
st.anchors.setMin(new vec2(1, 1))
st.anchors.setMax(new vec2(1, 1))
// Top half of parent
st.anchors.setMin(new vec2(0, 0.5))
st.anchors.setMax(new vec2(1, 1))
Offsets
Offsets add inset (in canvas units) from each anchor edge:
st.offsets.setLeft(10) // 10 units inset from left anchor
st.offsets.setRight(-10) // 10 units inset from right anchor
st.offsets.setBottom(20)
st.offsets.setTop(-20)
Position and size (point anchor)
When both anchors are the same point, the element is positioned by position and sized by size:
st.anchors.setCenter(new vec2(0.5, 0.5)) // pin to parent centre
st.position = new vec2(0, 0.3) // offset 30% up
st.size = new vec2(300, 60) // 300×60 canvas units
Coordinate Conversions
// Screen-space (pixels) ← ScreenTransform local
const screenPx: vec2 = st.localPointToScreenPoint(new vec2(0, 0))
// ScreenTransform local ← screen-space pixels
const local: vec2 = st.screenPointToLocalPoint(touchPosScreenPx)
// World space (3D) ← ScreenTransform local
const worldPt: vec3 = st.localPointToWorldPoint(new vec2(0, 0))
// ScreenTransform local ← World space
const localPt: vec2 = st.worldPointToLocalPoint(hit.position)
ScreenImage
ScreenImage renders a texture on a 2D quad in screen space.
const screenImage = this.sceneObject.getComponent('Component.Image')
// Assign a texture
screenImage.mainPass.baseTex = myTexture
// Tint color (RGBA)
screenImage.mainPass.baseColor = new vec4(1, 0.5, 0, 1)
// Reset to white (no tint)
screenImage.mainPass.baseColor = new vec4(1, 1, 1, 1)
// Show/hide
screenImage.enabled = false
Stretch modes (set in the Inspector)
| Mode | Behaviour |
|---|---|
| Stretch | Fills the ScreenTransform bounds, ignoring aspect ratio |
| Fit | Letterboxes to preserve aspect ratio |
| Fill | Crops to fill the bounds while preserving aspect ratio |
| Pixel Perfect | 1:1 pixel mapping (for UI icons) |
Text Component
const textComponent = this.sceneObject.getComponent('Component.Text')
// Set text content
textComponent.text = 'Score: ' + score
// Change font size
textComponent.size = 48
// Alignment
textComponent.horizontalAlignment = HorizontalAlignment.Center
textComponent.verticalAlignment = VerticalAlignment.Center
// Color
textComponent.textFill.color = new vec4(1, 1, 1, 1) // white
// Bold / italic via the text property (HTML-style tags)
textComponent.text = '<b>Bold</b> and <i>italic</i>'
Setting text from untrusted sources (network data, user content)
Never assign network or user-provided strings directly — unclosed HTML tags crash the renderer. Strip tags first:
// Strips HTML-style tags and caps length before displaying untrusted content
function safeSetText(component: Text, value: string, maxLength = 200): void {
const stripped = (value ?? '')
.slice(0, maxLength) // cap length
.replace(/<[^>]*>/g, '') // strip all tag-like sequences
component.text = stripped
}
// Usage — safe even if serverName contains malicious tags:
safeSetText(textComponent, serverName)
Touch Input
TapEvent (phone lenses, simple tap)
const tapEvent = this.createEvent('TapEvent')
tapEvent.bind((eventData) => {
const screenPos: vec2 = eventData.getPosition() // screen pixels
print('Tapped at: ' + screenPos.x + ', ' + screenPos.y)
handleTap(screenPos)
})
TouchComponent (multi-touch, drag, phone lenses)
const touchComponent = this.sceneObject.getComponent('Component.TouchComponent')
// Touch start
touchComponent.addMTouchStartCallback((eventData) => {
const pos = eventData.getPosition()
print('Touch start at: ' + pos.x + ', ' + pos.y)
})
// Touch move (drag)
touchComponent.addMTouchMoveCallback((eventData) => {
const pos = eventData.getPosition()
drawAtPosition(pos)
})
// Touch end
touchComponent.addMTouchEndCallback((eventData) => {
print('Touch ended')
finalizeStroke()
})
ScreenRegionComponent (define touch-active area)
ScreenRegionComponent on a scene object marks it as a specific region type. Use it to prevent touches from passing through your UI:
// In Inspector: add ScreenRegionComponent, set region type to TouchBlocking
// This stops the world from receiving taps that hit the UI panel
UI Buttons for Phone Lenses
For phone lenses (not Spectacles), use tap regions rather than SIK PinchButton:
// Pattern: invisible ScreenImage + TouchComponent as a button
@component
export class TapButton extends BaseScriptComponent {
@input label: string = 'Button'
onTapped: (() => void) | null = null
onAwake(): void {
const touch = this.sceneObject.getComponent('Component.TouchComponent')
touch.addMTouchStartCallback(() => {
if (this.onTapped) this.onTapped()
})
}
}
LSTween for UI Animations
Use LSTween (part of the Spectacles Interaction Kit) for smooth UI transitions:
import { LSTween } from 'SpectaclesInteractionKit.lspkg/Utils/LSTween/LSTween'
// Fade in a screen image
LSTween.colorTo(screenImage, new vec4(1, 1, 1, 1), 0.3).start() // fade to opaque
// Fade out
LSTween.colorTo(screenImage, new vec4(1, 1, 1, 0), 0.3).start() // fade to transparent
// Slide in from the right
const startPos = new vec2(1.5, 0) // off screen right
const endPos = new vec2(0, 0) // on screen centre
screenTransform.position = startPos
LSTween.moveToScreen(screenTransform.sceneObject, endPos, 0.4)
.easing(TWEEN.Easing.Quadratic.Out)
.start()
// Scale pop (bounce-in a button)
LSTween.scaleTo(this.sceneObject, new vec3(1, 1, 1), 0.2)
.easing(TWEEN.Easing.Back.Out)
.start()
Chain animations with .onComplete:
LSTween.colorTo(panel, new vec4(1, 1, 1, 0), 0.3) // fade out
.onComplete(() => panel.enabled = false) // then hide
.start()
Color Picker Pattern
From the Drawing example — multiple color swatches, with a visual selection indicator:
@component
export class ColorPicker extends BaseScriptComponent {
@input swatches: SceneObject[] // one per color
@input colors: vec4[] = [] // matching color values
@input selectionRing: SceneObject
private selectedIndex: number = 0
onAwake(): void {
this.swatches.forEach((swatch, i) => {
const touch = swatch.getComponent('Component.TouchComponent')
touch.addMTouchStartCallback(() => this.selectColor(i))
})
}
selectColor(index: number): void {
this.selectedIndex = index
// Move the selection ring to the chosen swatch
const swatchST = this.swatches[index].getComponent('Component.ScreenTransform')
const selST = this.selectionRing.getComponent('Component.ScreenTransform')
selST.position = swatchST.position // match position
// Apply color to the drawing material
drawingMaterial.mainPass.penColor = this.colors[index]
}
}
Undo Stack Pattern
From the Drawing example:
class UndoStack {
private readonly maxSize = 20
private stack: (() => void)[] = [] // each item is a "undo function"
push(undoFn: () => void): void {
if (this.stack.length >= this.maxSize) {
this.stack.shift() // drop oldest
}
this.stack.push(undoFn)
}
undo(): void {
const fn = this.stack.pop()
if (fn) fn()
}
get canUndo(): boolean {
return this.stack.length > 0
}
}
Common Gotchas
- Anchors use [0, 1] space, not the [-1, 1] ScreenTransform local space — mixing them up is the most common ScreenTransform bug.
positiononly works when both anchor points are the same — ifanchors.min ≠ anchors.max(stretch mode), position is ignored; use offsets instead.- Touch events don't automatically block — a touch on a UI panel passes through to the world unless a
ScreenRegionComponentwithTouchBlockingis present. ScreenRegionComponentregion types:TouchBlockingis the most common for UI;SafeRenderandFullFramedefine the playfield boundaries.touchComponent.addMTouchStartCallbackvsTapEvent:TapEventfires only on completed, non-moved taps;TouchComponentcallbacks fire immediately on contact — useTouchComponentfor drawing and drag interactions.- Pivot point affects how a ScreenTransform rotates and scales — a pivot of
(0, 0)rotates around the centre,(-1, -1)around the bottom-left corner. - Text HTML tags: Lens Studio supports
<b>,<i>,<color=#rrggbbaa>,<size=N>tags intextComponent.text. Non-closing tags crash the text renderer. - Never set
textComponent.textto untrusted string content directly (e.g., data from the network or from Dynamic Response). Untrusted strings containing unclosed HTML tags will crash the renderer; strip or escape them first.
More from rolandsmeenk/lensstudioagents
lens-studio-scripting
Reference guide for the Lens Studio TypeScript component system — covering the @component, @input, @hint, @allowUndefined, and @label decorators, the BaseScriptComponent lifecycle (onAwake vs OnStartEvent, UpdateEvent, DelayedCallbackEvent one-shot and repeating timers, TurnOnEvent/TurnOffEvent, onDestroy), accessing components with getComponent (plus null-check patterns to fix 'cannot read property of null' errors), cross-TypeScript imports with getTypeName(), NativeLogger vs print, prefab instantiation (sync and async), SceneObject hierarchy queries, and enabling/disabling objects. Use this skill whenever writing or debugging any Lens Studio TypeScript script, wiring up scene objects, or fixing 'this is undefined' or null-reference errors — platform-agnostic (works for Spectacles and phone lenses).
12spectacles-lens-essentials
Reference guide for foundational Lens Studio patterns on Spectacles — covering the GestureModule (pinch down/up/strength, targeting, grab, phone-in-hand with correct TypeScript API), SIK components (PinchButton, DragInteractable, GrabInteractable, ScrollView), hand-tracking gestures, physics bodies/colliders/callbacks (including audio-on-collision), LSTween animation (position/scale/rotation/color tweens), prefab instantiation at runtime, materials (clone-before-modify), spatial anchors, on-device persistent storage (putString/getFloat), spatial images, and the Path Pioneer raycasting pattern. Use this skill for any Spectacles lens that needs interaction, motion, animation, physics, audio, or persistent local storage — including Essentials, Throw Lab, Spatial Persistence, Spatial Image Gallery, Path Pioneer, Public Speaker, Voice Playback, Material Library, and DJ Specs samples.
9lens-studio-world-query
Reference guide for world understanding and scoring in Lens Studio — covering WorldQueryModule HitTestSession (HitTestSessionOptions.filter for jitter smoothing, semantic surface classification for floor/wall/ceiling/table detection, null result handling, per-frame performance), SIK InteractionManager targeting interactor ray pattern, Physics.createGlobalProbe().rayCast for scene-collider hits with collision layer filtering, aligning objects to surface normals using quat.lookAt, and the LeaderboardModule (create/retrieve with TTL and OrderingType, submitScore, getLeaderboardInfo with UsersType.Global/Friends). Use this skill when detecting real floors/walls/tables to place AR content, raycasting for hover or interaction against scene objects, or adding a global in-lens leaderboard — differentiates from spectacles-lens-essentials (physics/SIK) and from spectacles-cloud (Supabase persistence).
8spectacles-cloud
Reference guide for Snap Cloud (Supabase-powered backend) in Spectacles lenses — covering Fetch API setup (requires Internet Access capability in Project Settings), Postgres REST queries with the anon key, Row Level Security policies, Realtime WebSocket subscriptions with correct postgres_changes event format and reconnect-on-sleep patterns, cloud storage uploads of base64 images captured by Spectacles, serverless Edge Functions, and companion web dashboard architecture. Use this skill whenever a lens needs persistent cloud data, needs to share data with a web app in real time, uploads captured images to a bucket, or calls a cloud function — covering Snap Cloud and World Kindness Day samples. Use spectacles-networking for plain REST calls to non-Snap backends, and spectacles-connected-lenses for in-session multiplayer state.
7lens-studio-materials-shaders
Reference guide for materials and shaders in Lens Studio — covering runtime material property changes (clone-before-modify, mainPass.baseColor, mainPass.opacity, mainPass.baseTex), blend modes (Normal/Alpha/Add/Screen/Multiply), depth and cull settings (depthTest, depthWrite, twoSided, cullMode), render order, material variants, assigning textures and render targets, reading and writing RenderTarget textures for post-processing, the graph-based Material Editor node system, custom shader graph nodes, and common shader pitfalls. Use this skill for any lens that needs to change material colors or textures at runtime, implement custom visual effects with shaders, set up post-processing render pipelines, chain render targets, or debug material/blend-mode issues — covering MaterialEditor, Drawing, and HairSimulation examples.
6spectacles-networking
Reference guide for the Lens Studio Fetch API and WebView component in Spectacles lenses — covering InternetModule (Lens Studio 5.9+), Fetch API via internetModule.fetch(Request) with bytes/text/json response handling, performHttpRequest, Internet Access capability, GET/POST requests, custom headers, Bearer auth, polling, timeouts, CORS/HTTPS, WebSocket and RemoteMediaModule for media from URLs, and bidirectional WebView messaging. Use this skill for any lens that calls a REST API, polls a JSON endpoint, loads remote images, embeds a webpage, or talks to a custom backend — including the Fetch sample. Use spectacles-ai for LLM/RSG calls, or spectacles-cloud for Supabase/Snap Cloud integration.
6