さぁ!検索しよう!

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

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.を図に表すと以下のようになります。

瞳をカーソルに応じて動かす仕組み

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

参考文献