- 投稿日:2020-04-16T10:43:06+09:00
「スマポン」という、コミュニケーショントイの顔の表示のハナシ
ハジメに
「スマポン」という、コミュニケーショントイ
が投げ売りされていたのを買った。スマホに専用アプリを入れて、スマホに乗せることでコミニュケーションが取れるらしい
スマポン|商品情報|タカラトミーこのスマポン本体にコンピューター的なモノは搭載されておらず、
スマホの専用アプリで顔の表示制御や会話などを行っている。
じゃ仕組みさえわかってしまえば、好きな顔の表示が出来るんじゃね?
というおハナシ顔の表示の仕組み
画像データの並び替えについて
スマホに表示された画像データは、スマポン上では別の場所に表示される。
実装
なんとなく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); }底辺を取得する。
高さの中央を取得する
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}; }底辺の中央を取得する
底辺の中央~頂点の中央を取得する
角度を取得する
スマホに対してスマポンがどの角度で配置されているか取得する必要があるため
底辺の中央~頂点の中央、頂点の間の角度を取得する下記のような状態の場合、270°(-90°)として取得する。
2点間の角度を求める場合、Math.atan2関数で取得できるらしいので使用する。
あと、この関数では左上を基準に角度を取得するため
下図のような状態の場合0°となり
なので結果に+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; },画像を表示する
任意の位置に回転させた四角形を書く場合
基準点を移動する
回転は基準点を中心に行われるため、まず基準点を移動する。
基準点を移動// x : x座標, y : y座標 this.ctx.translate(x, y);回転する
任意の角度分、回転する。
回転する// angle : 角度 this.ctx.rotate((angle * Math.PI) / 180);四角形を描く
四角形を描く際左上が基準となるため、四角形のサイズの半分だけ左上にずらして作画する。
四角形を描く// 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を押すと、対応している顔データを読み込みます。
カラーバーぽいやつ(ボタン0)
画面上の表示
顔ぽいやつ(ボタン1)
画面上の表示
ドラゴンぽいやつ(ボタン2)
画面上の表示
感想
今回は顔の表示だけ掘り下げましたが、スマポンの真の売りは会話パターンの多さだと思います。
ちなみに嫁は3日間くらい起動して放置遊んでいました。
- 投稿日:2020-04-16T10:25:17+09:00
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 iをyarn 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から除外されます
.eslintignorenode_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環境を想定したものがなかったように感じたので、この記事によって同じ境遇の方の助けになればと思っております。参考


























