20200416のvue.jsに関する記事は2件です。

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

ハジメに

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

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日間くらい起動して放置遊んでいました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js + TypeScript + Webpack環境にESLintとPrettierを入れる

はじめに

どうも@chan_kakuです
今回はタイトルにもあるようにVue.js+TypeScript+Webpack環境(vue-cliを使わない)でESLint+Prettierを入れる時にハマった部分が多かったのでそのやり方などを説明していこうと思います。

ESLintとは

ESLint は JavaScript のための静的検証ツールです。コードを実行する前に明らかなバグを見つけたり、括弧やスペースの使い方などのスタイルを統一したりするのに役立ちます

特徴*

  • すべての検証ルールを自由に on/off できる
  • 自分のプロジェクトに合わせたカスタムルールを簡単に作れる
  • 豊富なビルトイン ルール (5.0.0 時点で 260 個) に加えて、たくさんのプラグインが公開されている
  • ECMAScript 2015 (ES6), 2016, 2017, 2018, 2019 を標準サポートしている
  • JSX 記法 をサポートしている
  • Babel と連携することで、仕様策定中の新しい構文や Flow 型注釈にも対応できる

*ESLint 最初の一歩から引用

なぜLintを使うのか

チーム開発ではそれぞれ独自の書き方でかけてしまう。特にJavaScriptだと、ESの種類によってたくさんの書き方があり、babelなどを使うとどの書き方でもかけてしまいます。
本来はチーム内で整合し、規約を作るのが良いがなかなかそれだけではうまくいかないのが現状である。
そこでLintを使うことで強制的にコードを合わせることでプロダクションコードを綺麗に保つことができます。

Prettierとは

コードフォーマッターで以下のものに対応している

  • JavaScript
  • JSX
  • Flow
  • TypeScript
  • Vue
  • Angular
  • CSS等
  • JSON
  • GraphQL Schmas
  • Markdown

など

なぜESLintとPrettier

ESLintは名前の通りLintとしての役割が大きく、最近TSLintがDeplicateになりその機能がESLintにマージされた。
またeslint --fixを使うことでコードを整形することができる。であればPrettierはいらないのではないかもしれないが、
Prettierはデフォルトでいい感じに整形してくれるところがとても良い(ESLintで同じような整形をしようとすると結構大変らしい)
そこで、Lintとしての機能はESLintにお任せして、コードの整形部分をPrettierに任せればいい感じにできる。(最近のフロント界隈はこの組み合わせでやることが多い)
また、ESLintの整形では1行が長い時に改行するかどうかのようなパターンのものは通常のeslint --fixで整形できなかったりする

問題点

ESLintやPrettierを別々で使うとそこまで問題は出ないのですが、今回の使い方のようにESLintをLintとして、Prettierでコードフォーマッターとして使うときにいろいろ問題があります。
ESLintはそもそもの機能として上記にもあるように--fixオプションを使うことによってコードフォマットをしてくれます。これがPrettierの機能とコンフリクトを起こしエラーが出ることが多々あります。
これを避けるためにESLint側のルールなどをoffにしたりするプラグインなどをいろいろ入れないといけないのですが、Prettier,Vue.js,TypeScriptの関連したプラグインが多すぎて何を使えば良いのかわからないと行った問題に直面しました。

導入方法

ESLint,Prettier関連のライブラリなどはたくさんありますが、ひとまずはコンフリクトを起こさず、Lintとコードフォマットができる最小限構成を目指しました。
こちらはVue.js + TypeScript環境にESLint + Prettierを入れたい場合です。TSではなく、JSなどでは結構入れるモジュールが変わってくるので注意してください
yarnを使っている方はnpm iyarn addに置き換えてください
npm i -D eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser

ここでparserやplugin、ruleなどを追加していきます

.eslintrc
{
    "env": {
        "es6": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:prettier/recommended",
        "plugin:vue/essential",
        "plugin:@typescript-eslint/eslint-recommended",
        "plugin:@typescript-eslint/recommended",
        "prettier/@typescript-eslint"
    ],
    "plugins": [
        "vue",
        "@typescript-eslint"
    ],
    "parser": "vue-eslint-parser",
    "parserOptions": {
        "parser": "@typescript-eslint/parser"
    },
    "rules": {
        // ここはよしなにルールを設定してください
        "prettier/prettier": [
            "error",
            {
                "singleQuote": true,
                "trailingComma": "es5"
            }
        ]
    }
}

ここに追加したものはlintから除外されます

.eslintignore
    node_modules/
package.json
    "scripts": {
        ・・・
        "lint": "eslint --fix 'src/**'" //src以下のファイルを読み込んでlint+コードフォーマットしてくれます
    }

npm run lintすることでlint+コードフォーマットしてくれます

追加したライブラリの説明

ESLintとPrettier以外を説明していきます

eslint-config-prettier

Prettierとのコンフリクトを起こすような不要なESLintのルールを解除するもの

eslint-plugin-prettier

ESLintのルールでPrettierを実行してくれる

eslint-plugin-vue

Vue.js用のESLintのプラグインである。Vue.jsのコミュニティによってメンテナンスされている。
.vue ファイルの <template><script>をチェックしてくれる。

使い方はこちら

@typescript-eslint/eslint-plugin

ESLintでTypeScriptのサポートをしてくれるプラグインである。

@typescript-eslint/parser

上記の@typescript-eslint/eslint-pluginと一緒につかう。ESLint用のパーサー

最後に

Lintとコードフォーマットしたいだけなのに結構複雑で、多くのライブラリを入れないといけなく、またその環境によって入れなければならないものが結構変わってくるのでESLintとPrettierの組み合わせは結構難しいように感じました。
この記事を書いた動機としてはTSLintがDeplicatedになってからVue.js + TypeScriptでかつ、vue-cliを使っていないという環境でのESLint+Prettierをいい感じに使う情報がまとまっていなかったため(自分調べ)書くに至りました。個人的な観測ではvue-cliを使ったサンプルが多く普通にwebpackを使うようなproduction環境を想定したものがなかったように感じたので、この記事によって同じ境遇の方の助けになればと思っております。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む