LoginSignup
0
0

canvasを使って操作できるアニメーションを作ろう

Last updated at Posted at 2024-04-14

今回のお題

canvasを使って操作できるアニメーションを実装する。
段階的に下記の方法を記載していく。

1.HTMLの準備
2.CSSの準備
3.オブジェクトの表示
4.オブジェクトの移動
5.複数オブジェクトの移動
6.イベントリスナーの実装
7.ゲームの作成

1.HTMLの準備

まず、シンプルにcanvasの要素があるだけのhtmlを用意する。

canvas_test.html
<!DOCTYPE html>
<html>
<head>
  <title>canvas動作テスト</title>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="./canvas_test.css" type="text/css">
  <script src="./canvas_test.js"></script>
</head>
<body>
  <canvas id="cw"></canvas>
</body>
</html>

2.CSSの準備

cssで背景色を設定する。

canvas_test.css
canvas {
  position: fixed;
  z-index: -1;
}

body {
  margin: 0;
  padding: 0;
  background-color: rgba(0, 0, 0, 1);
}

3.オブジェクトの表示

canvas上に静止画を描画する。

canvas_test.js
// キャンバス情報
let canvas;
let ctx;

// 画面ロード時のイベント
window.onload = function() {
  // canvas要素を取得
  canvas = document.getElementById("cw");
  if(canvas && typeof(canvas.getContext) === 'function') {
    // コンテキストの初期化
    ctx = canvas.getContext("2d");
    // canvas要素のリサイズ
    setCanvasSize();
    // メイン処理を実行
    main();
  }
}

// canvasのサイズを画面に合わせる
function setCanvasSize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}

// メイン処理
function main() {
  
  // 四角・塗りつぶし
  ctx.fillStyle   = "#fcc";
  ctx.fillRect(50, 100, 150, 200);
  
  // 四角・枠線
  ctx.strokeStyle = "#c00";
  ctx.strokeRect(250, 100, 150, 50);
  
  // 線を引いてイメージを作成
  // 線引開始
  ctx.beginPath();
  
  // カーソル移動
  ctx.moveTo(230, 280);

  // ベジエ曲線
  ctx.bezierCurveTo(280, 330, 340, 350, 250, 230);
  
  // 円弧
  ctx.arcTo(300, 360, 380, 280, 210);

  // 直線
  ctx.lineTo(380, 280);
  
  // 線引終了
  ctx.closePath();
  
  // 描画
  ctx.fillStyle   = "#ddf";
  ctx.strokeStyle = "#00f";
  ctx.fill();
  ctx.stroke();

}

4.オブジェクトの移動

オブジェクトがランダムに移動している状態を描画する。
アニメーションをするには window.requestAnimationFrame(main) を使用して main メソッドを一定期間で呼びながら 描画状態のクリア⇒次の状態を描画 を連続で行う事で動いているように見せる。

canvas_test.js
// キャンバス情報
let canvas;
let ctx;

// オブジェクトの座標
let nextX;
let nextY;

let typeX = true;
let typeY = true;

let coordinateList = [];

// 画面ロード時のイベント
window.onload = function() {
  canvas = document.getElementById("cw");
  if(canvas && typeof(canvas.getContext) === 'function') {
    ctx = canvas.getContext("2d");
    initializations();
    main();
  }
}

// canvasのサイズを画面に合わせる
function initializations() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  nextX = canvas.width / 2;
  nextY = canvas.height / 2;
}

// ランダムな値を取得
function getRandomInt(max) {
  return Math.floor(Math.random() * max );
}

// メイン処理
function main() {
  
  // メイン処理の再呼び出し登録
  window.requestAnimationFrame(main);
  
  // 座標移動計算
  movetoX = typeX ? getRandomInt(5) : -1 * getRandomInt(5);
  movetoY = typeY ? getRandomInt(5) : -1 * getRandomInt(5);
  
  nextX += movetoX;
  nextY += movetoY;
  
  // 画面外に行かないようにする
  if ( nextX < 100 || canvas.width - 100 < nextX ) {
      nextX -= movetoX * 2;
      typeX = !typeX;
  }
  if ( nextY < 100 || canvas.height - 100 < nextY ) {
      nextY -= movetoY * 2;
      typeY = !typeY;
  }
  
  // 画面のクリア
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 軌跡の描画
  coordinateList.forEach(function(element, index) {
    ctx.beginPath();
    ctx.arc( element["x"], element["y"], 50 - (0.5 * index), 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fillStyle   = "rgba(255,255,255," + (0.5 - (0.005 * index)) + ")";
    ctx.fill();
  });
  
  // 軌跡座標のリストの前側に保存
  coordinateList.unshift({x:nextX, y:nextY});
  
  // 軌跡座標が100個を超えた場合は後ろから削除
  if (coordinateList.length > 100) {
    coordinateList.pop()
  }
  
  // 今回描画する先頭の丸
  ctx.beginPath();
  ctx.arc(nextX, nextY, 50, 0, Math.PI * 2, true);
  ctx.closePath();
  ctx.fillStyle   = "rgba(255,255,255,1.0)";
  ctx.fill();
}

5.複数オブジェクトの移動

複数オブジェクトを管理する場合は各オブジェクトの状態をクラスで設定して持つようにすると管理しやすい。

canvas_test.js
// キャンバス情報
let canvas;
let ctx;

// 端判定をするサイズ
const EDGE_SIZE = 100;
// 物質数
const MATERIAL_NUM = 10;
// 軌跡数MAX
const COORDINATE_NUM = 90;
// 全物質の配列
let materialArray = [];

// 画面ロード時のイベント
window.onload = function() {
  canvas = document.getElementById("cw");
  if(canvas && typeof(canvas.getContext) === 'function') {
    ctx = canvas.getContext("2d");
    initializations();
    main();
  }
}

// canvasのサイズを画面に合わせる
function initializations() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  
  for (let i = 0; i < MATERIAL_NUM; i++) {
    materialArray.push(new Material());
    materialArray[i].init();
  }
}

