「スマポン」という、コミュニケーショントイの顔の表示のハナシ

# ハジメに
「スマポン」という、コミュニケーショントイ~~が投げ売りされていたの~~を買った。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/46e64703-6b65-621b-1638-8f89597f7a10.png)

スマホに専用アプリを入れて、スマホに乗せることでコミニュケーションが取れるらしい
[スマポン｜商品情報｜タカラトミー](https://www.takaratomy.co.jp/products/smapon/)

ちなみに、裏から見るとこんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/361adcd0-f38b-3e9f-d7c4-3b43c4fd4f9b.png)

このスマポン本体にコンピューター的なモノは搭載されておらず、
スマホの専用アプリで顔の表示制御や会話などを行っている。
じゃ仕組みさえわかってしまえば、好きな顔の表示が出来るんじゃね？
というおハナシ

# 顔の表示の仕組み

1. スマホにスマポンを置くことで、3つ付いている突起により3点タッチされる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/e3469e25-e127-e64b-f93a-1a87b49919e3.png)

2. タッチされた3点を結んで出来る二等辺三角形の底辺を、顔の底辺とする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/6735d056-61ff-7a5a-99ed-5510d724e951.png)

3. 二等辺三角形の高さの中央を、顔の中央とする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/6e6d9fd8-bb1d-ae74-2529-b14f337c3194.png)

4. 8×8の受光部分に対して、一定の法則で並び変えた画像データを表示する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/03553d4d-2941-2144-df6e-204fc099d2d5.png)

5. これをスマポンを通して見ると、本来の画像データで表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/5fa14f9e-10ec-01a9-d4a2-a44cc6828cdb.png)

## 画像データの並び替えについて

スマホに表示された画像データは、スマポン上では別の場所に表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/223a31ff-4d23-0af5-ca9b-e38915e0f86c.png)

下図が変換パターン（数字で対応）です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/f514ca18-7d30-aa7e-3249-2c7b293ea4c7.png)

# 実装

なんとなくVue.jsで実装してますが、
説明部分のコードは、JavaScriptが分かれば問題無いです。

## 3点の座標を取得する
touchstartイベントで取得する。
取得した際の座標は、全体座標なので
作画するcanvasを基準とした座標を取得する。

```javascript:3点タッチ
/**
 * canvasの＠touchstartに設定
 * @param e TouchEvent
 */
onTouchStart: function(e) {
  let x = [], y = [];

  for (let i = 0; i < e.touches.length; i++) {
    let targetTouches = e.targetTouches[i];
    let touchX = targetTouches.pageX;
    let touchY = targetTouches.pageY;

    // 要素の位置を取得
    let clientRect = this.canvas.getBoundingClientRect();
    let positionX = clientRect.left + window.pageXOffset;
    let positionY = clientRect.top + window.pageYOffset;

    // 要素内におけるタッチ位置を計算
    x[i] = touchX - positionX;
    y[i] = touchY - positionY;
  }

  // 3点タッチされた場合
  if(e.touches.length == 3){
    // x[0～2]、y[0～2]に座標が設定されている
  }
}
```
## それぞれの辺の長さを取得する
2点の座標の距離を求める式は
$\sqrt{(x_{2}−x_{1})^2+(y_{2}−y_{1})^2}$
そのまま実装

```javascript:2点の距離を取得
/**
 * 2点の距離を取得
 * @param x1 1つ目のX座標
 * @param y1 1つ目のY座標
 * @param x2 2つ目のX座標
 * @param y2 2つ目のY座標
 * @returns 距離
 */
getLengthOf2Point(x1, y1, x2, y2) {
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
```

## 底辺を取得する。
3点それぞれの長さを比較し、一番短いものが底辺となる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/b46d6c25-e1a5-955f-a87d-a34d6a1da7a4.png)

## 高さの中央を取得する

