Androidをペンタブレットにする(3/3)

[Androidをペンタブレットにする(1/3)](https://qiita.com/poruruba/items/7a48fd52aa302bc48caa)、[Androidをペンタブレットにする(2/3)](https://qiita.com/poruruba/items/6a797e8cb7cde166d013)と続けてきましたが、最後にWebブラウザからBLEでAndroidを操作してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a815d129-5cc6-bc75-8321-954a57ca903c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/b09efab3-4009-af9e-7837-b9cdf0bbad90.png)

すべてのソースコードは以下にあります。

poruruba/sensor_pen_tablet
　https://github.com/poruruba/sensor_pen_tablet/tree/master/html

# ユーティリティクラス化

クラス化して、流用がきくようにします。

```js:SensorPenTablet.js
'use strict';

class SensorPenTablet{
  constructor(){
    this.init_const();

    this.encoder = new TextEncoder('utf-8');
    this.decoder = new TextDecoder('utf-8');
    this.bluetoothDevice = null;
    this.characteristics = new Map();
    this.recv_buffer = new Uint8Array(512);
    this.recv_len = 0;
    this.expected_len = 0;
    this.expected_slot;
    this.width = 0;
    this.height = 0;
    this.eventsSensor = [];
    this.eventsTouch = [];
    this.eventsClick = [];
    this.eventsData = [];
    this.eventsInit = [];
    this.eventsChange = [];
    this.isInitialized = false;

    this.PACKET_BUFFER_SIZE = 20;
  }

  async deviceOpen(){
    if( this.bluetoothDevice != null && this.bluetoothDevice.gatt.connected )
      this.bluetoothDevice.gatt.disconnect();

    console.log('Execute : requestDevice');
    var device = await navigator.bluetooth.requestDevice({
        filters: [{services:[ this.UUID_SERVICE ]}]
    });

    console.log("requestDevice OK");
    this.characteristics.clear();
    this.bluetoothDevice = device;
    var _this_ = this;
    this.bluetoothDevice.addEventListener('gattserverdisconnected', function(event){ _this_.onDisconnect(event); } );
//    this.bluetoothDevice.addEventListener('gattserverdisconnected', this.onDisconnect);

    var server = await this.bluetoothDevice.gatt.connect()
    console.log('Execute : getPrimaryService');
    var service = await server.getPrimaryService(this.UUID_SERVICE);
    console.log('Execute : getCharacteristic');

    await this.setCharacteristic(service, this.UUID_WRITE);
    await this.setCharacteristic(service, this.UUID_READ);
    await this.setCharacteristic(service, this.UUID_NOTIFY);
    await this.startNotify(this.UUID_NOTIFY);
    console.log('device_open done');

    await this.readChar(this.UUID_READ);

    return this.bluetoothDevice.name;
  }

  deviceClose(){
    if( this.bluetoothDevice != null && this.bluetoothDevice.gatt.connected )
      this.bluetoothDevice.gatt.disconnect();
  }

  async onDisconnect(event){
    console.log('onDisconnect');
    this.isInitialized = false;
    this.characteristics.clear();
    await this.fireEvent(this.eventsInit, 1, {});
  }

  async setCharacteristic(service, characteristicUuid) {
    var characteristic = await service.getCharacteristic(characteristicUuid)
    console.log('setCharacteristic : ' + characteristicUuid);
    this.characteristics.set(characteristicUuid, characteristic);
    var _this_ = this;
    characteristic.addEventListener('characteristicvaluechanged', function(event){ _this_.onDataChanged(event); });
//    characteristic.addEventListener('characteristicvaluechanged', this.onDataChanged);
    return service;
  }

  startNotify(uuid) {
    if( this.characteristics.get(uuid) === undefined )
        throw "Not Connected";

    console.log('Execute : startNotifications');
    return this.characteristics.get(uuid).startNotifications();
  }

  writeChar(uuid, array_value) {
    if( this.characteristics.get(uuid) === undefined )
        throw "Not Connected";

    console.log('Execute : writeValue');
    let data = Uint8Array.from(array_value);
    return this.characteristics.get(uuid).writeValue(data);
  }
  
  readChar(uuid) {
    if( this.characteristics.get(uuid) === undefined )
        throw "Not Connected";

    console.log('Execute : readValue');
    return this.characteristics.get(uuid).readValue();
  }
  
  async onDataChanged(event){
    console.log('onDataChanged');

    let characteristic = event.target;
    if( characteristic.uuid == this.UUID_READ){
        let value = this.dataview_to_uint8array(characteristic.value);
        this.PACKET_BUFFER_SIZE = ((value[0] & 0x00ff) << 8) | value[1];

        if( !this.isInitialized ){
          this.isInitialized = true;
          var cap = ((value[2] & 0x00ff) << 24) | ((value[3] & 0x00ff) << 16) | ((value[4] & 0x00ff) << 8) | value[5]; 
          await this.fireEvent(this.eventsInit, 0, {cap: cap});
        }
    }else
    if( characteristic.uuid == this.UUID_NOTIFY ){
      let value = this.dataview_to_uint8array(characteristic.value);

      if( this.expected_len > 0 && value[0] != this.expected_slot )
        this.expected_len = 0;
  
      if( this.expected_len == 0 ){
          if( value[0] != 0x83 )
              return;
          this.recv_len = 0;
          this.expected_len = (value[1] << 8) | value[2];
          this.array_copy(this.recv_buffer, this.recv_len, value, 3, value.length - 3);
          this.recv_len += value.length - 3;
          this.expected_slot = 0;
          if( this.recv_len < this.expected_len )
              return;
      }else{
          this.array_copy(this.recv_buffer, this.recv_len, value, 1, value.length - 1);
          this.recv_len += value.length - 1;
          this.expected_slot++;
          if( this.recv_len < this.expected_len )
              return;
      }
      this.expected_len = 0;

      await this.processResponse(this.recv_buffer, this.recv_len);
    }
  }

  async processResponse(recv_buffer, recv_len){
    switch(recv_buffer[0]){
      case this.RSP_TEXT: {
        var len = ((recv_buffer[2] & 0x00ff) << 8 ) | recv_buffer[3];
        var text = this.decoder.decode(recv_buffer.slice(4, 4 + len));
        var event = {
          type: recv_buffer[1],
          text: text,
        };
        await this.fireEvent(this.eventsData, this.RSP_TEXT, event);
        break;
      }
      case this.RSP_LOCATION: {
        var dv = new DataView(recv_buffer.buffer, 1, 16);
        var lat = dv.getFloat64(0, false);
        var lng = dv.getFloat64(8, false);
        var event = {
          lat: lat,
          lng: lng,
        };
        await this.fireEvent(this.eventsData, this.RSP_LOCATION, event);
        break;
      }
      case this.RSP_MAGNETIC:
      case this.RSP_GYROSCOPE:
      case this.RSP_ACCELEROMETER:
      {
        var dv = new DataView(recv_buffer.buffer, 1, 12);
        var x = dv.getFloat32(0, false);
        var y = dv.getFloat32(4, false);
        var z = dv.getFloat32(8, false);
        var event = {
          x: x, y: y, z: z,
        };
        await this.fireEvent(this.eventsSensor, recv_buffer[0], event);
        break;
      }
      case this.RSP_TOUCH_EVENT: {
          var id = recv_buffer[1];
          var dv = new DataView(recv_buffer.buffer, 2, 8);
          var action = dv.getInt32(0, false);
          var targetId = dv.getInt32(4, false);
          var count = recv_buffer[10];
          var pointers = [];
          for( var i = 0 ; i < count ; i++ ){
              var dv = new DataView(recv_buffer.buffer, 11 + i * 4 * 3, 4 * 3);
              var pointer = { pointerId: dv.getInt32(0, false), x: dv.getFloat32(4, false), y: dv.getFloat32(8, false)};
              pointers.push(pointer);
          }

          var event = {
            id: id,
            action: action,
            targetId: targetId,
            pointers: pointers
          };
          await this.fireEvent(this.eventsTouch, this.RSP_TOUCH_EVENT, event);

          break;
      }
      case this.RSP_BUTTON_EVENT: {
        await this.fireEvent(this.eventsClick, this.RSP_BUTTON_EVENT, { button: recv_buffer[1] });
        break;
      }
      case this.RSP_PANEL_CHANGE: {
          var event = {
            panel: recv_buffer[1],
            size: [],
          };
          if( recv_buffer[1] == 0x02 ){
            var dv = new DataView(recv_buffer.buffer, 3, 8);
            event.size.push({ width: dv.getInt32(0, false), height: dv.getInt32(4, false)});
          }else if( recv_buffer[2] = 0x03 ){
            var dv = new DataView(recv_buffer.buffer, 3, 16);
            for( var i = 0 ; i < 2 ; i++ ){
              event.size.push({ width: dv.getInt32(i * 8, false), height: dv.getInt32(i * 8 + 4, false)});
            }
          }
          await this.fireEvent(this.eventsChange, this.RSP_PANEL_CHANGE, event);
          break;
      }
    }
  }

  async fireEvent(events, type, event){
    for( var i = 0 ; i < events.length ; i++ )
      await events[i](type, event);
  }

  async sendBuffer(send_buffer, len){
      var offset = 0;
      var slot = 0;
      var packet_size = 0;
      var value = new Array(this.PACKET_BUFFER_SIZE);
      do{
          if( offset == 0){
              packet_size = len - offset;
              if( packet_size >= (this.PACKET_BUFFER_SIZE - 3) )
                  packet_size = this.PACKET_BUFFER_SIZE - 3;
              else
                  value = new Array(packet_size + 3);

              value[0] = 0x83;
              value[1] = (len >> 8) & 0xff;
              value[2] = len & 0xff;
              this.array_copy(value, 3, send_buffer, offset, packet_size);

              offset += packet_size;
              packet_size += 3;
          }else{
              packet_size = len - offset;
              if( packet_size >= (this.PACKET_BUFFER_SIZE - 1) )
                  packet_size = this.PACKET_BUFFER_SIZE - 1;
              else
                  value = new Array(packet_size + 1);

              value[0] = slot++;
              this.array_copy(value, 1, send_buffer, offset, packet_size);
    
              offset += packet_size;
              packet_size += 1;
          }

          await this.writeChar(this.UUID_WRITE, value);
      }while(packet_size >= this.PACKET_BUFFER_SIZE);            
  }

  addEventListener(type, listener){
    var events;
    switch( type ){
      case 'sensor' : events = this.eventsSensor; break;
      case 'touch': events = this.eventsTouch; break;
      case 'change' : events = this.eventsChange; break;
      case 'data': events = this.eventsData; break;
      case 'click': events = this.eventsClick; break;
      case 'init': events = this.eventsInit; break;
      default:
        return;
    }      
    events.push(listener);
  }

  removeEventListener(type, listener){
    var events;
    switch( type ){
      case 'sensor' : events = this.eventsSensor; break;
      case 'touch' : events = this.eventsTouch; break;
      case 'change' : events = this.eventsChange; break;
      case 'data' : events = this.eventsData; break;
      case 'click' : events = this.eventsClick; break;
      case 'init' : events = this.eventsInit; break;
      default:
        return;
    }
    var index = events.indexOf(listener);
    if( index >= 0 )
      events.splice(index, 1);
  }

  async requestRecognition(){
    var send_buffer = [this.CMD_RECOGNITION];
    await this.sendBuffer(send_buffer, send_buffer.length);
  }

  async requestLocation(){
    var send_buffer = [this.CMD_LOCATION];
    await this.sendBuffer(send_buffer, send_buffer.length);
  }
  
  async sendClipboard(text){
    var text_bin = this.encoder.encode(text);
    var send_buffer = new Uint8Array(1 + 2 + text_bin.length);
    send_buffer[0] = this.CMD_PASTE;
    send_buffer[1] = (text_bin.length >> 8) & 0xff;
    send_buffer[2] = text_bin.length & 0xff;
    this.array_copy(send_buffer, 3, text_bin, 0, text_bin.length);
    await this.sendBuffer(send_buffer, send_buffer.length);
  }

  async sendToast(text){
    var text_bin = this.encoder.encode(text);
    var send_buffer = new Uint8Array(1 + 2 + text_bin.length);
    send_buffer[0] = this.CMD_TOAST;
    send_buffer[1] = (text_bin.length >> 8) & 0xff;
    send_buffer[2] = text_bin.length & 0xff;
    this.array_copy(send_buffer, 3, text_bin, 0, text_bin.length);
    await this.sendBuffer(send_buffer, send_buffer.length);
  }

  async setButton(num){
    var send_buffer = [this.CMD_PANEL_MODE, 0x01, num];
    await this.sendBuffer(send_buffer, send_buffer.length);
  }
  
  async setPanel(panel, num = 0){
    var send_buffer = [this.CMD_PANEL_MODE, panel, num];
    await this.sendBuffer(send_buffer, send_buffer.length);
  }

  async setSensorMask(types){
    var send_buffer = [this.CMD_SENSOR_MASK, types];
    await this.sendBuffer(send_buffer, send_buffer.length);
  }

  init_const(){
    this.UUID_SERVICE = '08030900-7d3b-4ebf-94e9-18abc4cebede';
    this.UUID_WRITE = "08030901-7d3b-4ebf-94e9-18abc4cebede";
    this.UUID_READ = "08030902-7d3b-4ebf-94e9-18abc4cebede";
    this.UUID_NOTIFY = "08030903-7d3b-4ebf-94e9-18abc4cebede";
    
    this.CMD_PASTE = 0x01;
    this.CMD_LOCATION = 0x03;
    this.CMD_PANEL_MODE = 0x08;
    this.CMD_TOAST = 0x0c;
    this.CMD_RECOGNITION = 0x0d;
    this.CMD_SENSOR_MASK = 0x0e;
    this.CMD_RAW = 0xff;
    
    this.RSP_ACK = 0x00;
    this.RSP_PANEL_CHANGE = 0x19;
    this.RSP_TEXT = 0x11;
    this.RSP_MAGNETIC = 0x12;
    this.RSP_LOCATION = 0x13;
    this.RSP_GYROSCOPE = 0x14;
    this.RSP_ACCELEROMETER = 0x15;
    this.RSP_TOUCH_EVENT = 0x17;
    this.RSP_BUTTON_EVENT = 0x18;
    this.RSP_RAW = 0xff;
  }

  dataview_to_uint8array(array){
    var result = new Uint8Array(array.byteLength);
    for( var i = 0 ; i < array.byteLength ; i++ )
      result[i] = array.getUint8(i);
      
    return result;
  }
  
  array_copy( dest, dest_offset, src, src_offset, length ){
      for( var i = 0 ; i < length ; i++ )
          dest[dest_offset + i] = src[src_offset + i];
      
      return dest;
  }
}
```

#動作確認用ページ

動きをつかんでいただけるように、すべての機能を確認できるページを作成しました。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <title>SensorPenTablet Controller</title>

  <link rel="stylesheet" href="css/start.css">
  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="js/SensorPenTablet.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>SensorPenTablet Controller</h1>

        <button class="btn btn-default" v-on:click="device_open">open</button>
        <label>deviceName</label> {{deviceName}}
        <label>capability</label> {{capability}}<br>
        <div class="row">
            <div class="col-xs-4">
                <h3>TouchPad</h3>
                <label>width</label> {{width}}<br>
                <label>height</label> {{height}}<br>
                <label>action</label> {{action}}<br>
                <label>targetId</label> {{targetId}}<br>
                <ol>
                    <li v-for="(pointer, index) in pointers">{{pointer.pointerId}} ({{pointer.x.toFixed(0)}},{{pointer.y.toFixed(0)}})</li>
                </ol>
            </div>
            <div class="col-xs-4">
                <h3>TouchPad1</h3>
                <label>width</label> {{width1}}<br>
                <label>height</label> {{height1}}<br>
                <label>action</label> {{action1}}<br>
                <label>targetId</label> {{targetId1}}<br>
                <ol>
                    <li v-for="(pointer, index) in pointers1">{{pointer.pointerId}} ({{pointer.x.toFixed(0)}},{{pointer.y.toFixed(0)}})</li>
                </ol>
            </div>
            <div class="col-xs-4">
                <h3>TouchPad2</h3>
                <label>width</label> {{width2}}<br>
                <label>height</label> {{height2}}<br>
                <label>action</label> {{action2}}<br>
                <label>targetId</label> {{targetId2}}<br>
                <ol>
                    <li v-for="(pointer, index) in pointers2">{{pointer.pointerId}} ({{pointer.x.toFixed(3)}},{{pointer.y.toFixed(0)}})</li>
                </ol>
            </div>
        </div>

        <div class="form-inline">
            <button class="btn btn-default" v-on:click="set_mask">SetMask</button>
            <label><input type="checkbox" v-model="is_magnetic">MagneticField&nbsp;</label>
            <label><input type="checkbox" v-model="is_gyroscope">Gyrometer&nbsp;</label>
            <label><input type="checkbox" v-model="is_accelerometer">Accelerometer&nbsp;</label>
        </div>

        <div class="row">
            <div class="col-xs-4">
                <h3>Magnetic Field</h3>
                <span v-if="magnetic">
                    <label>magnetic.x</label> {{magnetic.x.toFixed(4)}}<br>
                    <label>magnetic.y</label> {{magnetic.y.toFixed(4)}}<br>
                    <label>magnetic.z</label> {{magnetic.z.toFixed(4)}}<br>
                </span>
            </div>
            <div class="col-xs-4">
                <h3>Gyrometer</h3>
                <span v-if="gyro">
                    <label>gyro.x</label> {{gyro.x.toFixed(4)}}<br>
                    <label>gyro.y</label> {{gyro.y.toFixed(4)}}<br>
                    <label>gyro.z</label> {{gyro.z.toFixed(4)}}<br>
                </span>
            </div>
            <div class="col-xs-4">
                <h3>Accelerometer</h3>
                <span v-if="accel">
                    <label>accel.x</label> {{accel.x.toFixed(4)}}<br>
                    <label>accel.y</label> {{accel.y.toFixed(4)}}<br>
                    <label>accel.z</label> {{accel.z.toFixed(4)}}<br>
                </span>
            </div>
        </div>
        <h3>Buttons</h3>
        <ul class="list-unstyled">
            <li><label>fn_buttons</label> ( <span v-for="(count, index) in fn_count">{{count}} </span>)</li>
            <li><label>push_buttons</label> ( <span v-for="(count, index) in push_count">{{count}} </span>)</li>
        </ul>
        <div class="form-inline">
            <label>panel</label>
            <select class="form-control" v-model.number="panel" v-on:change="set_panel">
                <option value="0">None</option>
                <option value="1">Push</option>
                <option value="2">Touch</option>
                <option value="3">2Touch</option>
            </select>
            <label>button_num</label>
            <select class="form-control" v-model.number="button_num" v-on:change="set_button">
                <option value="0">0</option>
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
                <option value="4">4</option>
                <option value="5">5</option>
            </select>
        </div>
        <hr>
        <div>
            <input type="text" class="form-control" v-model="paste_toast">
            <button class="btn btn-default" v-on:click="send_copy">Clipboard</button>
            <button class="btn btn-default" v-on:click="send_toast">Toast</button>
        </div>
        <br>
        <div>
            <label>Location</label><br>
            <button class="btn btn-default" v-on:click="get_location">location</button> 
            <label>latitude</label> {{latitude}} <label>longiude</label> {{longitude}}<br>
        </div>
        <br>
        <div>
            <label>Recognition</label><br>
            <button class="btn btn-default" v-on:click="do_recognition">Recognition</button>
            <input type="text" class="form-control" v-model="recognition" readonly>
        </div>
        <br>
        <div>
            <label>QRCode</label>
            <input type="text" class="form-control" v-model="qrcode" readonly>
        </div>
        <br>
        <div>
            <label>Clipboard</label>
            <input type="text" class="form-control" v-model="copy" readonly>
        </div>
        <br>


        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="js/start.js"></script>
</body>
```

```js:start.js
'use strict';

//var vConsole = new VConsole();

var pentablet = new SensorPenTablet();

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        deviceName: '',
        latitude: 0.0,
        longitude: 0.0,
        paste_toast: '',
        button_num: 5,
        panel: 0,
        width: 0,
        height: 0,
        action: 0,
        targetId: 0,
        pointers: [],
        width1: 0,
        height1: 0,
        action1: 0,
        targetId1: 0,
        pointers1: [],
        width2: 0,
        height2: 0,
        action2: 0,
        targetId2: 0,
        pointers2: [],
        accel: null,
        gyro: null,
        magnetic: null,
        copy: '',
        qrcode: '',
        recognition: '',
        fn_count: [0, 0, 0, 0, 0],
        push_count: [0, 0, 0, 0, 0],
        capability: 0,
        is_magnetic: false,
        is_gyroscope: false,
        is_accelerometer: false,
    },
    computed: {
    },
    methods: {
        device_open: async function(){
            pentablet.addEventListener("init", this.initListener);
            pentablet.addEventListener("change", this.changeListener);
            pentablet.addEventListener("touch", this.touchListener);
            pentablet.addEventListener("click", this.buttonListener);
            pentablet.addEventListener("data", this.dataListener);
            pentablet.addEventListener("sensor", this.sensorListener);

            try{
                this.progress_open();
                this.deviceName = await pentablet.deviceOpen();
            }catch(error){
                console.log(error);
                this.progress_close();
                alert(error);
            }
        },
        initListener: async function(type, event){
            if( type == 0){
                this.capability = event.cap;
                this.progress_close();
            }
        },
        changeListener: async function(type, event){
            if( event.panel == 2 ){
                this.width = event.size[0].width;
                this.height = event.size[0].height;
            }else if( event.panel = 3 ){
                this.width1 = event.size[0].width;
                this.height1 = event.size[0].height;
                this.width2 = event.size[1].width;
                this.height2 = event.size[1].height;
                    }
        },
        touchListener: async function(type, event){
            var id = event.id;
            var action = event.action;
            var targetId = event.targetId;
            var list = event.pointers;
                    if( id == 0 ){
                        this.action = action;
                        this.targetId = targetId;
                        this.pointers = list;
                    }else if( id == 1 ){
                        this.action1 = action;
                        this.targetId1 = targetId;
                        this.pointers1 = list;
                    }else if( id == 2 ){
                        this.action2 = action;
                        this.targetId2 = targetId;
                        this.pointers2 = list;
                    }
        },
        buttonListener: function(type, event){
            var btn = event.button;
            if( btn >= 0x10 && btn <= 0x14 ){
                this.push_count[btn - 0x10]++;
                this.push_count = object_clone(this.push_count);
                }
            if( btn >= 0x20 && btn <= 0x24 ){
                this.fn_count[btn - 0x20]++;
                this.fn_count = object_clone(this.fn_count);
                }
        },
        dataListener: function(type, event){
            switch(type){
                case pentablet.RSP_LOCATION: {
                    this.latitude = event.lat;
                    this.longitude = event.lng;
                    break;
                }
                case pentablet.RSP_TEXT: {
                    var type = event.type;
                    switch( type ){
                        case 0x01: this.copy = event.text; break;
                        case 0x02: this.qrcode = event.text; break;
                        case 0x03: this.recognition = event.text; break;
                    }
                    break;
                }
                    }
        },
        sensorListener: function(type, event){
            switch(type){
                case pentablet.RSP_MAGNETIC: {
                    this.magnetic = { x: event.x, y: event.y, z: event.z };
                    break;
                }
                case pentablet.RSP_GYROSCOPE: {
                    this.gyro = { x: event.x, y: event.y, z: event.z };
                    break;
                    }
                case pentablet.RSP_ACCELEROMETER: {
                    this.accel = { x: event.x, y: event.y, z: event.z };
                    break;
                }
            }
        },
        get_location: async function(){
            try{
                await pentablet.requestLocation();
            }catch(error){
                console.log(error);
                alert(error);
            }
        },
        send_copy: async function(){
            try{
                await pentablet.sendClipboard(this.paste_toast);
            }catch(error){
                console.log(error);
                alert(error);
            }
        },
        send_toast: async function(){
            try{
                await pentablet.sendToast(this.paste_toast);
            }catch(error){
                console.log(error);
                alert(error);
            }
        },
        set_button: async function(){
            try{
                await pentablet.setButton(this.button_num);
            }catch(error){
                console.log(error);
                alert(error);
            }
        },
        set_panel: async function(){
            try{
                await pentablet.setPanel(this.panel);
            }catch(error){
                console.log(error);
                alert(error);
            }
        },
        set_mask: async function(){
            try{
                var mask = 0x00;
                if( this.is_magnetic ) mask |= 0x01;
                if( this.is_gyroscope ) mask |= 0x02;
                if( this.is_accelerometer ) mask |= 0x04;
                await pentablet.setSensorMask(mask);
            }catch(error){
                console.log(error);
                alert(error);
            }
        },
        do_recognition: async function(){
            try{
                await pentablet.requestRecognition();
            }catch(error){
                console.log(error);
                alert(error);
                }
        }
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );

