ツクログ

tsukulognet

tsukulognet

道産子。Reactでなまら面白いものを作りたい。

マウスポインターを目で追う白猫の作り方

eye catch

マウスポインターを目で追う白猫の作り方です。

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. ウィンドウ中央の座標からポインターの座標までのベクトルを取得

ウィンドウ中央の座標からポインターの座標までのベクトル、つまりdistanceXdistanceYのベクトル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プロパティのtranslateXtranslateYで行うため、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.を図に表すと以下のようになります。

nekomouse

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】マウスポインターを目で追う白猫の作り方を終わります。

参考文献