WakaMotion

WakaMotion is an optional wakaPAC plugin that provides reactive motion sensor properties. It handles accelerometer data, tilt angles, iOS permission flow, and axis inversion calibration — all exposed as reactive properties that update your UI automatically.

Getting Started

WakaMotion is a separate file that must be loaded after wakapac.js and registered before any components are created.

<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/wakapac.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/plugins/wakamotion.min.js"></script>
<script>
    wakaPAC.use(wakaMotion);

    wakaPAC('#app', {
        // motion properties are now available
    });
</script>

Once registered, all motion properties are automatically injected into every component. No additional configuration is required on non-iOS devices. On iOS 13+, the user must explicitly grant permission — see iOS Permission.

Battery: WakaMotion attaches a devicemotion listener as soon as it is registered. If you only need motion on specific pages, only load wakamotion.js on those pages.

Reactive Properties

All properties below are injected into every wakaPAC component automatically. They update in real-time as sensor data arrives and trigger reactive template updates just like your own data properties.

motionSupported

Type: boolean | Updates: Never | Writable: No

Whether the browser exposes the DeviceMotion API at all. false on most desktop browsers and devices without motion hardware. Always check this before showing any motion-dependent UI.

<div data-pac-bind="visible: !motionSupported">
    Motion sensor not available on this device.
</div>

<div data-pac-bind="visible: motionSupported">
    Tilt: {{ motionTiltX }}°
</div>

motionPermissionRequired

Type: boolean | Updates: Once, after permission is granted | Writable: No

true on iOS 13+ where motion data requires explicit user permission before becoming available. false everywhere else. Call wakaMotion.requestMotionPermission() from a user gesture (e.g. a button click) to trigger the permission prompt.

<button data-pac-bind="visible: motionPermissionRequired, click: requestMotion">
    Enable Motion Sensor
</button>

motionPermissionGranted

Type: boolean | Updates: Once, after permission is granted | Writable: No

true if motion data is allowed to flow. On non-iOS devices this is true immediately — no permission is required. On iOS 13+ it starts as false and flips to true once wakaMotion.requestMotionPermission() resolves with 'granted'.

<div data-pac-bind="visible: motionPermissionGranted">
    Tilt: {{ motionTiltX }}°
</div>

motionHasAcceleration

Type: boolean | null | Updates: Once, on first event | Writable: No

Whether the device provides acceleration data. null until the first devicemotion event arrives, then a stable true or false. Some Android WebViews report accelerationIncludingGravity as null on every event despite supporting the API — this flag exposes that.

motionHasRotationRate

Type: boolean | null | Updates: Once, on first event | Writable: No

Whether the device has a gyroscope. null until the first event, then stable. Many budget tablets and mid-range phones ship without a gyroscope — rotation properties will remain null on such devices. Use this to conditionally show or hide rotation-based UI.

<div data-pac-bind="visible: motionHasRotationRate">
    Rotation alpha: {{ motionRotationAlpha }}°/s
</div>

<div data-pac-bind="visible: motionHasRotationRate === false">
    Rotation not available on this device.
</div>
Note: motionHasRotationRate === false is different from !motionHasRotationRate — the latter also matches null (not yet determined). Use strict equality when you want to distinguish "confirmed unavailable" from "not yet known".

motionAccelerationX / motionAccelerationY / motionAccelerationZ

Type: number | null | Updates: devicemotion events | Writable: No

Raw acceleration including gravity along each device axis, in m/s² — this is the accelerationIncludingGravity field from the DeviceMotion spec, not the gravity-subtracted acceleration field. The gravity component is what makes tilt calculation possible: when the device is stationary, these values reflect gravitational pull on each axis, which is exactly what motionTiltX and motionTiltY are derived from. Values are rounded to 2 decimal places to suppress floating point noise. null until the first event arrives, or if the device does not report acceleration data.

  • motionAccelerationX — left/right axis. Positive = right edge tilted down (W3C spec)
  • motionAccelerationY — forward/back axis. Positive = bottom edge tilted down
  • motionAccelerationZ — through-screen axis. ~9.81 m/s² when device is flat face-up (gravity)
Hardware variation: Some devices report one or more axes inverted relative to the W3C spec due to firmware bugs. Use wakaMotion.setMotionAxisInversion() to correct known devices, or wakaMotion.detectMotionAxisInversion() to run interactive calibration.

