Conventions
Cross-cutting rules that hold everywhere in the SDK. Every reference page assumes these.
No imports — everything is a global
The whole SDK surface (Scene, Sprite, UIScreen, Vec3, fetch, …) is injected at build
time; user code never imports it. A project's only import statements are relative paths to
its own files (import { hud } from './ui/hud') and to its assets (import hero from './hero.png'). A bare package import is a build error.
Entry point: the file with no exports. In a multi-file project it wires the others together
and starts the app (Router.init(home) or scene.open()).
Assets: reference a project file with an import or the inline asset('./path') macro — a
compile-time rewrite, so the path must be a string literal. Remote https://… URLs are plain
strings.
Time
setLoop(dt => …)fires every frame;dtis seconds since the previous frame (~0.016 at 60 fps). Frame-independent motion isposition += speed * dt.setTimeout/setIntervaltake milliseconds.- Animation durations are milliseconds:
animate({ duration })and UI.animateTo()/.animateFrom()(duration,delay).
Space, units, angles
2D (Scene2D) |
3D (Scene) |
UI / screen | |
|---|---|---|---|
| Axes | X right, Y up | right-handed, Y up, forward = −Z | X right, Y down |
| Unit | 1 world unit = 1 logical px at zoom 1 | ≈ meters | logical px (dp/pt) |
| Angles | rotation in degrees, CCW |
eulerAngles in degrees |
— |
- Raw-angle arguments are radians (
Quat.fromAxisAngle,Mat4.rotate*,Vec2.rotate); euler-angle properties and factories are degrees (node.rotation,Quat.fromEuler).DEG2RAD/RAD2DEGare compile-time constants for converting. - Screen coordinates (
clientX/clientY,device.width/height) are logical px (like iOS pt / Android dp), never physical pixels;device.pixelRatiois the physical-per-logical ratio. - One sign trap: 2D world Y points up, but screen Y and sprite anchor space point down
(
anchor: [0.5, 1]= bottom-center = "feet").
Colors
Anywhere a color is accepted, all of these work (ColorInput):
hex strings "#e33", "#e33c", "#ff3333", "#ff3333cc"; packed int 0xff3333 (24-bit RGB only —
alpha can't be passed as a number); normalized float arrays [r, g, b] / [r, g, b, a] (0..1).
hex is the only string form. CSS-style rgba(...) / named colors are not parsed —
they silently become opaque black. (UI styles are the exception: they go to the UI engine, which
has its own parser — see ui/styling.md.)
Math value semantics
Vec2 / Vec3 / Quat / Mat4 are mutable structs with pure methods: fields are settable
(v.x = 3), but every method returns a NEW value and never mutates its source. They are iterable
and interchangeable with raw tuples — [0, 1, 0] works wherever a Vec3 is accepted.
Node transform getters return fresh copies (value semantics, like Unity):
node.position.x = 3 // ✗ silent no-op — mutates a discarded copy
node.x = 3 // ✓ single-axis setter
const p = node.position; p.y += 1; node.position = p // ✓ mutate a local, assign backEvents
- Scenes, nodes,
device,WebSocket, media players:addEventListener(channel, cb)/removeEventListener(channel, cb). - UI elements instead use chainable
on*methods (.onClick(cb),.onChange(cb)). - Pointer events:
clickfires on pointer-up over the target;touchstartfires on pointer-down and its event object canev.track({...})to capture the rest of the gesture. See runtime/touch.md.
Chaining
Constructors take an options object; setters that configure return this, so building reads as
one chain. UI factories chain .style() and on*. Node capabilities attach as aspects and
also chain:
const hero = new Sprite({ texture })
.aspect(SpriteAnimation, { size: [32, 48], fps: 10, clips: { walk: [1, 2, 3] } })
.aspect(Shape2D, { box: [16, 8] })
.aspect(Physics2D, { motion: 'dynamic', fixedRotation: true })
hero.anim.play('walk') // each aspect adds a named accessorSee core/aspects.md.
Physics owns dynamic transforms
Once a node has a dynamic body (Physics / Physics2D), the physics step writes its transform.
Do not set node.position per frame — drive the body with velocity / applyImpulse
(kinematic bodies: moveTo). Reading node.position / node.worldPosition is always correct;
the SDK re-syncs from native automatically.
Host gating — features that can be absent
Some capabilities depend on the host build. They silently no-op when absent; guard with:
Physics.supported(3D physics),Physics2D.supported(2D physics)QRScanner.isSupporteddevice.setPreciseTouch()— no-op where the platform has no coalesced-input concept
Lifecycle
- Anything started in a screen's
onOpenmust be stopped inonClose(clearLoop,clearInterval,ws.close()), or it keeps running across navigation. - Objects wrapping native resources free them explicitly where a method exists:
node.destroy()(2D/3D nodes),player.dispose()(media — throws on any use afterwards),Texture2D.destroy(),response.dispose()(fetch bodies). 3DTexturehas no free — it lives until the engine closes.