2点の座標の中央の座標をcx,cyとすると求める式は
$cx = \frac{x_{1} + x_{2}}{2}$
$cy = \frac{y_{1} + y_{2}}{2}$
そのまま実装

```javascript:2点の中央を取得
/**
 * 2点の中央を取得
 * @param x1 1つ目のX座標
 * @param y1 1つ目のY座標
 * @param x2 2つ目のX座標
 * @param y2 2つ目のY座標
 * @returns 2点の中央
 */
getCenterOf2Point(x1, y1, x2, y2) {
  let cx, cy;
  cx = (x1 + x2) / 2;
  cy = (y1 + y2) / 2;

  return {cx, cy};
}
```
### 底辺の中央を取得する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/fc6f4b0f-11d2-2c11-e412-0b58bb7b20ae.png)

### 底辺の中央～頂点の中央を取得する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/2ecf9d6b-631f-492f-05c1-a8460e57e95c.png)

## 角度を取得する
スマホに対してスマポンがどの角度で配置されているか取得する必要があるため
底辺の中央～頂点の中央、頂点の間の角度を取得する

下図のような状態の場合を0°とし
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/7a432db0-033d-c7fd-e7a8-d799148f33bb.png)

下記のような状態の場合、270°(-90°)として取得する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/8cd1e17f-f291-aa35-f31f-1be6eb0ff512.png)

2点間の角度を求める場合、Math.atan2関数で取得できるらしいので使用する。

あと、この関数では左上を基準に角度を取得するため
下図のような状態の場合0°となり
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/839fd256-6e39-6e63-9c94-4f8dbe576764.png)

下図の場合、270°(-90°)として取得されてしまう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/80d0f2c8-4b1c-44f7-e89a-cfa496d7ddd0.png)

なので結果に＋90°する。

```javascript:2点の角度を取得
/**
 * 角度を取得
 * @param x1 1つ目のX座標
 * @param y1 1つ目のY座標
 * @param x2 2つ目のX座標
 * @param y2 2つ目のY座標
 * @returns 角度
 */
getDegreeOf2Point(x1, y1, x2, y2) {
  let radian = Math.atan2(y2 - y1, x2 - x1);
  return radian * (180 / Math.PI) + 90;
},
```
## 画像を表示する

### 任意の位置に回転させた四角形を書く場合

下図のように指定角度分回転させた四角形を作画する場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/79d2fa10-5e64-0833-5d44-3cdfb76c1b98.png)

#### 基準点を移動する

回転は基準点を中心に行われるため、まず基準点を移動する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/ddaba36d-41ea-9995-8636-8602f852186e.png)

```javascript:基準点を移動
// x : x座標, y : y座標
this.ctx.translate(x, y);
```

#### 回転する

任意の角度分、回転する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/e3094aa9-a236-c212-5fd3-66c8e5bcfcc4.png)

```javascript:回転する
// angle : 角度
this.ctx.rotate((angle  * Math.PI) / 180);
```
#### 四角形を描く

四角形を描く際左上が基準となるため、四角形のサイズの半分だけ左上にずらして作画する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/c2a220d2-bd6b-c321-846f-630c5cbe0973.png)

```javascript:四角形を描く
// width : 幅, height : 高さ
this.ctx.fillRect(
  - width / 2,
  - height / 2,
  width,
  height
);
```

### 実装してみたコード
<details><summary>割と長いので折り畳み</summary><div>

