1
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-16

ハジメに

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

image.png

スマホに専用アプリを入れて、スマホに乗せることでコミニュケーションが取れるらしい
スマポン|商品情報|タカラトミー

ちなみに、裏から見るとこんな感じ
image.png

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

顔の表示の仕組み

  1. スマホにスマポンを置くことで、3つ付いている突起により3点タッチされる。
    image.png

  2. タッチされた3点を結んで出来る二等辺三角形の底辺を、顔の底辺とする。
    image.png

  3. 二等辺三角形の高さの中央を、顔の中央とする。
    image.png

  4. 8×8の受光部分に対して、一定の法則で並び変えた画像データを表示する。
    image.png

  5. これをスマポンを通して見ると、本来の画像データで表示される。
    image.png

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

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

image.png

下図が変換パターン(数字で対応)です。
image.png

実装

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

3点の座標を取得する

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

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}$
そのまま実装

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

高さの中央を取得する

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

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

底辺の中央~頂点の中央を取得する

image.png

角度を取得する

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

下図のような状態の場合を0°とし
image.png

下記のような状態の場合、270°(-90°)として取得する。
image.png

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

あと、この関数では左上を基準に角度を取得するため
下図のような状態の場合0°となり
image.png

下図の場合、270°(-90°)として取得されてしまう。
image.png

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

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

基準点を移動する

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

image.png

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

回転する

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

image.png

回転する
// angle : 角度
this.ctx.rotate((angle  * Math.PI) / 180);

四角形を描く

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

image.png

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

実装してみたコード

割と長いので折り畳み
サンプルコード
<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>

動かしてみた

起動時

グレーの四角形の部分にスマポンを置きます。
ボタン0~2を押すと、対応している顔データを読み込みます。
IMG_0963.PNG

カラーバーぽいやつ(ボタン0)

イイ感じじゃないでしょうか?
IMG_0108.JPG

画面上の表示

IMG_0965.PNG

顔ぽいやつ(ボタン1)

なんとか顔に見えるカナ
IMG_0109.JPG

画面上の表示

IMG_0966.PNG

ドラゴンぽいやつ(ボタン2)

わかる人にはわかるハズ
IMG_0111.JPG

画面上の表示

IMG_0967.PNG

感想

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0