motionTiltX / motionTiltY

Type: number | null | Updates: devicemotion events | Writable: No

Tilt angle in whole degrees from horizontal, derived from acceleration using asin(axis / g). More intuitive than raw acceleration values for building tilt-based interfaces. null until the first event.

  • motionTiltX — left/right roll. = flat, +90° = right edge fully down, -90° = left edge fully down
  • motionTiltY — forward/back pitch. = flat, +90° = bottom edge fully down, -90° = top edge fully down

Range is -90° to +90° and is stable across all device orientations.

<div id="tilt-demo">
    <p>Left/right: {{ motionTiltX }}°</p>
    <p>Fwd/back: {{ motionTiltY }}°</p>
</div>
wakaMotion.setMotionThreshold(0.5); // suppress noise below 0.5 m/s²

wakaPAC('#tilt-demo', {
    computed: {
        // Map tilt to pixel offset for a ball-in-maze effect
        ballX() { return (this.motionTiltX ?? 0) * 2; },
        ballY() { return (this.motionTiltY ?? 0) * 2; }
    }
});

motionRotationAlpha / motionRotationBeta / motionRotationGamma

Type: number | null | Updates: devicemotion events | Writable: No

Rotation rate around each axis in degrees per second. Requires a gyroscope. Remains null on devices without gyroscope hardware. Check motionHasRotationRate before using these values.

  • motionRotationAlpha — rotation around the Z axis (spinning flat on a table)
  • motionRotationBeta — rotation around the X axis (tilting forward/back)
  • motionRotationGamma — rotation around the Y axis (tilting left/right)

motionAxisInversion

Type: object | Updates: after calibration or manual set | Writable: No

The current axis inversion multipliers as { x: 1|-1, y: 1|-1 }. A value of -1 means that axis is being corrected before values are broadcast. Updated automatically when wakaMotion.detectMotionAxisInversion() completes, or immediately when wakaMotion.setMotionAxisInversion() is called.

motionAxisDetectionStep

Type: string | Updates: during calibration | Writable: No

Current state of the axis inversion calibration flow. Possible values:

  • 'idle' — not started
  • 'tilt-x' — waiting for the user to tilt the right edge down
  • 'tilt-y' — waiting for the user to tilt the bottom edge down
  • 'done' — both axes detected, inversion applied
  • 'timeout' — user did not tilt within the allotted time

Watch this property to drive custom calibration UI. For simple cases use motionAxisDetectionStepLabel instead.

motionAxisDetectionStepLabel

Type: string | Updates: during calibration | Writable: No

Human-readable instruction for the current calibration step. Empty string when step is 'idle'. Allows a minimal calibration UI with no mapping code required.

<div id="calibration">
    <button data-pac-bind="click: calibrate">Calibrate axes</button>
    <p>{{ motionAxisDetectionStepLabel }}</p>
</div>
wakaPAC('#calibration', {
    calibrate() {
        wakaMotion.detectMotionAxisInversion();
    },

    watch: {
        motionAxisDetectionStep(step) {
            if (step === 'done') {
                // Persist the result so it can be restored on next page load
                localStorage.setItem('axisInversion', JSON.stringify(this.motionAxisInversion));
            }
        }
    }
});

iOS Permission

iOS 13+ requires an explicit user gesture before motion data is available. WakaMotion defers listener attachment on iOS — no data flows until wakaMotion.requestMotionPermission() is called from a tap handler. Calling it at page load causes a silent denial. The recommended pattern is to check motionPermissionRequired and show a button only when needed:

<div id="app">
    <!-- Shown only on iOS 13+ before permission is granted -->
    <button data-pac-bind="visible: motionPermissionRequired, click: requestMotion">
        Enable Motion Sensor
    </button>

    <!-- Shown once data is flowing -->
    <div data-pac-bind="visible: motionPermissionGranted">
        Tilt: {{ motionTiltX }}°
    </div>
</div>
wakaPAC('#app', {
    async requestMotion() {
        const result = await wakaMotion.requestMotionPermission();

        if (result !== 'granted') {
            console.warn('Motion permission denied.');
        }
    }
});

API

These methods configure WakaMotion behavior. They can be called at any time, though threshold and inversion settings are typically set before the first component is created.

wakaPAC.use(wakaMotion)

