今回のお題
canvasを使って操作できるアニメーションを実装する。
段階的に下記の方法を記載していく。
1.HTMLの準備
2.CSSの準備
3.オブジェクトの表示
4.オブジェクトの移動
5.複数オブジェクトの移動
6.イベントリスナーの実装
7.ゲームの作成
1.HTMLの準備
まず、シンプルにcanvasの要素があるだけの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 {
position: fixed;
z-index: -1;
}
body {
margin: 0;
padding: 0;
background-color: rgba(0, 0, 0, 1);
}
3.オブジェクトの表示
canvas上に静止画を描画する。
// キャンバス情報
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 メソッドを一定期間で呼びながら 描画状態のクリア⇒次の状態を描画 を連続で行う事で動いているように見せる。
// キャンバス情報
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.複数オブジェクトの移動
複数オブジェクトを管理する場合は各オブジェクトの状態をクラスで設定して持つようにすると管理しやすい。
// キャンバス情報
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.イベントリスナーの実装
イベントリスナーを実装すると画面を操作しているユーザーの行動を画面に反映させることができる。
// キャンバス情報
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.ゲームプレイ画面ではなんらかのゲームを行い、何かを達成した時にクリア画面が表示される事。
スタート・ゲームプレイ・クリアの切り替えサンプル
// キャンバス情報
let canvas;
let ctx;
// 各フラグ
let mousedownFlag = false;
let startFlag = false;
let mainFlag = false;
let endFlag = false;
// 画面ロード時のイベント
window.onload = function() {
canvas = document.getElementById("cw");
// マウスダウン
canvas.onmousedown = function(event) {
// ログ出力
console.log("onmousedown");
mousedownFlag = true;
};
if(canvas && typeof(canvas.getContext) === 'function') {
ctx = canvas.getContext("2d");
initCanvasSize();
start();
}
}
// ウィンドウリサイズイベント
window.onresize = function(event) {
initCanvasSize();
};
// 画面サイズ初期化
function initCanvasSize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
// スタート処理
function start() {
// 再呼び出し登録
if (mainFlag) {
// 1秒後に呼ぶ
window.setTimeout(main, 1000);
return;
} else {
window.requestAnimationFrame(start);
}
// ログ出力
console.log("スタート");
if (mousedownFlag) {
mainFlag = true;
startFlag = false;
mousedownFlag = false;
}
}
// メイン処理
function main() {
// 再呼び出し登録
if (endFlag) {
window.requestAnimationFrame(end);
return;
} else {
window.requestAnimationFrame(main);
}
// ログ出力
console.log("メイン");
if (mousedownFlag) {
endFlag = true;
mainFlag = false;
mousedownFlag = false;
}
}
// エンド処理
function end() {
// 再呼び出し登録
if (startFlag) {
window.requestAnimationFrame(start);
return;
} else {
window.requestAnimationFrame(end);
}
// ログ出力
console.log("エンド");
if (mousedownFlag) {
endFlag = true;
mainFlag = false;
mousedownFlag = false;
}
}
例
// キャンバス情報
let canvas;
let ctx;
// 各フラグ
let mousedownFlag = false;
let startFlag = false;
let mainFlag = false;
let endFlag = false;
let completFlag = false;
// キーダウンフラグ
let keydownFlag = false;
let eventCode = "";
let direction = "ArrowUp";
let MYSELEF_SIZE = 10;
let ENEMY_SIZE = 10;
let ENEMY_MATERIAL_NUM = 2;
let BULLET_SIZE = 5;
// 自分
let materialMyself;
// 敵物質の配列
let eneemyMaterialArray = [];
// 弾物質の配列
let bulletMaterialArray = [];
// 画面ロード時のイベント
window.onload = function() {
canvas = document.getElementById("cw");
// マウスダウン
canvas.onmousedown = function(event) {
// ログ出力
console.log("onmousedown");
mousedownFlag = true;
};
if(canvas && typeof(canvas.getContext) === 'function') {
ctx = canvas.getContext("2d");
initCanvasSize();
start();
}
}
// キーダウン
window.onkeydown = function(event) {
// キーダウンイベントは押している間連続で実行されるので最初の1回だけ実行する
if (!keydownFlag) {
console.log("onmousedown:[" + event.code + "]");
eventCode = event.code;
if (eventCode === "ArrowUp"
|| eventCode === "ArrowDown"
|| eventCode === "ArrowLeft"
|| eventCode === "ArrowRight") {
direction = eventCode;
}
}
keydownFlag = true;
};
// ウィンドウリサイズイベント
window.onresize = function(event) {
initCanvasSize();
};
// 画面サイズ初期化
function initCanvasSize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
// 全体の初期化
function initializations() {
initCanvasSize();
eneemyMaterialArray = [];
bulletMaterialArray = [];
materialMyself = new MyselfMaterial();
materialMyself.init();
for (let i = 0; i < ENEMY_MATERIAL_NUM; i++) {
eneemyMaterialArray.push(new EnemyMaterial());
eneemyMaterialArray[i].init();
}
}
// ランダムな値を取得
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
// 自分クラス
function MyselfMaterial() {
this.x;
this.y;
this.moveX;
this.moveY;
// 初期化
this.init = () => {
this.x = (canvas.width / 2) + (MYSELEF_SIZE / 2);
this.y = (canvas.height / 2) - (MYSELEF_SIZE / 2);
this.moveX = 0;
this.moveY = 0;
}
// 当たり判定
this.checkHit = (target) => {
var a = Math.abs(this.x - target.x) < (MYSELEF_SIZE / 2) + (ENEMY_SIZE / 2); //横の判定
var b = Math.abs(this.y - target.y) < (MYSELEF_SIZE / 2) + (ENEMY_SIZE / 2); //縦の判定
var result = a && b;
if (a || b) {
console.log("mx:" + this.x + " my:" + this.y);
console.log("tx:" + target.x + " ty:" + target.y);
console.log("a:" + a + " b:" + b + " result:" + result);
}
return result;
}
// イベント確認
this.eventCheck = () => {
if(keydownFlag){
if (eventCode === "Space") {
var bulletMaterial = new BulletMaterial();
bulletMaterial.init(this);
bulletMaterialArray.push(bulletMaterial);
}
if (eventCode === "ArrowUp") {
this.moveY = -5;
}
if (eventCode === "ArrowDown") {
this.moveY = 5;
}
if (eventCode === "ArrowLeft") {
this.moveX = -5;
}
if (eventCode === "ArrowRight") {
this.moveX = 5;
}
keydownFlag = false;
}
}
// 移動
this.move = () => {
this.x += this.moveX;
this.y += this.moveY;
this.moveY = 0;
this.moveX = 0;
}
// 描画
this.draw = () => {
ctx.fillStyle = "rgba(255,255,255,1.0)";
ctx.fillRect(this.x, this.y, MYSELEF_SIZE, MYSELEF_SIZE);
ctx.fill();
};
}
// 敵クラス
function EnemyMaterial() {
this.x;
this.y;
this.kill;
// 初期化
this.init = () => {
this.x = getRandomInt(canvas.width);
this.y = getRandomInt(canvas.height);
this.kill = false;
}
// 当たり判定
this.checkHit = (target) => {
var a = Math.abs(this.x - target.x) < (ENEMY_SIZE / 2) + (BULLET_SIZE / 2); //横の判定
var b = Math.abs(this.y - target.y) < (ENEMY_SIZE / 2) + (BULLET_SIZE / 2); //縦の判定
var result = a && b;
return result;
}
// 移動
this.move = () => {
// this.y -= 1;
}
// 描画
this.draw = () => {
if(!this.kill) {
ctx.fillStyle = "rgba(255,0,0,1.0)";
ctx.fillRect(this.x, this.y, ENEMY_SIZE, ENEMY_SIZE);
ctx.fill();
}
};
}
// 弾クラス
function BulletMaterial() {
this.x;
this.y;
this.moveY;
this.moveX;
// 初期化
this.init = (origin) => {
this.x = origin.x + (MYSELEF_SIZE / 2) - (BULLET_SIZE / 2);
this.y = origin.y + (MYSELEF_SIZE / 2) - (BULLET_SIZE / 2);
this.moveY = 0;
this.moveX = 0;
if (direction === "ArrowUp") {
this.moveY = -1;
}
if (direction === "ArrowDown") {
this.moveY = 1;
}
if (direction === "ArrowLeft") {
this.moveX = -1;
}
if (direction === "ArrowRight") {
this.moveX = 1;
}
}
// 移動
this.move = () => {
this.y += this.moveY;
this.x += this.moveX;
}
// 描画
this.draw = () => {
ctx.fillStyle = "rgba(0,255,0,1.0)";
ctx.fillRect(this.x, this.y, BULLET_SIZE, BULLET_SIZE);
ctx.fill();
};
}
// スタート処理
function start() {
// 再呼び出し登録
if (mainFlag) {
window.requestAnimationFrame(main);
initializations();
return;
} else {
window.requestAnimationFrame(start);
}
// 画面のクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "rgba(255,255,255,1.0)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fill();
ctx.fillStyle = "rgba(0,0,0,1.0)";
ctx.font = '30px Roboto medium';
ctx.fillText("開始するにはクリックしてください", 128 + 15, 128 * 3);
ctx.fill();
if (mousedownFlag) {
mainFlag = true;
startFlag = false;
mousedownFlag = false;
}
}
// メイン処理
function main() {
// 再呼び出し登録
if (endFlag) {
window.requestAnimationFrame(end);
return;
} else {
window.requestAnimationFrame(main);
}
// 画面のクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 自分の描画
materialMyself.eventCheck();
materialMyself.move();
materialMyself.draw();
// 弾
bulletMaterialArray.forEach(function(element, index) {
eneemyMaterialArray.forEach(function(eneemy, index) {
if(eneemy.checkHit(element)) {
console.log("弾が当たってる["+index+"]");
eneemy.kill = true;
}
});
element.move();
element.draw();
});
// 相手の描画
var allFlag = true;
eneemyMaterialArray.forEach(function(element, index) {
if(materialMyself.checkHit(element)) {
console.log("当たってる["+index+"]");
completFlag = true;
}
if (!element.kill) {
allFlag = false;
}
element.move();
element.draw();
});
if (completFlag || allFlag) {
endFlag = true;
mainFlag = false;
mousedownFlag = false;
completFlag = false;
}
}
// エンド処理
function end() {
// 再呼び出し登録
if (startFlag) {
window.requestAnimationFrame(start);
return;
} else {
window.requestAnimationFrame(end);
}
// 画面のクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "rgba(255,255,255,1.0)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fill();
ctx.fillStyle = "rgba(0,0,0,1.0)";
ctx.font = '30px Roboto medium';
ctx.fillText("クリアーです", 128 + 15, 128 * 3);
ctx.fill();
if (mousedownFlag) {
startFlag = true;
endFlag = false;
mousedownFlag = false;
initializations();
}
}