DEV Community

HarmonyOS
HarmonyOS

Posted on

How to develop a dynamic balancing simulation interface using the gyroscope and accelerometer.

Read the original article:How to develop a dynamic balancing simulation interface using the gyroscope and accelerometer.

Requirement Description

Developers frequently use various sensors in different games and applications. In particular, the gyroscope sensor allows for great balance without being affected by gravity, enabling the creation of fun and engaging interfaces for users. By using the accelerometer and gyroscope sensors simultaneously, various systems such as aiming mechanisms, balance ball simulations, and air mouse functionality can be implemented.

Background Knowledge

Thanks to the Sensor Service Kit included in HarmonyOS, developers can not only access data from many different sensors but also use these sensors to create a variety of applications. Some of these sensors are as follows:

  • Accelerometer
  • Gravity
  • Gyroscope
  • Barometer
  • Heart Rate
  • Ambition

You can review the documentation for all sensor data provided by the relevant kit.

Implementation Steps

In the following example, it is explained how a balance simulation can be developed for a wearable device using gyroscope and accelerometer sensor data. By implementing the examples in this section, a dynamic balance interface–based application can be created. The implementation sequence and the corresponding algorithm are as follows:

  • Define Component and States
    • Create a component Index.
    • Initialize state variables for accelerometer (accelX, accelY, accelZ) and gyroscope (gyroX, gyroY, gyroZ).
    • Initialize state variables for cursor position (cursorX, cursorY) and circle color (circleColor).
    • Set private variables for circle dimensions, cursor dot size, sensitivity, and edge threshold.
  • Calculate Circle Dimensions on Component Mount
    • In aboutToAppear():
    • Get the display width.
    • Calculate circle diameter, radius, and center coordinates.
    • Start sensor monitoring.
  • Stop Sensors on Component Unmount
    • In aboutToDisappear(), stop the accelerometer and gyroscope sensors.
  • Start Sensor Monitoring
    • Use sensor.on() for accelerometer and gyroscope.
    • Update state variables with sensor readings.
    • Call updateCursorPosition() with accelerometer data.
    • Show a toast message indicating sensors have started.
    • Handle any errors using BusinessError and display an error toast.
  • Update Cursor Position
    • Calculate the new cursor position based on accelerometer values and sensitivity.
    • Calculate distance from the circle center.
    • If the cursor exceeds the circle boundary, adjust the position to stay within the circle using trigonometry.
    • Call checkEdgeProximity() to update circle color if near the edge.
    • Update state variables cursorX and cursorY.
  • Check Edge Proximity
    • Calculate distance from cursor to the edge.
    • Change circle color to blue if the cursor is near the edge, otherwise keep it red.
  • Stop Sensor Monitoring
    • Use sensor.off() for accelerometer and gyroscope.
    • Set isMonitoring to false.
    • Show a toast message indicating sensors have stopped.
    • Handle errors using BusinessError.
  • Show Cursor Position
    • Display a toast with current cursor coordinates when the user taps the screen.
  • Build UI
    • Create a Stack container:
    • Draw the circle with current circleColor.
    • Draw the cursor dot at (cursorX, cursorY) with opacity.
    • Set full width and height for the container.
    • Add background color.
    • Attach an onClick handler to show the cursor position.

Code Snippet / Configuration

The following code block provides all the implementation steps necessary for completing the respective project.

import { sensor } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';
import display from '@ohos.display';

@Entry
@Component
struct Index {
  @State accelX: string = '0.000';
  @State accelY: string = '0.000';
  @State accelZ: string = '0.000';
  @State gyroX: string = '0.000';
  @State gyroY: string = '0.000';
  @State gyroZ: string = '0.000';
  @State isMonitoring: boolean = false;

  @State cursorX: number = 111.5;
  @State cursorY: number = 111.5;
  @State circleColor: Color = Color.Red;

  private circleDiameter: number = 0
  private circleRadius: number = 0
  private circleCenterX: number = 0
  private circleCenterY: number = 0

  private dotRadius: number = 7.5;
  private sensitivity: number = 2;
  private edgeThreshold: number = 10;

  aboutToAppear(): void {

    this.circleDiameter = display.getDefaultDisplaySync().width/2
    this.circleRadius = this.circleDiameter / 2;
    this.circleCenterX = this.circleRadius;
    this.circleCenterY = this.circleRadius;
    this.startSensors()
  }

