マウスポインターを目で追う白猫の作り方です。
DEMO
マウスを動かすと白猫が目でポインターを追っていることが確認できます。
HTMLを書く
始めに以下のHTMLを書きます。
HTML
<div class="wrap flex"> <div class="cat"> <div class="face"> <div class="ears"> <div class="ear ear-left flex"></div> <div class="ear ear-right flex"></div> </div> <div class="eyes"> <div class="eye eye-left flex"> <div class="eye-green eye-green-left flex"> <div class="eye-black eye-black-left flex"></div> </div> </div> <div class="eye eye-right flex"> <div class="eye-green eye-green-right flex"> <div class="eye-black eye-black-right flex"></div> </div> </div> </div> <div class="nose"></div> <div class="whisker-pads"> <div class="whisker-pad whisker-pad-left"> <div class="pores pores-left flex"> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> </div> <div class="whiskers whiskers-left flex"> <div class="whisker whisker-1"></div> <div class="whisker whisker-2"></div> <div class="whisker whisker-3"></div> </div> </div> <div class="whisker-pad whisker-pad-right"> <div class="pores pores-right flex"> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> <div class="pore flex"></div> </div> <div class="whiskers whiskers-right flex"> <div class="whisker whisker-1"></div> <div class="whisker whisker-2"></div> <div class="whisker whisker-3"></div> </div> </div> </div> <div class="mouth"></div> </div> <p class="message">mousemove!</p> </div> </div>
JavaScriptで制御する要素は、.eye-green-left
と.eye-green-right
です。
CSSを書く
次に以下のCSSを書きます。
CSS
@charset "UTF-8"; @import url("https://fonts.googleapis.com/css?family=Baloo+Bhaina"); * { box-sizing: border-box; } .flex { display: -webkit-box; display: -ms-flexbox; display: flex; } html, body, .wrap { /* .catをウィンドウ中央に配置するため */ height: 100%; } body { background: #27313D; } .wrap { /* .catをウィンドウ中央に配置 */ -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; } .cat { height: 150px; width: 200px; } .face { background: #ededed; border-radius: 100%; height: inherit; position: relative; } .ears { position: absolute; top: 0; left: 0; width: 100%; z-index: -1; } .ear { background: #ededed; border-radius: 30px; /* .ear::beforeを.earの中央に配置 */ -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; height: 80px; position: absolute; top: -6px; width: 80px; } .ear::before { background: #F58E7E; border-radius: 23px; content: ''; height: 73px; width: 73px; } .ear-left { left: 14px; /* rotateで回転、skewX,Yで傾斜変形させて猫耳の形に近づける */ -webkit-transform: rotate(25deg) skewX(30deg) skewY(8deg); transform: rotate(25deg) skewX(30deg) skewY(8deg); } .ear-right { right: 14px; /* .ear-leftの逆 */ -webkit-transform: rotate(-25deg) skewX(-30deg) skewY(-8deg); transform: rotate(-25deg) skewX(-30deg) skewY(-8deg); } .eyes { height: 40px; position: absolute; top: 40%; left: 0; -webkit-transform: translateY(-40%); transform: translateY(-40%); width: 100%; } .eye { background: #333333; border: 2px solid #333333; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; height: 35px; overflow: hidden; position: absolute; top: 0; width: 45px; /* 虹彩 */ /* 瞳 */ } .eye-green { background: #58BE89; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; height: 40px; width: 40px; } .eye-green, .eye-black, .eye-black::before { border-radius: 100%; } .eye-black { background: #333333; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; height: 35px; width: 35px; /* 瞳の光 */ } .eye-black::before { background: #fff; content: ''; height: 6px; width: 6px; } .eye-left { border-radius: 0 70% 0 70%; left: 30px; } .eye-right { border-radius: 70% 0 70% 0; right: 30px; } /* 鼻筋 */ .nose { /* border-top,left,rightで台形を作っている */ border-top: 20px solid #ededed; border-left: 2px solid transparent; border-right: 2px solid transparent; height: 20px; position: absolute; top: 56%; left: 50%; -webkit-transform: translate(-50%, 0%); transform: translate(-50%, 0%); width: 50px; z-index: 3; /* 鼻 */ } .nose::before { background: #F26752; border-radius: 40%; content: ''; height: 30px; position: absolute; top: -6px; left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%); width: 50px; z-index: 1; } .whisker-pads { position: absolute; left: 50%; bottom: 0; -webkit-transform: translateX(-50%); transform: translateX(-50%); z-index: 4; } .whisker-pads > .whisker-pad { border-radius: 100%; } .whisker-pads > .whisker-pad-left, .whisker-pads > .whisker-pad-right { background: whitesmoke; border-bottom: 1px solid #b3b3b3; height: 44px; position: absolute; bottom: 2px; width: 60px; } .whisker-pads > .whisker-pad-left { border-right: 2px solid #b3b3b3; left: 50%; -webkit-transform: translateX(-98%); transform: translateX(-98%); } .whisker-pads > .whisker-pad-right { border-left: 2px solid #b3b3b3; right: 50%; -webkit-transform: translateX(98%); transform: translateX(98%); } .pores { -ms-flex-wrap: wrap; flex-wrap: wrap; height: 30px; position: absolute; top: 50%; width: 50px; } .pores-left { left: 50%; -webkit-transform: skewX(5deg) translate(-50%, -50%); transform: skewX(5deg) translate(-50%, -50%); } .pores-right { right: 50%; -webkit-transform: skewX(-5deg) translate(50%, -50%); transform: skewX(-5deg) translate(50%, -50%); } .pores > .pore { -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; height: 33.33333%; width: 33.33333%; } .pores > .pore::before { background: #b3b3b3; border-radius: 100%; content: ''; display: block; height: 3px; width: 3px; } .whiskers { -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-line-pack: distribute; align-content: space-around; height: 100%; position: absolute; top: 0; width: 100%; } .whiskers-left { -webkit-transform: translateX(-50%); transform: translateX(-50%); } .whiskers-left .whisker-2 { -webkit-transform: skewY(-10deg); transform: skewY(-10deg); } .whiskers-left .whisker-3 { -webkit-transform: skewY(-20deg); transform: skewY(-20deg); } .whiskers-right { -webkit-transform: translateX(50%); transform: translateX(50%); } .whiskers-right .whisker-2 { -webkit-transform: skewY(10deg); transform: skewY(10deg); } .whiskers-right .whisker-3 { -webkit-transform: skewY(20deg); transform: skewY(20deg); } .whiskers > .whisker { -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; border-top: 1px solid #b3b3b3; border-top-left-radius: 50%; border-top-right-radius: 50%; height: 10px; width: 100%; } .mouth { background: #ededed; border-radius: 100%; height: 55px; position: absolute; bottom: -9px; left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%); width: 90px; } .message { color: #fff; font-size: 28px; font-family: 'Baloo Bhaina', cursive; margin-top: 30px; text-align: center; }
白猫は、CSSのみで描かれています。
JavaScript(TypeScript)を書く
最後に以下の順でJavaScriptを書きます。
Eyeクラスを作成・オブジェクト生成
白猫の目に関するEye
クラスを作成します。また、このクラスから左目eyeLeft
と右目eyeRight
の2つのオブジェクトを生成します。生成したオブジェクトは後にまとめて処理を行うため、eyes
配列に格納しておきます。
JavaScript
//白猫の目に関するクラス class Eye { private _pupil: HTMLElement; constructor(sel) { //瞳 this._pupil = document.querySelector(sel); } get pupil(): HTMLElement { return this._pupil; } } //eyeLeftオブジェクトとeyeRightオブジェクトを生成 const eyeLeft: Eye = new Eye('.eye-pupil-left'); const eyeRight: Eye = new Eye('.eye-pupil-right'); //配列に格納 const eyes: Eye[] = [eyeLeft, eyeRight];
このクラスでは.pupil
の参照のみを取得しています。
Pointerクラスを作成・インスタンス化
ポインターに関するPointerクラスを作成・インスタンス化します。
JavaScript
//ポインターに関するクラス class Pointer { constructor() { //mousemoveされたらmoveメソッドを実行 window.addEventListener('mousemove', (e) => this.move(e)); } //マウスが動いたら実行される。瞳を動かす関数 move(e): void { //ポインターの座標 const mouseX: number = e.clientX; const mouseY: number = e.clientY; //ウィンドウサイズ width = window.innerWidth; height = window.innerHeight; //ウィンドウ横中央(width/2)の座標からみたポインターのx相対座標(mouseX) //ウィンドウ横中央(width/2)の座標からポインターのx相対座標(mouseX)までの距離 const distanceX: number = mouseX - width / 2;//底辺 //ウィンドウ縦中央(height/2)の座標からみたポインターのy相対座標(mouseY) //ウィンドウ縦中央(height/2)の座標からポインターのy座標(mouseY)までの距離 const distanceY: number = mouseY - height / 2;//高さ //ウィンドウ中央からポインターまでの距離。長さ。ベクトル(量・方向) //ピタゴラスの定理によって斜辺の長さdistanceを算出 const distance: number = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2)); //底辺distanceX、高さdistanceYのときのラジアン(度数法でいう角度) //ラジアンとは国際単位系 (SI) における角度(平面角)の単位。半径の長さと弧の長さの比(弧 / 半径)で得ることができる。半径と弧の長さが同じであるラジアンは1ラジアン //atan2メソッドは高さと底辺を引数とすることでラジアン(アークタンジェント)を得ることができる。 //引数としてdistanceXに対するdistanceYの割合(tan)を渡す。 const rad: number = Math.atan2(distanceY, distanceX); //translateX,Yに値を入れる必要があるため、sinとcosに分ける //Math.cos: cosrad°ということ。傾きを1としたときのそれに対する横方向cos(底辺・半径)の比(割合)を求めることができる。 //distanceを掛けることで傾きをdistanceとしたときの底辺の比(正確な底辺の長さ)がわかる。 //distanceを掛けるということ→distance * cos / distance * 1 const x: number = distance * Math.cos(rad); const y: number = distance * Math.sin(rad); //ポインターの動きに応じて、瞳を決められた範囲内で動かす for(let i = 0; i < eyes.length; i++) { eyes[i].pupil.style.transform = 'translateX(' + Pointer.clamp(x / 28, -10, 10) + 'px)' + 'translateY(' + Pointer.clamp(y / 28, -10, 10) + 'px)'; } console.log(Pointer.clamp(x / 28, -10, 10)); } //numberをminからmaxまでの値で返す static clamp(number, min, max): number { return Math.max(min, Math.min(number, max)); } } //pointerオブジェクト生成 const pointer: Pointer = new Pointer;
このクラスでは以下のことを行っています。
瞳をポインターの動きに応じて動かす
addEventListener
メソッドでポインターを動かす(mousemove)とmove
メソッドが実行されて瞳.pupil
がポインターの動きに応じて動くようにします。
JavaScript
//mousemoveされたらmoveメソッドを実行 window.addEventListener('mousemove', (e) => this.move(e));
moveメソッドでは以下のことを行っています。
1. ウィンドウ中央のx,y座標からみたポインターの相対的なx,y座標を取得
ウィンドウ中央のx(y)座標からみたポインターの相対的なx(y)座標distanceX
(distanceY
)は、ポインターの現在x(y)座標mouseX
(mouseY
)とウィンドウ中央のx(y)座標width / 2
(height / 2
)の差となります。
JavaScript
//ウィンドウ横中央(width/2)の座標からみたポインターのx相対座標 const distanceX: number = mouseX - width / 2;//底辺 //ウィンドウ縦中央(height/2)の座標からみたポインターのy相対座標 const distanceY: number = mouseY - height / 2;//高さ
2. ウィンドウ中央の座標からポインターの座標までのベクトルを取得
ウィンドウ中央の座標からポインターの座標までのベクトル、つまりdistanceX
とdistanceY
のベクトルdistance
を、ピタゴラスの定理を利用して取得します。
JavaScript
//ウィンドウ中央からポインターまでの距離。長さ。ベクトル(量・方向) const distance: number = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));
3. 2.で取得したベクトルの傾きからラジアンを取得
distanceX
に対するdistanceY
の割合(tan)、つまり2.で取得したベクトルの傾きからラジアンrad
を取得します。
ラジアンは国際単位系 (SI) における角度の単位であり、半径の長さと弧の長さの比(弧 / 半径)で得ることができます。半径と弧の長さが同じであるラジアンは1ラジアンとなります。
このラジアンはatan2
メソッドを使えば、高さと底辺を引数に指定することで得られます。
JavaScript
const rad: number = Math.atan2(distanceY, distanceX);
4. 半径distanceであるときのcos,sinを取得
瞳の動きはtransform
プロパティのtranslateX
とtranslateY
で行うため、x方向とy方向の値を2つ用意する必要があります。なので、斜辺distance
に対するcos(底辺)x
とsin(高さ)y
を取得ます。
JavaScript
//translateX,Yに値を入れる必要があるため、sinとcosに分ける //Math.cos = cosrad°ということ。ラジアンradを入れることで傾きを1としたときのそれに対する横方向(底辺・半径)の比cos(割合)を求めることができる。 //distanceを掛けることで傾きをdistanceとしたときの底辺の比(正確な底辺の長さ)がわかる。 //distanceを掛けるということ→distance * cos / distance * 1 const x: number = distance * Math.cos(rad); const y: number = distance * Math.sin(rad);
1.~4.を図に表すと以下のようになります。
5. 瞳の移動範囲を決める
translateX
(translateY
)にただx
(y
)と指定しただけでは、マウスを動かすと瞳が目の枠からはみ出てしまいます。なので、clamp
メソッドで移動範囲を決めます。clamp
メソッドは第一引数に制御する値、第二引数には最小値、第三引数には最大値を入れます。
JavaScript
//numberをminからmaxまでの値で返す static clamp(number, min, max): number { return Math.max(min, Math.min(number, max)); }
6. 瞳の移動スピードを調節
clamp
メソッドの第一引数をx
(y
)としただけでは、瞳の移動スピードが速すぎてカクカクした動きになってしまいます。
これを防ぐためにx
(y
)を適当な数値で割ることで、フレームが小刻みになり瞳が滑らかに動くようにします。
JavaScript
//ポインターの動きに応じて、瞳を決められた範囲内で動かす for(let i = 0; i < eyes.length; i++) { eyes[i].pupil.style.transform = 'translateX(' + Pointer.clamp(x / 28, -10, 10) + 'px)' + 'translateY(' + Pointer.clamp(y / 28, -10, 10) + 'px)'; }
完成したJavaScript
完成したJavaScriptは以下の通りです。
JavaScript
(() => { //ウィンドウサイズ let width: number; let height: number; //白猫の目に関するクラス class Eye { private _pupil: HTMLElement; constructor(sel) { //瞳 this._pupil = document.querySelector(sel); } get pupil(): HTMLElement { return this._pupil; } } //eyeLeftオブジェクトとeyeRightオブジェクトを生成 const eyeLeft: Eye = new Eye('.eye-pupil-left'); const eyeRight: Eye = new Eye('.eye-pupil-right'); //配列に格納 const eyes: Eye[] = [eyeLeft, eyeRight]; //ポインターに関するクラス class Pointer { constructor() { //mousemoveされたらmoveメソッドを実行 window.addEventListener('mousemove', (e) => this.move(e)); } //マウスが動いたら実行される。瞳を動かす関数 move(e): void { //ポインターの座標 const mouseX: number = e.clientX; const mouseY: number = e.clientY; //ウィンドウサイズ width = window.innerWidth; height = window.innerHeight; //ウィンドウ横中央(width/2)の座標からみたポインターのx相対座標(mouseX) //ウィンドウ横中央(width/2)の座標からポインターのx相対座標(mouseX)までの距離 const distanceX: number = mouseX - width / 2;//底辺 //ウィンドウ縦中央(height/2)の座標からみたポインターのy相対座標(mouseY) //ウィンドウ縦中央(height/2)の座標からポインターのy座標(mouseY)までの距離 const distanceY: number = mouseY - height / 2;//高さ //ウィンドウ中央からポインターまでの距離。長さ。ベクトル(量・方向) //ピタゴラスの定理によって斜辺の長さdistanceを算出 const distance: number = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2)); //底辺distanceX、高さdistanceYのときのラジアン(度数法でいう角度) //ラジアンとは国際単位系 (SI) における角度(平面角)の単位。半径の長さと弧の長さの比(弧 / 半径)で得ることができる。半径と弧の長さが同じであるラジアンは1ラジアン //atan2メソッドは高さと底辺を引数とすることでラジアン(アークタンジェント)を得ることができる。 //引数としてdistanceXに対するdistanceYの割合(tan)を渡す。 const rad: number = Math.atan2(distanceY, distanceX); //translateX,Yに値を入れる必要があるため、sinとcosに分ける //Math.cos: cosrad°ということ。傾きを1としたときのそれに対する横方向cos(底辺・半径)の比(割合)を求めることができる。 //distanceを掛けることで傾きをdistanceとしたときの底辺の比(正確な底辺の長さ)がわかる。 //distanceを掛けるということ→distance * cos / distance * 1 const x: number = distance * Math.cos(rad); const y: number = distance * Math.sin(rad); //ポインターの動きに応じて、瞳を決められた範囲内で動かす for(let i = 0; i < eyes.length; i++) { eyes[i].pupil.style.transform = 'translateX(' + Pointer.clamp(x / 28, -10, 10) + 'px)' + 'translateY(' + Pointer.clamp(y / 28, -10, 10) + 'px)'; } } //numberをminからmaxまでの値で返す static clamp(number, min, max): number { return Math.max(min, Math.min(number, max)); } } //pointerオブジェクト生成 const pointer: Pointer = new Pointer; })();
最後に
以上で、【JavaScript】マウスポインターを目で追う白猫の作り方を終わります。
参考文献
- 数学部屋
- 直角三角形の底辺と高さから傾斜角と斜辺を計算することはできますか。たとえ... - Yahoo!知恵袋
- サイン、コサイン、タンジェントについて分かりやすく説明してください... - Yahoo!知恵袋
- テーマ「三角関数講座」のブログ記事一覧 機織り職人募集中。 /ウェブリブログ
- ラジアン ■わかりやすい高校物理の部屋■
- arctan2(アークタンジェント2)ってなんぞ? | No More Retake
- 三角関数を使ったオブジェクトの移動 | jstarted.com
- [CSS] 台形をつくる(角を丸くしてみたり、中に文字を入れてみたり。) - Qiita
- 2点間の距離や角度の求め方 | ただの屍
- 算数ドリル ... 2点間の距離と角度 | MONSTER DIVE【モンスターダイブ】
- 角度と座標の計算 - Flash の三角関数を使う
- JavaScriptでマウスの位置座標を取得する方法
- 2点間の距離と角度と座標の求め方 - Qiita