ハジメに
「スマポン」という、コミュニケーショントイが投げ売りされていたのを買った。
スマホに専用アプリを入れて、スマホに乗せることでコミニュケーションが取れるらしい
スマポン|商品情報|タカラトミー
このスマポン本体にコンピューター的なモノは搭載されておらず、
スマホの専用アプリで顔の表示制御や会話などを行っている。
じゃ仕組みさえわかってしまえば、好きな顔の表示が出来るんじゃね?
というおハナシ
顔の表示の仕組み
画像データの並び替えについて
スマホに表示された画像データは、スマポン上では別の場所に表示される。
実装
なんとなくVue.jsで実装してますが、
説明部分のコードは、JavaScriptが分かれば問題無いです。
3点の座標を取得する
touchstartイベントで取得する。
取得した際の座標は、全体座標なので
作画するcanvasを基準とした座標を取得する。
/**
* 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点の距離を取得
* @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点の中央を取得
* @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°する。
/**
* 角度を取得
* @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日間くらい起動して放置遊んでいました。