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
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 resizePlatform info
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/iOSpixelRatio 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.
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
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.
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
device.addEventListener('resize', (width, height) => { }) // logical px
device.removeEventListener('resize', callback) // same function referenceFires whenever the host viewport changes (rotation, window resize).
Connectivity: device.online + online / offline events
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.
host-gated — reads true and the events never fire on hosts that don't track
connectivity (headless).
Precise touch
device.setPreciseTouch(enabled: boolean) // OFF by defaultOpt 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.
host-gated — a silent no-op on hosts without a coalesced-input concept (web already coalesces pointermove; headless).
Haptics
device.vibrate() // default "medium" impact
device.vibrate("selection") // a light tick for a value changeFire 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.
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.
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.
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
- Pointer events & gestures — the events precise touch feeds
- Conventions — logical px, the
addEventListenerpattern