```vue:サンプルコード
<template>
<div>
  <canvas width="300" height="300" class="canvas" @touchstart="onTouchStart($event)"></canvas>
  <br>
  <button class="button" @click="onClick(0)">0</button>
  <button class="button" @click="onClick(1)">1</button>
  <button class="button" @click="onClick(2)">2</button>
</div>
</template>

<script>
// 顔データ
const faceData = [
  ["#FFFFFF","#FFFF00","#00FFFF","#00FF00","#FF00FF","#FF0000","#0000FF","#000000"
  ,"#FFFFFF","#FFFF00","#00FFFF","#00FF00","#FF00FF","#FF0000","#0000FF","#000000"
  ,"#FFFFFF","#FFFF00","#00FFFF","#00FF00","#FF00FF","#FF0000","#0000FF","#000000"
  ,"#FFFFFF","#FFFF00","#00FFFF","#00FF00","#FF00FF","#FF0000","#0000FF","#000000"
  ,"#FFFFFF","#FFFF00","#00FFFF","#00FF00","#FF00FF","#FF0000","#0000FF","#000000"
  ,"#FFFFFF","#FFFF00","#00FFFF","#00FF00","#FF00FF","#FF0000","#0000FF","#000000"
  ,"#FFFFFF","#FFFF00","#00FFFF","#00FF00","#FF00FF","#FF0000","#0000FF","#000000"
  ,"#FFFFFF","#FFFF00","#00FFFF","#00FF00","#FF00FF","#FF0000","#0000FF","#000000"] // 0
 ,["#000000","#000000","#B97A57","#000000","#000000","#B97A57","#000000","#000000"
  ,"#B97A57","#B97A57","#000000","#000000","#000000","#000000","#B97A57","#B97A57"
  ,"#000000","#FFFFFF","#FFFFFF","#000000","#000000","#FFFFFF","#FFFFFF","#000000"
  ,"#000000","#FFFFFF","#000000","#000000","#000000","#FFFFFF","#000000","#000000"
  ,"#000000","#7F7F7F","#7F7F7F","#000000","#000000","#7F7F7F","#7F7F7F","#000000"
  ,"#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000"
  ,"#000000","#ED1C24","#ED1C24","#ED1C24","#ED1C24","#ED1C24","#ED1C24","#000000"
  ,"#000000","#000000","#ED1C24","#ED1C24","#ED1C24","#ED1C24","#000000","#000000"] // 1
 ,["#000000","#000000","#000000","#FFA300","#FFA300","#000000","#000000","#000000"
  ,"#000000","#FFA300","#FFA300","#00E756","#00E756","#00E756","#008751","#000000"
  ,"#000000","#FFA300","#00E756","#00E756","#000000","#00E756","#000000","#008751"
  ,"#000000","#000000","#00E756","#00E756","#000000","#00E756","#000000","#008751"
  ,"#000000","#FFA300","#00E756","#FFA300","#008751","#FFFFFF","#008751","#AB5236"
  ,"#000000","#FF0042","#FF0042","#00E756","#00E756","#00E756","#00E756","#008751"
  ,"#FFA300","#FF0042","#FF0042","#00E756","#FFFFFF","#FFFFFF","#FFFFFF","#C2C3C7"
  ,"#00E756","#008751","#00E756","#FF0042","#FF0042","#FFFFFF","#FFFFFF","#7E2053"] // 2
];

export default {
  data: function() {
    return {
      cx: 0,
      cy: 0,
      height: 0,
      degree: 0,
      face : []
    };
  },
  props: {
  },
  watch: {
     face() {
      if(this.height > 0){
        this.paintFace();
      }
    }
  },
  methods: {
    onClick(number){
      this.face = faceData[number];
    },
    /**
     * 2点の距離を取得
     * @param x1 1つ目のX座標
     * @param y1 1つ目のY座標
     * @param x2 2つ目のX座標
     * @param y2 2つ目のY座標
     * @returns 中央の座標
     */
    getCenterOf2Point(x1, y1, x2, y2) {
      let cx, cy;
      cx = (x2 + x1) / 2;
      cy = (y2 + y1) / 2;
      return { cx, cy };
    },
    /**
     * 2点の距離を取得
     * @param x1 1つ目のX座標
     * @param y1 1つ目のY座標
     * @param x2 2つ目のX座標
     * @param y2 2つ目のY座標
     * @returns 距離
     */
    getLengthOf2Point(x1, y1, x2, y2) {
      return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
    },
    /**
     * 角度を取得
     * @param x1 1つ目のX座標
     * @param y1 1つ目のY座標
     * @param x2 2つ目のX座標
     * @param y2 2つ目のY座標
     * @returns 角度
     */
    getDegreeOf2Point(x1, y1, x2, y2) {
      let radian = Math.atan2(y2 - y1, x2 - x1);
      return radian * (180 / Math.PI) + 90;
    },
    /**
     * 顔作画
     */
    paintFace() {
      const magnificationFactor = 0.73;
      const baseDotSize = 5;
      const numberOfDotsByLine = 8;
      const baseFrameSize = 2;
      const numberOfDotsInLine = 8;
      const conversionTable = [ 2, 3,18,19,20,21, 4, 5
                              ,10,11,26,27,28,29,12,13
                              ,0 ,1	,16,17,22,23,6 , 7
                              ,8 ,9	,24,25,30,31,14,15
                              ,48,49,32,33,38,39,54,55
                              ,56,57,40,41,46,47,62,63
                              ,50,51,34,35,36,37,52,53
                              ,58,59,42,43,44,45,60,61];

      let magnification = (this.height / (baseDotSize * numberOfDotsByLine)) * magnificationFactor;
      let w = baseDotSize * numberOfDotsByLine * magnification;
      let h = baseDotSize * numberOfDotsByLine * magnification;
      let paintDotCoordinates = [];
      let paintDotSize = baseDotSize * magnification;
      for(let i = 0; i < numberOfDotsByLine; i++){
        paintDotCoordinates[i] = i * baseDotSize * magnification;
      }

      let dots = [];
      for(let i=0;i < numberOfDotsByLine * numberOfDotsByLine; i++){
        dots[conversionTable[i]] = this.face[i];
      }

      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      this.ctx.beginPath();
      this.ctx.save();
      this.ctx.translate(this.cx, this.cy);
      this.ctx.rotate((this.degree * Math.PI) / 180);

      this.ctx.fillStyle = "#000000";
      this.ctx.fillRect(
        -w / 2 - (baseFrameSize * magnification),
        -h / 2 - (baseFrameSize * magnification),
        magnification * (baseDotSize * numberOfDotsInLine + baseFrameSize * 2),
        magnification * (baseDotSize * numberOfDotsInLine + baseFrameSize * 2)
      );

      for (let indexY = 0; indexY < numberOfDotsInLine; indexY++) {
        for (let indexX = 0; indexX < numberOfDotsInLine; indexX++) {
          this.ctx.fillStyle = dots[indexY * numberOfDotsInLine + indexX];
          this.ctx.fillRect(
            -w / 2 + paintDotCoordinates[indexX],
            -h / 2 + paintDotCoordinates[indexY],
            paintDotSize,
            paintDotSize
          );
        }
      }

      this.ctx.restore();
    },
    /**
     * タッチスタート
     * @param e イベント
     */
    onTouchStart: function(e) {
      let x = [], y = [];

      for (let i = 0; i < e.touches.length; i++) {
        let targetTouches = e.targetTouches[i];
        let touchX = targetTouches.pageX;
        let touchY = targetTouches.pageY;

        // 要素の位置を取得
        let clientRect = this.canvas.getBoundingClientRect();
        let positionX = clientRect.left + window.pageXOffset;
        let positionY = clientRect.top + window.pageYOffset;

        // 要素内におけるタッチ位置を計算
        x[i] = touchX - positionX;
        y[i] = touchY - positionY;
        // this.paintDot(x[i], y[i]);
      }

      if(e.touches.length == 3){
        this.initialization(x[0], y[0], x[1], y[1], x[2], y[2]);
        this.paintFace();
      }
    },
    /**
     * 初期化
     * @param x1 1つ目のX座標
     * @param y1 1つ目のY座標
     * @param x2 2つ目のX座標
     * @param y2 2つ目のY座標
     * @param y3 3つ目のX座標
     * @param y3 3つ目のY座標
     */
    initialization(x1, y1, x2, y2, x3, y3) {
      let length12 = this.getLengthOf2Point(x1, y1, x2, y2);
      let length13 = this.getLengthOf2Point(x1, y1, x3, y3);
      let length23 = this.getLengthOf2Point(x2, y2, x3, y3);

      let bottomLine = { cx: 0, cy: 0 };
      let centerLine = { cx: 0, cy: 0 };

      if (length23 < length12 && length23 < length13) {
        // x1,y1が頂点
        bottomLine = this.getCenterOf2Point(x2, y2, x3, y3);
        centerLine = this.getCenterOf2Point(bottomLine.cx, bottomLine.cy, x1, y1);
        this.height = this.getLengthOf2Point(bottomLine.cx, bottomLine.cy, x1, y1);
        this.degree = this.getDegreeOf2Point(centerLine.cx, centerLine.cy, x1, y1);
      } else if (length13 < length12 && length13 < length23) {
        // x2,y2が頂点
        bottomLine = this.getCenterOf2Point(x1, y1, x3, y3);
        centerLine = this.getCenterOf2Point(bottomLine.cx, bottomLine.cy, x2, y2);
        this.height = this.getLengthOf2Point(bottomLine.cx, bottomLine.cy, x2, y2);
        this.degree = this.getDegreeOf2Point(centerLine.cx, centerLine.cy, x2, y2);
      } else {
        // x3,y3が頂点
        bottomLine = this.getCenterOf2Point(x1, y1, x2, y2);
        centerLine = this.getCenterOf2Point(bottomLine.cx, bottomLine.cy, x3, y3);
        this.height = this.getLengthOf2Point(bottomLine.cx, bottomLine.cy, x3, y3);
        this.degree = this.getDegreeOf2Point(centerLine.cx, centerLine.cy, x3, y3);
      }

      this.cx = centerLine.cx;
      this.cy = centerLine.cy;
    }
  },
  mounted() {
    this.canvas = document.querySelector(".canvas");
    this.ctx = this.canvas.getContext("2d");

    this.face = faceData[0];
    // this.initialization(110, 200, 150, 200, 130, 10);
  }
};
</script>

<style scoped>
.canvas {
  border: 1px solid #000000;
  background-color: #EEEEEE;
}
.button {
  width: 60px;
  height:60px;
}
</style>
```

