Skip to main content

ScreenTransform

APJS Script API reference for the ScreenTransform class.

TypeNameInterface Description
VariablesanchoredPosition: Vector2f

Function: Gets or sets the pixel offset of the pivot point relative to the anchor center point. Anchor center = Lerp(anchorLeft, anchorRight, pivot.x), Lerp(anchorBottom, anchorTop, pivot.y). Unit: pixels. Default: (0, 0). Example: anchors=(0.5,0.5,0.5,0.5), pivot=(0.5,0.5), anchoredPosition=(100,50) means the element center is 100px to the right and 50px above the parent center.

Variablesanchors: Vector4f

Function: Gets or sets the anchor points as (left, right, bottom, top) = (x, y, z, w), each normalized in [0,1] relative to the parent rect (0=left/bottom edge, 1=right/top edge). - Point anchor (x==y, z==w): element attaches to a single point. e.g. (0.5,0.5,0.5,0.5) = parent center. - Stretch anchor (x!=y or z!=w): element stretches with parent. e.g. (0,1,0,1) = fill parent. - (0,0,0,0) = bottom-left corner; (1,1,1,1) = top-right corner. Default: (0.5,0.5,0.5,0.5).

Variablesoffsets: Vector4f

Function: Gets or sets the four edge positions of the element relative to the anchor center, in pixels. Format: (left, right, bottom, top). Derived from anchoredPosition + sizeDelta + pivot (not stored independently): left = anchoredPosition.x - sizeDelta.x pivot.x right = anchoredPosition.x + sizeDelta.x (1 - pivot.x) bottom = anchoredPosition.y - sizeDelta.y pivot.y top = anchoredPosition.y + sizeDelta.y (1 - pivot.y) Writing offsets back-calculates and updates sizeDelta and anchoredPosition accordingly.

Variablespivot: Vector2f

Function: Gets or sets the pivot point of the UI element, normalized to [0,1] range. (0,0) represents the bottom-left corner, (1,1) represents the top-right corner. Determines the origin point for rotation, scaling, and positioning calculations.

Variablesrotation: number

Function: Gets or sets the 2D rotation angle in degrees applied to the UI element. Positive values = counter-clockwise. Rotation is relative to parent and centered on the pivot point.

Variablesscale: Vector2f

Function: Gets or sets the 2D scaling factor applied to the UI element. Values greater than 1 enlarge the element, while values between 0 and 1 shrink it. The scale is applied relative to the parent transform and centered on the pivot point.

VariablessizeDelta: Vector2f

Function: Gets or sets the element size delta, in pixels. Meaning depends on anchor mode: - Point anchor (anchors.x==anchors.y && anchors.z==anchors.w): sizeDelta IS the absolute element size. e.g. (360, 640) = 360px wide, 640px tall. - Stretch anchor (anchors.x!=anchors.y || anchors.z!=anchors.w): sizeDelta = element size minus anchor region size. Negative = element is inset. e.g. anchors=(0,1,0,1), sizeDelta=(-40,-40) means 20px inset on each side.

Functionsconstructor()

Examples

constructor()

let obj = new APJS.ScreenTransform();

Use Case

Example 1 — AR goalkeeper game — track hand position via AlgorithmManager, check distance to incoming ball objects, catch/deflect balls that come near the hand

@component()
export class HandCatchGame extends APJS.BasicScriptComponent {
@serializeProperty
balls: APJS.SceneObject[] = [];
@serializeProperty
scoreDisplay!: APJS.SceneObject;
@serializeProperty
handIndicator!: APJS.SceneObject;

private score = 0;
private scoreText!: APJS.Text;
private catchRadius = 80; // pixels
private handST!: APJS.ScreenTransform;
private ballStartPos: APJS.Vector2f[] = [];

// RecordStart: reset score + restore every ball (visible=true + cached startPos). Hand
// tracking state managed by AR system. See GameState §"RecordStart / RecordEnd Lifecycle".
private onRecordStart = (_event: APJS.IEvent) => {
this.score = 0;
if (this.scoreText) this.scoreText.text = "Catches: 0";
for (let i = 0; i < this.balls.length; i++) {
const ball = this.balls[i];
if (!ball) continue;
ball.visible = true;
const ballST = ball.getComponent("ScreenTransform") as APJS.ScreenTransform;
if (ballST && this.ballStartPos[i]) {
ballST.anchoredPosition = new APJS.Vector2f(this.ballStartPos[i].x, this.ballStartPos[i].y);
}
}
};

onStart(): void {
this.scoreText = this.scoreDisplay.getComponent("Text") as APJS.Text;
this.scoreText.text = "Catches: 0";
this.handST = this.handIndicator.getComponent("ScreenTransform") as APJS.ScreenTransform;
for (const ball of this.balls) {
const st = ball ? ball.getComponent("ScreenTransform") as APJS.ScreenTransform : null;
if (st) this.ballStartPos.push(new APJS.Vector2f(st.anchoredPosition.x, st.anchoredPosition.y));
else this.ballStartPos.push(new APJS.Vector2f(0, 0));
}
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.onRecordStart);
}

onDestroy(): void {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.onRecordStart);
}

