【JavaScript】オブジェクトをイージング(比例運動)させる

イージング(比例運動)とは、出発点から目標点に近づくにつれ、徐々に遅くなって止まるような動き。出発から到着まで同じ速度の等速運動と違い自然な動き見える。

下のスクリプトはCanvasの中心を目標点に設定し、マウスでCanvas内をクリックすると、猫がクリック地点に移動し、目標点に向かってイージングをかけた移動をする。

まず、猫画像を読み込み表示するCatクラス。

/**
 * 猫画像を表示(cat.js)
 */
export class Cat {
	constructor(parent = undefined, x = 0, y = 0) {
		if (parent !== undefined) {
			this._parent = parent;
		}
		this._x = x;
		this._y = y;
		this._rotation = 0;
		this._alpha = 1.0;
		this._img;
		
		this._init();
	}

	//////////////////////////////////
	// Private and protected
	//////////////////////////////////

	_init() {
		if (this._parent !== undefined) {
			this._ctx = this._parent;
		}
		this._img = new Image();
		this._img.src = "./cat.png";
		this.draw();
	}

	//////////////////////////////////
	// Public
	//////////////////////////////////

	draw() {
		// 現在の描画状態を保存
		this._ctx.save();
		// コンテキストの座標を変更し、キャンバス中央に移動
		this._ctx.translate(this._x, this._y);
		// コンテキストの角度をラジアン値で指定
		this._ctx.rotate(this._rotation);
		this._ctx.globalAlpha = this._alpha;
		this._ctx.drawImage(this._img, -(this._img.width / 2), -(this._img.height / 2));
		// save()で保存した描画状態を復元
		this._ctx.restore();
	}

	//////////////////////////////////
	// Getters/Setters
	//////////////////////////////////

	get x() {
		return this._x;
	}
	set x(x) {
		this._x = x;
	}

	get y() {
		return this._y;
	}
	set y(y) {
		this._y = y;
	}

	get width() {
		return this._img.width;
	}

	get height() {
		return this._img.height;
	}

	get rotation() {
		return this._rotation;
	}
	set rotation(rotation) {
		this._rotation = rotation;
	}

	get alpha() {
		return this._alpha;
	}
	set alpha(alpha) {
		this._alpha = alpha;
	}
}
スポンサーリンク

次に、画像をイージングをかけて移動させるEasingクラス。

import { Cat } from "./cat.js";

/**
 * オブジェクトをイージングさせる(easing.js)
 */
export class Easing {
	constructor() {
		this._cvs = document.getElementById('canvas');
		this._ctx = this._cvs.getContext('2d');
		this._cat;
		this._easing = 0.2;                     // ①イージングに使う値
		this._targetX = this._cvs.width / 2;	// ②画像を向かわせるX、Y座標
		this._targetY = this._cvs.height / 2;
		this._isMouseDown = 0;                  // クリック中か判断するフラグ変数
		this._cvs.addEventListener('mousedown', this._onMouseDown.bind(this));
		this._onMouseUp = this._onMouseUp.bind(this);
		this._onMouseMove = this._onMouseMove.bind(this);
		// タイマー関連
		this._animID;
		this._isAnim = 0;
		this._FPS = 60;
		this._frame = 0;
		this._startTime;
		this._nowTime;
		
		this._init();
	}

	//////////////////////////////////
	// Private and protected
	//////////////////////////////////

	_init() {
		this._cvs.style.backgroundColor = "#eeeeee";
		this._cat = new Cat(this._ctx);

		this._startTime = performance.now();
		this._mainLoop();
	}

	_mainLoop() {
		this._nowTime = performance.now();
		let elapsedTime = this._nowTime - this._startTime;
		let idealTime = this._frame * (1000 / this._FPS);
		if (idealTime < elapsedTime) {
			this._ctx.clearRect(0, 0, this._cvs.width, this._cvs.height);

			// ③イージング処理部分
			if (!this._isMouseDown) {
				let dx = this._targetX - this._cat.x;
				let dy = this._targetY - this._cat.y;
				// ④1ピクセル以内まで近づいたら到着と判断し停止させる
				if (Math.sqrt(dx * dx + dy * dy) < 1) {
                                    this._cat.x = this._targetX;
                                    this._cat.y = this._targetY;
                                } else {
                                    this._cat.x += dx * this._easing;
                                    this._cat.y += dy * this._easing;
                                }
                        }
                        this._cat.draw();

                        this._frame++;

                        if (elapsedTime >= 1000) {
				this._startTime = this._nowTime;
				this._frame = 0;
			}
		}
	
		this._animID = requestAnimationFrame(this._mainLoop.bind(this));
	}

	//////////////////////////////////
	// Handlers
	//////////////////////////////////

	_onMouseDown(e) {
		this._isMouseDown = 1 - this._isMouseDown;
		this._cvs.addEventListener('mouseup', this._onMouseUp);
		this._cvs.addEventListener('mousemove', this._onMouseMove);
	}

	_onMouseUp(e) {
		this._isMouseDown = 1 - this._isMouseDown;
		this._cvs.removeEventListener('mouseup', this._onMouseUp);
		this._cvs.removeEventListener('mousemove', this._onMouseMove);
	}

	_onMouseMove(e) {
                this._cat.x = e.clientX;
                this._cat.y = e.clientY;
	}
}

①イージングの値を設定。

②目標点を設定。今回はCanvasの中央。

③画像にイージング処理を施した速度で移動させている。処理の流れは以下。

  1. 画像から目標点までの距離を計算
  2. 距離にイージングの値をかけて速度を求める
  3. 速度の値を現在の位置に加算
  4. 画像が到着するまで1〜3を繰り返す

④では、画像と目標点の距離が分かった時点で、1ピクセル以内に近づいていたら、画像のX,Y座標に目標点のX、Y座標を代入して移動を停止。

今回の目標点はCanvas(400×400)の中央の200ピクセル。イージングの値は小数だから、かけ続けても決して200に達することがない。最も近づいても199.99999999999994まで。これだと移動処理が果てしなく続くため、停止処理を追加している。

Math.sqrt(dx * dx + dy * dy)は三平方の定理で距離を計算している。

イージングは移動だけではなく、透明度の値の変更にイージングをかけることでフェードイン・フェードアウトの効果をつけたり、回転で使用したりと応用の幅が広い。

別ページで表示(フェードイン・フェードアウト)

別ページで表示(回転)

requestAnimationFrameを使ったゲームループの実装は、「【JavaScript】requestAnimationFrameでゲームループを作る」を参照。

コメント

タイトルとURLをコピーしました