</div></details>

# 動かしてみた
## 起動時
グレーの四角形の部分にスマポンを置きます。
ボタン0～2を押すと、対応している顔データを読み込みます。
![IMG_0963.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/352f5581-b30d-08a8-1f97-bafaf30f1448.png)
## カラーバーぽいやつ（ボタン0）
イイ感じじゃないでしょうか？
![IMG_0108.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/ef1ae918-fc7c-0d19-965e-ff44f9aa5b14.jpeg)
### 画面上の表示
![IMG_0965.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/cb02347c-4ca9-2d06-02ef-2a6e8ce1461a.png)
## 顔ぽいやつ（ボタン1）
なんとか顔に見えるカナ
![IMG_0109.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/191b1884-66dd-a589-9fa6-bf7a50b383c8.jpeg)
### 画面上の表示
![IMG_0966.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/05067b2b-1f49-bd2b-729c-38743635c2b6.png)
## ドラゴンぽいやつ（ボタン2）
わかる人にはわかるハズ
![IMG_0111.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/a5cf4840-6b90-16f9-5ee0-1d6c1c69f6cd.jpeg)
### 画面上の表示
![IMG_0967.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/b3668acb-b395-4964-33af-02f6437b646a.png)


# 感想

今回は顔の表示だけ掘り下げましたが、スマポンの真の売りは会話パターンの多さだと思います。
ちなみに嫁は3日間くらい~~起動して放置~~遊んでいました。