onUpdate(dt: number): void {
const result = APJS.AlgorithmManager.getResult();
if (result.getHandCount() === 0) {
this.handIndicator.visible = false;
return;
}

this.handIndicator.visible = true;
const hand = result.getHandInfo(0);

// Convert hand rect center (0-1, bottom-left origin, y-up) to ScreenTransform coords
const handCenterX = hand.rect.x + hand.rect.width / 2;
const handCenterY = hand.rect.y + hand.rect.height / 2;
const handScreenX = (handCenterX - 0.5) * 720;
const handScreenY = (handCenterY - 0.5) * 1280;

// Move hand indicator
this.handST.anchoredPosition = new APJS.Vector2f(handScreenX, handScreenY);

// Check distance to each active ball
for (const ball of this.balls) {
if (!ball.visible) continue;
const ballST = ball.getComponent("ScreenTransform") as APJS.ScreenTransform;
if (!ballST) continue;

const ballPos = ballST.anchoredPosition;
const dx = ballPos.x - handScreenX;
const dy = ballPos.y - handScreenY;
const dist = Math.sqrt(dx * dx + dy * dy);

if (dist < this.catchRadius) {
ball.visible = false; // caught!
this.score++;
this.scoreText.text = "Catches: " + this.score;
console.log("Caught ball! Score: " + this.score);
}
}
}
}

Example 2 — Virtual joystick using ScreenTransform: detect tap on joystick top via worldToScreenPoint radius check, drag with clamped distance, output normalized direction.

@component()
export class JoystickInput extends APJS.BasicScriptComponent {
@serializeProperty
private uiCameraObject: APJS.SceneObject;
@serializeProperty
private joystickBase: APJS.SceneObject;
@serializeProperty
private joystickTop: APJS.SceneObject;

private uiCamera: APJS.Camera;
private baseTransform: APJS.ScreenTransform;
private topTransform: APJS.ScreenTransform;
private maxDistance: number = 100;
private active: boolean = false;
public direction: APJS.Vector2f = new APJS.Vector2f(0, 0);

onStart(): void {
this.uiCamera = this.uiCameraObject.getComponent("Camera") as APJS.Camera;
this.baseTransform = this.joystickBase.getComponent("ScreenTransform") as APJS.ScreenTransform;
this.topTransform = this.joystickTop.getComponent("ScreenTransform") as APJS.ScreenTransform;
const size = this.baseTransform.sizeDelta;
const topSize = this.topTransform.sizeDelta;
this.maxDistance = Math.min(size.x, size.y) / 2 - Math.min(topSize.x, topSize.y) / 2;
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.Touch, this.onTouch);
}

private isInRadius(vp: APJS.Vector2f, st: APJS.ScreenTransform, radius: number): boolean {
const worldPos = st.getWorldPosition();
const screenPos = this.uiCamera.worldToScreenPoint(worldPos);
const origin = this.uiCamera.worldToScreenPoint(new APJS.Vector3f(0, 0, 0));
const sw = origin.x * 2;
const sh = origin.y * 2;
const dx = vp.x * sw - screenPos.x;
const dy = vp.y * sh - screenPos.y;
return Math.sqrt(dx * dx + dy * dy) <= radius;
}

private onTouch = (event: APJS.IEvent) => {
const t = event.args[0] as APJS.TouchData;
const vp = new APJS.Vector2f(t.position.x, 1 - t.position.y);
if (t.phase === APJS.TouchPhase.Began) {
this.active = this.isInRadius(vp, this.topTransform, this.maxDistance);
} else if (t.phase === APJS.TouchPhase.Moved && this.active) {
const baseWorld = this.baseTransform.getWorldPosition();
const baseScreen = this.uiCamera.worldToScreenPoint(baseWorld);
const origin = this.uiCamera.worldToScreenPoint(new APJS.Vector3f(0, 0, 0));
const sw = origin.x * 2;
const sh = origin.y * 2;
const dx = vp.x * sw - baseScreen.x;
const dy = vp.y * sh - baseScreen.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const clamped = Math.min(dist, this.maxDistance);
if (dist > 0) {
this.direction.set(dx / dist, dy / dist);
}
const topWorld = this.topTransform.getWorldPosition();
const topScreen = this.uiCamera.worldToScreenPoint(topWorld);
const newScreen = new APJS.Vector3f(baseScreen.x + this.direction.x * clamped, baseScreen.y + this.direction.y * clamped, topScreen.z);
this.topTransform.setWorldPosition(this.uiCamera.screenToWorldPoint(newScreen));
} else if (t.phase === APJS.TouchPhase.Ended && this.active) {
this.active = false;
this.direction.set(0, 0);
const baseWorld = this.baseTransform.getWorldPosition();
const topWorld = this.topTransform.getWorldPosition();
this.topTransform.setWorldPosition(new APJS.Vector3f(baseWorld.x, baseWorld.y, topWorld.z));
}
};

onDestroy(): void {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.Touch, this.onTouch);
}
}
Copyright © 2026 TikTok. All rights reserved.
About TikTokHelp CenterCareersContactLegalTerms of ServicePrivacy PolicyCookies