Registers the WakaMotion plugin with the wakaPAC runtime. Must be called before any wakaPAC() calls.

ParameterTypeDescription
wakaMotionobjectThe WakaMotion plugin object.
Returns void

wakaMotion.setMotionThreshold(threshold)

Suppress events unless at least one axis changes by more than threshold m/s² since the last dispatched event. Prevents constant re-renders from sensor noise while the device is stationary. Default is 0 (every event is dispatched). Recommended starting value for tilt UIs: 0.5.

ParameterTypeDescription
thresholdnumberMinimum axis change in m/s² required to dispatch an event. Default: 0.
Returns void
wakaMotion.setMotionThreshold(0.5);

wakaMotion.requestMotionPermission()

Request iOS motion sensor permission. Must be called from within a user gesture (button click). On non-iOS this is a no-op that immediately resolves with 'granted'.

ParameterTypeDescription
Returns Promise<string>Resolves to 'granted', 'denied', or 'error'.
wakaPAC('#app', {
    async requestMotion() {
        const result = await wakaMotion.requestMotionPermission();
        if (result !== 'granted') {
            this.errorMessage = 'Motion permission denied.';
        }
    }
});

wakaMotion.setMotionAxisInversion(x, y)

Manually correct for devices that report an acceleration axis inverted relative to the W3C spec. To identify inversion manually: hold the device flat and tilt the right edge down — motionAccelerationX should go positive. If it goes negative, the X axis is inverted on that device.

ParameterTypeDescription
xnumber-1 to invert the X axis, 1 to leave it as reported.
ynumber-1 to invert the Y axis, 1 to leave it as reported.
Returns void
wakaMotion.setMotionAxisInversion(-1, 1); // X inverted, Y normal

wakaMotion.detectMotionAxisInversion(options)

Guides the user through two tilt gestures to automatically detect and correct axis inversion. Updates motionAxisDetectionStep, motionAxisDetectionStepLabel, and motionAxisInversion as detection progresses. Safe to call from a button click handler. Ignored if calibration is already in progress.

ParameterTypeDescription
options.thresholdnumberMinimum acceleration in m/s² to register a deliberate tilt. Default: 4.
options.timeoutnumberMilliseconds to wait per axis before setting step to 'timeout'. Default: 8000.
Returns void
wakaMotion.detectMotionAxisInversion({ threshold: 4, timeout: 10000 });

wakaMotion.getMotionCapabilities()

Returns the current motion sensor capabilities of the device. hasAcceleration and hasRotationRate are null until the first devicemotion event arrives — call this after motion data has started flowing for reliable results.

ParameterTypeDescription
Returns objectPlain object with motionSupported, requiresPermission, hasAcceleration, and hasRotationRate flags.
const caps = wakaMotion.getMotionCapabilities();
// {
//   motionSupported:    true,
//   requiresPermission: false,
//   hasAcceleration:    true,
//   hasRotationRate:    null   // not yet known
// }

Quick Reference

Reactive Properties

Property Type Description
motionSupported boolean Browser exposes the DeviceMotion API
motionPermissionRequired boolean iOS 13+: explicit permission needed before data flows
motionPermissionGranted boolean Permission granted; true immediately on non-iOS, after grant on iOS
motionHasAcceleration boolean | null Device reports acceleration data (null until first event)
motionHasRotationRate boolean | null Device has a gyroscope (null until first event)
motionAccelerationX number | null Acceleration incl. gravity, left/right axis (m/s²)
motionAccelerationY number | null Acceleration incl. gravity, forward/back axis (m/s²)
motionAccelerationZ number | null Acceleration incl. gravity, through-screen axis (m/s²)
motionTiltX number | null Left/right tilt from horizontal in degrees (-90 to +90)
motionTiltY number | null Forward/back tilt from horizontal in degrees (-90 to +90)
motionRotationAlpha number | null Rotation rate, Z axis (deg/s) — gyroscope required
motionRotationBeta number | null Rotation rate, X axis (deg/s) — gyroscope required
motionRotationGamma number | null Rotation rate, Y axis (deg/s) — gyroscope required
motionAxisInversion object Current axis inversion multipliers { x: 1|-1, y: 1|-1 }
motionAxisDetectionStep string Calibration flow state (idle, tilt-x, tilt-y, done, timeout)
motionAxisDetectionStepLabel string Human-readable instruction for the current calibration step