LeCodesdocs

Device

Platform info, the current display size, connectivity, and the host resize event. device is a plain global object — nothing to construct.

At a glance

TypeScript
if (device.platform === 'ios') toast('running on iOS')

const layout = (w: number, h: number) => {
  console.log('display is', w, 'x', h, 'logical px')
}
layout(device.width, device.height)               // readable any time
device.addEventListener('resize', layout)         // and again on every resize

Platform info

TypeScript
device.platform     // "web" | "android" | "ios" | string  (read-only)
device.language     // host language code, e.g. "en"       (read-only)
device.pixelRatio   // physical px per logical px; 1 on standard displays, 2–3 on retina/iOS

pixelRatio is the ratio to bake a Canvas at for crisp output without hardcoding: new Canvas(w, h, { pixelRatio: device.pixelRatio }). It falls back to 1 if the host doesn't report one.

Note

the web 2D/GL surface currently renders at logical resolution, so on web pixelRatio only helps UI canvases; on iOS (physical surface) it makes 2D canvases crisp.

Display size

TypeScript
device.width        // current display width, logical px  (read-only)
device.height       // current display height, logical px (read-only)

The same values the resize event delivers, but readable at any time — not only inside the listener. Both are logical px (like clientX/clientY), never physical pixels.

Note

both are 0 until the host has reported a size. Don't lay out from device.width at the top of a file that runs before the first frame — subscribe to resize as well.

resize event

TypeScript
device.addEventListener('resize', (width, height) => { })   // logical px
device.removeEventListener('resize', callback)              // same function reference

Fires whenever the host viewport changes (rotation, window resize).

Connectivity: device.online + online / offline events

TypeScript
device.online                                    // boolean (read-only)
device.addEventListener('offline', () => toast('No connection'))
device.addEventListener('online', () => sync())

Best-effort navigator.onLine semantics: false only when the platform is sure there's no network; true doesn't guarantee the internet is reachable — treat it as a hint for UI ("you're offline" banners, deferring sync), and still handle fetch failures.

Note

host-gated — reads true and the events never fire on hosts that don't track connectivity (headless).

Precise touch

TypeScript
device.setPreciseTouch(enabled: boolean)    // OFF by default

Opt into the precise-touch system where the platform supports it. When on, fast strokes are sampled at the touch digitizer's full rate (iOS coalesced touches, ≈120–240 Hz) instead of once per display frame (~60 Hz) — a pointer-heavy app (drawing, handwriting, dragging) gets more points and smoother lines. Leave it off for tap/button UIs.

Note

host-gated — a silent no-op on hosts without a coalesced-input concept (web already coalesces pointermove; headless).

Haptics

TypeScript
device.vibrate()             // default "medium" impact
device.vibrate("selection")  // a light tick for a value change

Fire a one-shot haptic of a semantic style (default "medium"):

Group Styles Meaning
Impact "light" "medium" "heavy" "soft" "rigid" a physical "tap" of varying weight
Notification "success" "warning" "error" an outcome cue
Selection "selection" a light tick for a changing value

The API is semantic rather than a duration/pattern on purpose: that's the only vocabulary that feels native on both iOS (Taptic Engine / UIFeedbackGenerator) and Android (HapticFeedbackConstants / VibrationEffect). A vibrate(ms) duration would only be faithful on web/Android — iOS has no public API for arbitrary-length vibration.

Note

host-gated — a silent no-op where there's no haptic hardware (iPad, older iPhones, web, headless). It also respects the user's system haptics setting.

Motion

The fused device-orientation sensor (gyro + accelerometer) — for tilt/steering controls and magic-window / 360° panoramas.

TypeScript
await device.motion.start()          // begin updates (battery); Promise<boolean> — false = no sensor

setLoop(() => {
  scene.camera.quaternion = device.motion.attitude   // 360° panorama camera, one line
  // or a 2D tilt game:
  ball.x += device.motion.gravity.x
  ball.y += device.motion.gravity.y
})

device.motion.recenter()             // make "here" the forward direction (yaw-only)
device.motion.stop()                 // release the sensor
Member Type Notes
start(options?) Promise<boolean> Begin updates. false = no sensor / permission denied.
stop() void Stop updates, release the sensor.
recenter() void Zero the current heading (pitch/roll stay gravity-referenced).
available boolean Does the device have a gyro at all?
enabled boolean Are updates currently running?
attitude Quat Current orientation. World frame by default — assign straight to a node/camera quaternion.
gravity Vec3 Gravity direction in screen space (x → right, y → down); read x/y for 2D tilt.

start(options) takes { interval?: number, frame?: "world" | "device" }. interval (seconds, default 1/60) is a floor — the sensor fuses at ≥ that rate in the background and you poll the freshest sample each frame, so the sensor rate need not match your frame rate. frame defaults to "world": attitude/gravity arrive already converted into the engine's Y-up world (and, for gravity, screen) space and are orientation-aware — rotate the phone to landscape and the camera stays upright automatically. Pass "device" for the raw sensor frame if you want to do the math yourself.

Poll inside setLoop; the readables always return the latest fused sample. attitude is drift- corrected, so a panorama horizon stays level indefinitely.

Note

host-gated — a silent no-op where there's no gyro (older iPads, web without permission, headless). available tells you up front; attitude reads Quat.identity and gravity reads (0,0,0) when it isn't running.

See also