function object_clone(object){
    return JSON.parse(JSON.stringify(object));   
}
```

#お絵描きページ

なんとなく動きがわかったところで、応用例として、自由お絵描きページを作ってみました。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <title>Free Painter</title>

  <link rel="stylesheet" href="css/start.css">
  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="js/SensorPenTablet.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>Free Painter</h1>

        <button class="btn btn-default" v-on:click="device_open">open</button>
        <label>deviceName</label> {{deviceName}}<br>
        <label>original_width</label> {{original_width}} <label>original_height</label> {{original_height}}<br>

        <div class="btn-group" role="group">
            <button class="btn btn-default" v-on:click="canvas_button(0x20)">Black</button>
            <button class="btn btn-default" v-on:click="canvas_button(0x21)">Red</button>
            <button class="btn btn-default" v-on:click="canvas_button(0x22)">Green</button>
            <button class="btn btn-default" v-on:click="canvas_button(0x23)">Blue</button>
            <button class="btn btn-default" v-on:click="canvas_button(0x24)">Clear</button>
        </div><br>
        <canvas id="canvas" class="img-thumbnail"></canvas>

        
        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="js/start.js"></script>
</body>
```

```js:start.js
'use strict';

//var vConsole = new VConsole();

const CANVAS_SIZE = 1000;

var pentablet = new SensorPenTablet();

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        deviceName: '',
        original_width: 0,
        original_height: 0,
        prev_pointerId: -1,
        div: 0.0,
        canvas: null,
        context: null,
        capability: 0,
    },
    computed: {
    },
    methods: {
        device_open: async function(){
            pentablet.addEventListener("init", this.initListener);
            pentablet.addEventListener("change", this.changeListener);
            pentablet.addEventListener("touch", this.touchListener);
            pentablet.addEventListener("click", this.buttonListener);

            try{
                this.progress_open();
                this.deviceName = await pentablet.deviceOpen();
            }catch(error){
                this.progress_close();
            }
        },
        touchListener: async function(type, event){
            if( this.context != null ){
                if( event.id != 0 )
                    return;

                var action = event.action;
                var targetId = event.targetId;
                var pointers = event.pointers;
                if( action == 0){
                    this.prev_pointerId = targetId;
                    this.context.beginPath();
                    this.context.moveTo(Math.floor(pointers[0].x * this.div), Math.floor(pointers[0].y * this.div));
                }else if( action == 2 || action == 5 || action == 6){
                    if( this.prev_pointerId >= 0 ){
                        var index = 0;
                        for( var i = 0 ; i < pointers.length ; i++ ){
                            if( pointers[i].pointerId == this.prev_pointerId ){
                                index = i;
                                break;
                            }
                        }
                        this.context.lineTo(Math.floor(pointers[index].x * this.div), Math.floor(pointers[index].y * this.div));
                        this.context.stroke();
                        if( action == 6 && this.prev_pointerId == pointers[index].pointerId )
                            this.prev_pointerId = -1;
                    }
                }else if( action == 1 ){
                    if( this.prev_pointerId > 0 ){
                        if( this.prev_pointerId == pointers[0].pointerId ){
                            this.context.lineTo(Math.floor(pointers[0].x * this.div), Math.floor(pointers[0].y * this.div));
                            this.context.stroke();
                        }
                        this.prev_pointerId = -1;
                    }
                }
            }
        },
        initListener: async function(type, event){
            this.capability = event.cap;
            try{
                await pentablet.setPanel(0x02);
            }catch(error){
                console.log(error);
                alert(error);
            }finally{
                this.progress_close();
            }
        },
        changeListener: async function(type, event){
            if( event.panel == 2 && this.context == null ){
                this.original_width = event.size[0].width;
                this.original_height = event.size[0].height;
                this.canvas = $('#canvas')[0];
                if( this.original_width >= this.original_height ){
                    this.div = CANVAS_SIZE / this.original_width;
                    this.canvas.width = CANVAS_SIZE;
                    this.canvas.height = Math.floor(this.original_height * this.div);
                }else{
                    this.div = CANVAS_SIZE / this.original_height;
                    this.canvas.width = Math.floor(this.original_width * this.div);
                    this.canvas.height = CANVAS_SIZE;
                }
                this.context = canvas.getContext('2d');
            }
        },
        buttonListener: function(type, event){
            this.canvas_button(event.button);
        },
        canvas_button: function(btn){
            if( this.context != null ){
                if( btn == 0x20 ){
                    this.context.strokeStyle = 'rgb(0, 0, 0)';
                }else if( btn == 0x21 ){
                    this.context.strokeStyle = 'rgb(255, 0, 0)';
                }else if( btn == 0x22 ){
                    this.context.strokeStyle = 'rgb(0, 255, 0)';
                }else if( btn == 0x23 ){
                    this.context.strokeStyle = 'rgb(0, 0, 255)';
                }else if( btn == 0x24 ){
                    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
                }
            }
        },
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );

function object_clone(object){
    return JSON.parse(JSON.stringify(object));   
}
```

#最後に

ソースコードそのままですみません。
ユーティリティクラスはそこまで複雑ではないので理解できると思います。
それを使ったWebページは、そのクラスを呼んでいるだけです。。。。お許しください。

以上