// ランダムな値を取得
function getRandomInt(max) {
  return Math.floor(Math.random() * max);
}

// 物質クラス
function Material() {
  this.colorR;
  this.colorG;
  this.colorB;
  this.x;
  this.y;
  this.moveX;
  this.moveY;
  this.coordinateList;
  
  // 初期化
  this.init = () => {
    this.colorR = getRandomInt(256);
    this.colorG = getRandomInt(256);
    this.colorB = getRandomInt(256);
    this.x = getRandomInt(canvas.width - EDGE_SIZE * 2) + EDGE_SIZE;
    this.y = getRandomInt(canvas.height - EDGE_SIZE * 2) + EDGE_SIZE;
    this.moveX = getRandomInt(20) - 10;
    this.moveY = getRandomInt(20) - 10;
    this.coordinateList = [];
  };
  
  // 移動
  this.move = () => {
    
    // 座標移動計算
    this.x += this.moveX;
    this.y += this.moveY;
    
    // 画面外に行かないようにする
    if ( this.x < EDGE_SIZE || canvas.width - EDGE_SIZE < this.x ) {
        this.moveX = -1 * this.moveX;
        this.x += this.moveX * 2;
    }
    if ( this.y < EDGE_SIZE || canvas.height - EDGE_SIZE < this.y ) {
        this.moveY = -1 * this.moveY;
        this.y += this.moveY * 2;
    }
    
    // 軌跡座標のリストの前側に保存
    this.coordinateList.unshift({x:this.x, y:this.y});
    
    // 軌跡座標が最大値を超えた場合は後ろから削除
    if (this.coordinateList.length > COORDINATE_NUM) {
      this.coordinateList.pop()
    }
  };
  
  // 描画
  this.draw = () => {
    for (let i = 0; i < this.coordinateList.length; i++) {
      ctx.beginPath();
      ctx.arc( this.coordinateList[i]["x"], this.coordinateList[i]["y"], 50 - (0.5 * i), 0, Math.PI * 2, true);
      ctx.closePath();
      ctx.fillStyle = "rgba(" + this.colorR + "," + this.colorG + "," + this.colorB + "," + (0.5 - (0.005 * i)) + ")";
      ctx.fill();
    }
  };
}

// メイン処理
function main() {
  // メイン処理の再呼び出し登録
  window.requestAnimationFrame(main);
  
  // 画面のクリア
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 各データを移動、描画
  materialArray.forEach(function(element, index) {
    element.move();
    element.draw();
  });
}

6.イベントリスナーの実装

イベントリスナーを実装すると画面を操作しているユーザーの行動を画面に反映させることができる。

canvas_test.js
// キャンバス情報
let canvas;
let ctx;

// マウスダウンフラグ
let mousedownFlag = false;
// キーダウンフラグ
let keydownFlag = false;

// マウスポインタの位置
const cursor = {
  x: window.innerWidth / 2,
  y: window.innerHeight / 2,
};

// 端判定をするサイズ
const EDGE_SIZE = 100;
// 物質数
const MATERIAL_NUM = 10;
// 軌跡数MAX
const COORDINATE_NUM = 90;
// 全物質の配列
let materialArray = [];

// 画面ロード時のイベント
window.onload = function() {
  canvas = document.getElementById("cw");

    // マウスダウン
  canvas.onmousedown = function(event) {
    mousedownFlag = true;
  };

  // マウスアップ
  canvas.onmouseup = function(event) {
    mousedownFlag = false;
    // 全オブジェクトの当たり判定状態を初期化する
    materialArray.forEach(function(element, index) {
      element.checkOut();
    });
  };

  // マウスムーブ
  canvas.onmousemove = function(event){
    cursor.x = event.offsetX;
    cursor.y = event.offsetY;
  };

  if(canvas && typeof(canvas.getContext) === 'function') {
    ctx = canvas.getContext("2d");
    initializations();
    main();
  }
}