  aboutToDisappear(): void {
    this.stopSensors()
  }

  private startSensors(): void {
    try {
      sensor.on(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
        this.accelX = data.x.toFixed(3);
        this.accelY = data.y.toFixed(3);
        this.accelZ = data.z.toFixed(3);

        this.updateCursorPosition(data.x, data.y);
      }, { interval: 10000000 });

      sensor.on(sensor.SensorId.GYROSCOPE, (data: sensor.GyroscopeResponse) => {
        this.gyroX = data.x.toFixed(3);
        this.gyroY = data.y.toFixed(3);
        this.gyroZ = data.z.toFixed(3);
      }, { interval: 10000000 });

      this.isMonitoring = true;
      promptAction.showToast({ message: 'Sensors started', duration: 1000 });
    } catch (error) {
      const e: BusinessError = error as BusinessError;
      console.error(`Failed to start sensors. Code: ${e.code}, message: ${e.message}`);
      promptAction.showToast({ message: `Error: ${e.code}`, duration: 2000 });
    }
  }

  private updateCursorPosition(accelX: number, accelY: number): void {
    let newX = this.cursorX + (-accelX * this.sensitivity);
    let newY = this.cursorY + (accelY * this.sensitivity);

    const distanceFromCenter = Math.sqrt(
      Math.pow(newX + this.dotRadius - this.circleCenterX, 2) +
      Math.pow(newY + this.dotRadius - this.circleCenterY, 2)
    );

    if (distanceFromCenter > this.circleRadius - this.dotRadius) {
      const angle = Math.atan2(
        newY + this.dotRadius - this.circleCenterY,
        newX + this.dotRadius - this.circleCenterX
      );

      newX = this.circleCenterX + Math.cos(angle) * (this.circleRadius - this.dotRadius) - this.dotRadius;
      newY = this.circleCenterY + Math.sin(angle) * (this.circleRadius - this.dotRadius) - this.dotRadius;
    }

    this.checkEdgeProximity(distanceFromCenter);

    this.cursorX = newX;
    this.cursorY = newY;
  }

  private checkEdgeProximity(distanceFromCenter: number): void {
    const distanceToEdge = this.circleRadius - distanceFromCenter;

    if (distanceToEdge <= this.edgeThreshold) {
      this.circleColor = Color.Blue;
    } else {
      this.circleColor = Color.Red;
    }
  }

  private stopSensors(): void {
    try {
      sensor.off(sensor.SensorId.ACCELEROMETER);
      sensor.off(sensor.SensorId.GYROSCOPE);
      this.isMonitoring = false;
      promptAction.showToast({ message: 'Sensors stopped', duration: 1000 });
    } catch (error) {
      const e: BusinessError = error as BusinessError;
      console.error(`Failed to stop sensors. Code: ${e.code}, message: ${e.message}`);
    }
  }

  private showPosition(): void {
    const positionText = `X: ${this.cursorX.toFixed(1)}, Y: ${this.cursorY.toFixed(1)}`;
    promptAction.showToast({
      message: positionText,
      duration: 1000,
      bottom: '50%'
    });

  }

  build() {
    Stack() {
      Circle()
        .width(this.circleDiameter)
        .height(this.circleDiameter)
        .fill(Color.Transparent)
        .stroke(this.circleColor)
        .strokeWidth(3)
      Circle()
        .width(15)
        .height(15)
        .position({ x: this.cursorX, y: this.cursorY })
        .fill(Color.Red)
        .opacity(0.8)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a1a')
    .onClick(() => {
      this.showPosition();
    })

  }
}
Enter fullscreen mode Exit fullscreen mode

In addition, developers must also ensure the required sensor permissions.

"requestPermissions": [
      {
        "name": "ohos.permission.ACCELEROMETER",
        "reason": "$string:app_name",
        "usedScene": {

        }
      },
      {
        "name": "ohos.permission.GYROSCOPE",
        "reason": "$string:app_name",
        "usedScene": {

        }
      }
    ]
Enter fullscreen mode Exit fullscreen mode

Test Results

You can view sample screenshots from the application.

image.pngimage.pngimage.png

Limitation and Considerations

A real HarmonyOS watch is required to test the relevant sensor data, as the sensors do not work on emulators.

Related Documents or Links

https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-sensor

Written by Mehmet Algul

Top comments (0)