| name | lens-studio-2d-ui |
| description | Reference guide for 2D UI and screen-space interaction in Lens Studio — covering ScreenTransform anchors/offsets/size/pivot and coordinate conversions (localPointToScreenPoint, screenPointToLocalPoint, localPointToWorldPoint), ScreenImage (texture, stretch mode, color tint), Text component (content, font, alignment, color, size), ScreenRegionComponent for defining tap/touch areas, TouchComponent with TouchStartEvent/TouchMoveEvent/TouchEndEvent, tap input for phone lenses, LSTween UI animations (colorTo, moveTo on screen elements), multi-swatch color picker pattern, undo stack pattern, and common gotchas. Use this skill whenever a lens needs a 2D UI panel, tap interaction, on-screen buttons, text labels, color pickers, swipeable menus, or undo/redo — covering Drawing, Quiz, TappableQuestion, MemeSticker, MusicVideo, and HighScore examples. |
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):
st.anchors.setMin(new vec2(0, 0))
st.anchors.setMax(new vec2(1, 1))
st.anchors.setMin(new vec2(1, 1))
st.anchors.setMax(new vec2(1, 1))
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)
st.offsets.setRight(-10)
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))
st.position = new vec2(0, 0.3)
st.size = new vec2(300, 60)
Coordinate Conversions
const screenPx: vec2 = st.localPointToScreenPoint(new vec2(0, 0))
const local: vec2 = st.screenPointToLocalPoint(touchPosScreenPx)
const worldPt: vec3 = st.localPointToWorldPoint(new vec2(0, 0))
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')
screenImage.mainPass.baseTex = myTexture
screenImage.mainPass.baseColor = new vec4(1, 0.5, 0, 1)
screenImage.mainPass.baseColor = new vec4(1, 1, 1, 1)
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')
textComponent.text = 'Score: ' + score
textComponent.size = 48
textComponent.horizontalAlignment = HorizontalAlignment.Center
textComponent.verticalAlignment = VerticalAlignment.Center
textComponent.textFill.color = new vec4(1, 1, 1, 1)
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:
function safeSetText(component: Text, value: string, maxLength = 200): void {
const stripped = (value ?? '')
.slice(0, maxLength)
.replace(/<[^>]*>/g, '')
component.text = stripped
}
safeSetText(textComponent, serverName)
Touch Input
TapEvent (phone lenses, simple tap)
const tapEvent = this.createEvent('TapEvent')
tapEvent.bind((eventData) => {
const screenPos: vec2 = eventData.getPosition()
print('Tapped at: ' + screenPos.x + ', ' + screenPos.y)
handleTap(screenPos)
})
TouchComponent (multi-touch, drag, phone lenses)
const touchComponent = this.sceneObject.getComponent('Component.TouchComponent')
touchComponent.addMTouchStartCallback((eventData) => {
const pos = eventData.getPosition()
print('Touch start at: ' + pos.x + ', ' + pos.y)
})
touchComponent.addMTouchMoveCallback((eventData) => {
const pos = eventData.getPosition()
drawAtPosition(pos)
})
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:
UI Buttons for Phone Lenses
For phone lenses (not Spectacles), use tap regions rather than SIK PinchButton:
@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'
LSTween.colorTo(screenImage, new vec4(1, 1, 1, 1), 0.3).start()
LSTween.colorTo(screenImage, new vec4(1, 1, 1, 0), 0.3).start()
const startPos = new vec2(1.5, 0)
const endPos = new vec2(0, 0)
screenTransform.position = startPos
LSTween.moveToScreen(screenTransform.sceneObject, endPos, 0.4)
.easing(TWEEN.Easing.Quadratic.Out)
.start()
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)
.onComplete(() => panel.enabled = false)
.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[]
@input colors: vec4[] = []
@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
const swatchST = this.swatches[index].getComponent('Component.ScreenTransform')
const selST = this.selectionRing.getComponent('Component.ScreenTransform')
selST.position = swatchST.position
drawingMaterial.mainPass.penColor = this.colors[index]
}
}
Undo Stack Pattern
From the Drawing example:
class UndoStack {
private readonly maxSize = 20
private stack: (() => void)[] = []
push(undoFn: () => void): void {
if (this.stack.length >= this.maxSize) {
this.stack.shift()
}
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.
position only works when both anchor points are the same — if anchors.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
ScreenRegionComponent with TouchBlocking is present.
ScreenRegionComponent region types: TouchBlocking is the most common for UI; SafeRender and FullFrame define the playfield boundaries.
touchComponent.addMTouchStartCallback vs TapEvent: TapEvent fires only on completed, non-moved taps; TouchComponent callbacks fire immediately on contact — use TouchComponent for 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 in textComponent.text. Non-closing tags crash the text renderer.
- Never set
textComponent.text to 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.