// ウィンドウリサイズイベント
window.onresize = function(event) {
  initCanvasSize();
};

// キーダウン
window.onkeydown = function(event) {
  // エンターキーのイベントコード「Enter」の場合
  if (event.code === "Enter") {
    // キーダウンイベントは押している間連続で実行されるので最初の1回だけ実行する
    if (!keydownFlag) {
      // オブジェクトを掴んでいる状態でエンターした場合、オブジェクトの色を変更する
      materialArray.forEach(function(element, index) {
        if(element.check()) {
          element.convertColor();
        }
      });
    }
  }
  keydownFlag = true;
};

// キーアップ
window.onkeyup = function(event) {
  keydownFlag = false;
};

// 画面サイズ初期化
function initCanvasSize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}

// 全体の初期化
function initializations() {
  initCanvasSize();
  for (let i = 0; i < MATERIAL_NUM; i++) {
    materialArray.push(new Material());
    materialArray[i].init();
  }
}

// ランダムな値を取得
function getRandomInt(max) {
  return Math.floor(Math.random() * max);
}

// 物質クラス
function Material() {
  this.colorR;
  this.colorG;
  this.colorB;
  this.x;
  this.y;
  this.radius;
  this.moveX;
  this.moveY;
  this.coordinateList;
  this.moveFlag;
  
  // 初期化
  this.init = () => {
    this.colorR = getRandomInt(256);
    this.colorG = getRandomInt(256);
    this.colorB = getRandomInt(256);
    this.x = getRandomInt(canvas.width - EDGE_SIZE * 2) + EDGE_SIZE;
    this.y = getRandomInt(canvas.height - EDGE_SIZE * 2) + EDGE_SIZE;
    this.radius = 50
    this.moveX = getRandomInt(10) - 5;
    this.moveY = getRandomInt(10) - 5;
    this.coordinateList = [];
    this.moveFlag = true;
    this.colorChangeFlag = false;
  };
  
  // 当たり判定
  this.check = () => {
    if (mousedownFlag && this.checkMouseHit(cursor.x, cursor.y)) {
      this.moveFlag = false;
      return true;
    }
    return false;
  };

  // マウスアップ時のフラグ初期化
  this.checkOut = () => {
    this.moveFlag = true;
  }

  // 色変更
  this.convertColor = () => {
    this.colorR = getRandomInt(256);
    this.colorG = getRandomInt(256);
    this.colorB = getRandomInt(256);
  };
  
  // 当たり判定
  this.checkMouseHit = (checkX, checkY) => {
    var a = this.x - checkX;
    var b = this.y - checkY;
    var c = Math.sqrt(a * a + b * b);
    var result = c <= this.radius;
    
    return result;
  }
  
  // 移動
  this.move = () => {
    if (!this.moveFlag) {
      // カーソルに合うように座標を設定
      this.x = cursor.x;
      this.y = cursor.y;
    } else {
    
      // 座標移動計算
      this.x += this.moveX;
      this.y += this.moveY;
      
      // 画面外に行かないようにする
      if ( this.x < EDGE_SIZE || canvas.width - EDGE_SIZE < this.x ) {
          this.moveX = -1 * this.moveX;
          this.x += this.moveX * 2;
      }
      if ( this.y < EDGE_SIZE || canvas.height - EDGE_SIZE < this.y ) {
          this.moveY = -1 * this.moveY;
          this.y += this.moveY * 2;
      }
    }
    
    // 軌跡座標のリストの前側に保存
    this.coordinateList.unshift({x:this.x, y:this.y});
    
    // 軌跡座標が最大値を超えた場合は後ろから削除
    if (this.coordinateList.length > COORDINATE_NUM) {
      this.coordinateList.pop()
    }
  };
  
  // 描画
  this.draw = () => {
    
    for (let i = 0; i < this.coordinateList.length; i++) {
      ctx.beginPath();
      ctx.arc( this.coordinateList[i]["x"], this.coordinateList[i]["y"], this.radius - (0.5 * i), 0, Math.PI * 2, true);
      ctx.closePath();
      ctx.fillStyle = "rgba(" + this.colorR + "," + this.colorG + "," + this.colorB + "," + (0.5 - (0.005 * i)) + ")";
      ctx.fill();
    }
  };
}

// メイン処理
function main() {
  // メイン処理の再呼び出し登録
  window.requestAnimationFrame(main);
  
  // 画面のクリア
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 各データを移動、描画
  materialArray.forEach(function(element, index) {
    element.check();
    element.move();
    element.draw();
  });
}

7.ゲームの作成

上記の技術を用いてゲームを作成する。
作成する上でのゲームの仕様は下記の通り

1.スタート画面・ゲームプレイ画面・クリア画面の3種の表示状態が存在する事。
2.スタート画面・クリア画面はその画面を画面をクリックしたら次の表示状態となる事。
3.ゲームプレイ画面ではなんらかのゲームを行い、何かを達成した時にクリア画面が表示される事。

0
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0