Sensors and device #

This page covers the parts of Budo that talk to the device itself: motion sensors with sys.sensors, and the screen, clipboard, haptics, system preferences, and mouse cursor with sys.device. Every call is safe on every platform: where a feature is missing, it returns false or null instead of failing, so your app can adapt.

Sensors #

sys.sensors reads the accelerometer and the compass. They are available on Android, and on the web where the browser supports device motion and orientation events, which mostly means phones and tablets. Desktop builds have no sensors and report them as unavailable.

Check what the device has, start the sensors, then read the latest values in your frame function.

main.js
// Start the sensors where the device has any.
if (sys.sensors.isAvailable()) {
  sys.sensors.start();
}

function frame() {
  // The latest accelerometer reading, or null.
  const accel = sys.sensors.getAccel();
  if (accel) {
    // Tilt, from the acceleration along the screen's axes.
    state.tiltX = accel.x;
    state.tiltY = accel.y;
  }

  // The latest compass reading, or null.
  const compass = sys.sensors.getCompass();
  if (compass) {
    // Degrees clockwise from north.
    state.heading = compass.heading;
  }

  draw();
  sys.animation.requestFrame(frame);
}

isAvailable() is true when the device has either sensor; hasAccelerometer() and hasCompass() check each one. start() returns true once the sensors are running, isActive() tells whether they are, and stop() turns them off. Stop them when your app no longer needs them, to save battery.

getAccel() returns { x, y, z }, the acceleration along each axis in m/s², gravity included, so a device at rest reads about 9.8 m/s² in total. getCompass() returns { x, y, z, heading }: the magnetic field in µT, and the heading in degrees, where 0 is north and 90 is east. Both return null while the sensors are stopped and until the first reading arrives, so always check the result.

On the web, iPhones and iPads ask the user for permission to use motion sensors, and the browser only shows that request after a tap. When start() is called before any tap, readings begin after the user's next tap or click.

You can also check sys.capabilities.sensors.available before showing a sensor-driven feature. examples/sound-instrument bends the pitch of its voices when the device is tilted, where the device has an accelerometer.

Keeping the screen awake #

Timers, instruments, kiosks, games during play, and monitoring tools should keep the screen on while they are in use.

main.js
// Keep the display awake while the experience needs it.
sys.device.keepScreenOn(true);

Call keepScreenOn(false) when the app returns to a state that does not need it, such as a menu. The call returns false when the platform cannot apply the change. On desktop it disables the screensaver, on Android it keeps the app's window on, and on the web it uses the Screen Wake Lock API where the browser allows it.

Clipboard #

sys.device.setClipboardText(text) copies text, and sys.device.getClipboardText() reads it, or returns null when the clipboard holds no text.

main.js
// Copy some text.
sys.device.setClipboardText('Budo');
// Read the clipboard's text, or null.
const pasted = sys.device.getClipboardText();

Browsers only expose the clipboard asynchronously. On the web, getClipboardText() returns the last text copied, cut, or pasted on the page, and starts reading the system clipboard so that text copied in other apps arrives on a later call; the browser may ask the user once. Copying may also require a recent tap or key press. Text fields rarely need these calls on the web and on Android, where the platform's editor pastes by itself (see Text editing sessions).

Haptics #

sys.device.haptic(kind) plays a short vibration: 'light' (the default), 'medium', 'heavy', 'selection' (a value changed), 'success', 'warning', or 'error'. It returns false where the device cannot vibrate, as on desktop computers, so you can call it everywhere. An unknown kind throws a RangeError.

main.js
// A switch was flipped: a light tick.
sys.device.haptic('selection');

System preferences #

sys.device.getPreferences() returns the choices the user made in the system settings.

main.js
// Read the user's preferences.
const { darkMode, reducedMotion, highContrast, fontScale, safeArea, keyboardInset } = sys.device.getPreferences();
  • darkMode: the system uses a dark appearance.
  • reducedMotion: the user asked for less motion. Skip or shorten animations.
  • highContrast: the user asked for more contrast.
  • fontScale: the user's text size multiplier, 1 by default. Multiply your font sizes by it.
  • safeArea: { top, right, bottom, left }, in physical pixels, the edges to keep clear of notches, rounded corners, and system bars.
  • keyboardInset: the height, in physical pixels from the bottom, covered by the on-screen keyboard; 0 when it is hidden, and on desktop. Keep the focused text field above it (budo-ui lays its interface out above it for you).

The call is cheap enough to make every frame, and the values follow the settings while the app runs. They come from the system settings on macOS and Windows, from GNOME settings on Linux, from media queries, CSS safe areas, and the visual viewport on the web, and from the system configuration and window insets on Android.

main.js
function frame() {
  // Read the preferences each frame: they follow the settings live.
  const p = sys.device.getPreferences();
  // Follow the system's appearance.
  sys.canvas.clear(p.darkMode ? '#101116' : '#f8fafc');
  sys.canvas.setFillColor(p.darkMode ? '#f8fafc' : '#101116');
  // Scale text with the user's text size.
  sys.canvas.drawText('Dark mode: ' + p.darkMode, 24, 40, 20 * p.fontScale);
  // Check again within a second, or on the next input.
  sys.animation.waitForInput(frame, 1000);
}

Mouse cursor #

sys.device.setCursor(name) changes the mouse cursor, using CSS cursor names: 'default', 'text', 'pointer', 'grab', 'grabbing', 'move', 'ew-resize', 'ns-resize', 'nwse-resize', 'nesw-resize', 'not-allowed', 'wait', 'crosshair', or 'none'. Set it every frame from what is under the pointer. It returns false where there is no mouse cursor, and throws a RangeError on an unknown name.

main.js
// An I-beam over a text field, the arrow elsewhere.
sys.device.setCursor(overField ? 'text' : 'default');