目录
canvas动画原理
假设一个元素x轴为0,运动到x = 100这样直接绘制是没有动画的
x值递增,x值改变时清除画布再次绘制,x=100时运动结束
这就是动画原理
1. 思路
这是一个标准的4*4的布局
1. 框架
我们已知小方块包含在4*4的大盒子里,设定小方块的宽度为w,盒子宽高就是w*4。代码就为
// 我这里预留了每个小方块之间的空隙
// 也可以写完 width * 4 这样每个小方块就紧挨着了
const size = width * 4 + 4 * (4 - 1);
dom.ctx.clearRect(0, 0, size, size);
dom.cvs.width = size;
dom.cvs.height = size;
2. 小方块
1. 每个小方块都有自己的xy轴数值,还有宽度,我这里设置小方块的属性有这些。
let boxItem = {
x:x轴坐标, 这里的x,y时原始坐标{x:1,y:1}
y:y轴坐标,
color:方块颜色,
step:方块合成次数, 两个相同的合成一个step++
num:方块值,
dx:绘制x轴, x * w
dy:绘制y轴,
duration:动画持续时间,
direction:运动的轴, x,y两个轴方便后续添加动画
};
2. 运动后只改变x和y轴,dx和dy将在动态加减用于动画绘制
3. 移动
1. 我这里生成的初始数据是一维数组
2. 为了方便计算运动前会对数据进行分组,分组根据向y或x轴运动分组,然后在根据运动方 向计算
这是根据y轴分组
这是根据x轴分组
向上运动时可以知道时y轴在动,向上y在减少,向下y在增加,这里是向上运动
第一步判断是否有方块在边界,没有的话第一个方块移动到边界
第二步判断当前数组长度是否大于1,大于1获取上一位对象判断num值相等,相等就合成不 相等就移动,移动的算法是上一位对象的y轴加一,相等就删除当前元素改变上一位对象的 step,num,num += num,step++
4. 移动动画
在上面已经计算出对象的x,y轴但是dx,dy还未改变,绘制也是根据dx,dy这两个值绘制的,当设置动画时间time = 16里时可以计算出在这time时间里每次移动像素值,当y = 0,dy = 100时 每次运动的值duration = (y * w - dy) / time,每次改变值时需要判断duration是否不等于0,因为可能当前元素初始位置就是在边界duration = 0,然后就是判断dy,dx是否改变完成,改变完成后清除requestAnimationFrame这个函数,动画结束后生成随机方块
2.全部代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2048</title>
<!-- <script src="./2048.js"></script> -->
<style>
.btnGroup{
width: 150px;
display: flex;
flex-wrap: wrap;
}
.btnGroup button{
width: 50px;
height: 50px;
margin: 0;
padding: 0;
}
.btnGroup #up{
margin-left: 50px;
margin-right: 50px;
}
.box{
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<audio src="https://webfs.hw.kugou.com/202311131637/678484d6605c880f4def32372a95e708/v2/6e1986632da3fef9d4dd19242d93d7c8/G200/M01/17/13/qJQEAF5ci-CAepwjACSesLRjZs4083.mp3"></audio>
<div style="text-align: center;">
<h1 id="score">分数:0</h1>
<button onclick="start()">开始游戏</button>
</div>
<div class="box">
<canvas id="canvas"></canvas>
<span class="btnGroup">
<button class="direction" id="up">↑</button>
<button class="direction" id="left">←</button>
<button class="direction" id="down">↓</button>
<button class="direction" id="right">→</button>
</span>
</div>
</body>
<script>
/**
* 2048游戏
* 方块参数{x:x轴坐标,y:y轴坐标,color:方块颜色,step:方块合成次数,num:方块值,dx:绘制x轴,dy:绘制y轴,duration:动画持续时间,direction:运动的轴};
*/
class Game {
constructor() {
this.config = {
bg: "#ccc",
border: "1px solid #e3e3e3",
width: 100,
boxNum: 4,
dom: {
cvs: document.querySelector("#canvas"),
ctx: document.querySelector("#canvas").getContext("2d"),
},
box: {},
};
this.maxScore =
this.config.boxNum *
this.config.boxNum *
(this.config.boxNum *
this.config.boxNum *
this.config.boxNum *
this.config.boxNum *
this.config.boxNum *
this.config.boxNum *
(this.config.boxNum / 2));
this.score = 0;
this.boxArr = [];
this.color = [];
this.time = 32;
this.isActive = false;
this.requestAFId = "";
this.init();
}
init = () => {
const { dom, width, boxNum, border } = this.config;
const size = width * boxNum + boxNum * (boxNum - 1);
dom.ctx.clearRect(0, 0, size, size);
dom.cvs.width = size;
dom.cvs.height = size;
dom.cvs.style.border = border;
this.boxArr = this.getBox();
this.getBoxColor();
this.draw();
this.addEvent();
};
draw = () => {
const { width, boxNum, dom } = this.config;
const size = width * boxNum + boxNum * (boxNum - 1);
dom.ctx.clearRect(0, 0, size, size);
for (let i = 0; i < this.boxArr.length; i++) {
const item = this.boxArr[i];
this.drawBox(
item.dx + item.x * boxNum,
item.dy + item.y * boxNum,
this.color[item.step] ? this.color[item.step] : "#e3e3e3",
item.num
);
}
};
// 移动过渡动画
transition = () => {
if (this.isActive) {
return
}
this.isActive = true;
const { width, boxNum } = this.config;
const MaxNum = (boxNum - 1) * width;
for (let i = 0; i < this.boxArr.length; i++) {
const item = this.boxArr[i];
// 判断方向
if (item.x * width !== item.dx) {
item.direction = "x";
item.duration = (item.x * width - item.dx) / this.time;
} else if (item.y * width !== item.dy) {
item.direction = "y";
item.duration = (item.y * width - item.dy) / this.time;
}
}
const checkD = () => {
let d = false;
for (let i = 0; i < this.boxArr.length; i++) {
const item = this.boxArr[i];
if (item.direction && item[item.direction] * width !== item[`d${item.direction}`]) {
d = false;
break;
} else {
d = true;
}
}
return d;
};
/**
* 每次移动一像素需要在time规定的时间内完成移动绘制
*/
let is = false;
const start = () => {
for (let i = 0; i < this.boxArr.length; i++) {
const item = this.boxArr[i];
if (checkD()) {
is = true;
break;
}
if (item[`d${item.direction}`] === item[item.direction] * width) {
item.duration = 0;
continue;
}
if (item.duration > 0 || item.duration < 0) {
item[`d${item.direction}`] += item.duration;
if (item[`d${item.direction}`] >= MaxNum) {
item[`d${item.direction}`] = MaxNum;
item.duration = 0;
} else if (item[`d${item.direction}`] <= 0) {
item[`d${item.direction}`] = 0;
item.duration = 0;
}
}
}
this.draw();
if (!is) {
this.requestAFId = requestAnimationFrame(start);
} else {
cancelAnimationFrame(this.requestAFId);
this.requestAFId = "";
this.isActive = false;
this.getRamdomBox();
}
};
start();
};
// 获取对应数值的色块
getBoxColor = () => {
const num = this.config.boxNum * this.config.boxNum * 2;
for (let i = 0; i < num; i++) {}
};
// 查找item下标
findItemIndex = (x, y) => {
const index = this.boxArr.findIndex((item) => item.x === x && item.y === y);
return index;
};
// 查找BoxItem
findItem = (x, y) => {
const item = this.boxArr.find((item) => item.x === x && item.y === y);
return item;
};
// 分组
group = (arr, direction = "y") => {
const group = {};
const keys = direction === "x" ? ["x", "y"] : ["y", "x"];
for (const item of arr) {
const key = item[keys[0]];
if (!group[key]) {
group[key] = {};
}
group[key][item[keys[1]]] = item;
}
const arrs = Object.values(group).map((item) => {
item = Object.values(item);
return item;
});
return arrs;
};
// 判断移动
/**
* 分组是按照x,y进行分组
*/
isCanMove = (arr, direction, isWhere) => {
const { width, boxNum } = this.config;
const groupArr = arr;
if (direction === "right" || direction === "down") {
for (let i = 0; i < groupArr.length; i++) {
groupArr[i] = groupArr[i].reverse();
}
}
const boundaryNum =
direction === "right" || direction === "down" ? boxNum - 1 : 0; // 根据方向判断边界值
for (let i = 0; i < groupArr.length; i++) {
const itemArr = groupArr[i];
for (let j = 0; j < itemArr.length; j++) {
let item = itemArr[j];
let prevItem = itemArr[j - 1];
switch (direction) {
case "left":
if (j === 0) {
if (item.x <= boundaryNum) continue;
else {
item.x = boundaryNum;
}
} else {
if (prevItem.num === item.num) {
prevItem.num += prevItem.num;
prevItem.step++;
itemArr.splice(j, 1);
j--;
this.score += prevItem.num / 2;
}
item.x = prevItem.x + 1;
}
break;
case "right":
if (j === 0) {
if (item.x >= boundaryNum) continue;
else {
item.x = boundaryNum;
}
} else {
if (prevItem.num === item.num) {
prevItem.num += prevItem.num;
prevItem.step++;
itemArr.splice(j, 1);
j--;
this.score += prevItem.num / 2;
}
item.x = prevItem.x - 1;
}
break;
case "up":
if (j === 0) {
if (item.y <= boundaryNum) continue;
else {
item.y = boundaryNum;
}
} else {
if (prevItem.num === item.num) {
prevItem.num += prevItem.num;
prevItem.step++;
itemArr.splice(j, 1);
j--;
this.score += prevItem.num / 2;
}
item.y = prevItem.y + 1;
}
break;
case "down":
if (j === 0) {
if (item.y >= boundaryNum) continue;
else {
item.y = boundaryNum;
}
} else {
if (prevItem.num === item.num) {
prevItem.num += prevItem.num;
prevItem.step++;
itemArr.splice(j, 1);
j--;
this.score += prevItem.num / 2;
}
item.y = prevItem.y - 1;
}
break;
}
}
}
return groupArr;
};
// 移动方块
move = (e) => {
// type : up right down left
const type = e.srcElement.id;
const isWhere = type == "up" || type == "down" ? "x" : "y";
const groupArr = this.isCanMove(
this.group(this.boxArr, isWhere),
type,
isWhere == "x" ? "y" : "x"
);
this.boxArr = groupArr.flat(1);
this.transition();
this.drawScore();
};
// 添加事件
addEvent = () => {
const buttonArr = document.querySelectorAll(".direction");
for (let i = 0; i < buttonArr.length; i++) {
buttonArr[i].addEventListener("click", this.move);
}
};
// 生成方块对应的颜色
getBoxColor = () => {
const step = this.config.boxNum * this.config.boxNum;
for (let i = 0; i < step; i++) {
const color = "#" + Math.floor(Math.random() * 16777215).toString(16);
this.color.push(color);
}
};
// 合成后生成随机方块
ramdomBox = () => {
const { width, boxNum } = this.config;
const x = Math.floor(Math.random() * boxNum);
const y = Math.floor(Math.random() * boxNum);
if (this.findItem(x, y)) {
this.ramdomBox();
return;
} else {
const obj = {
x,
y,
color: "#e3e3e3",
step: 0,
num: 2,
dx: x * width,
dy: y * width,
};
this.boxArr.push(obj);
}
};
// 获取随机方块
getRamdomBox = () => {
const MaxBox = this.config.boxNum * this.config.boxNum;
if (this.boxArr.length >= MaxBox) {
alert(`游戏结束!!!分数${this.score}`);
this.re();
return;
}
const num =
MaxBox - this.boxArr.length >= 2 ? 2 : MaxBox - this.boxArr.length;
for (let i = 0; i < num; i++) {
this.ramdomBox();
}
this.draw();
};
// 获取初始方块
getBox = () => {
const { width, boxNum } = this.config;
const boxArr = [];
const num = 2;
for (let i = 0; i < num; i++) {
let x = Math.floor(Math.random() * boxNum);
let y = Math.floor(Math.random() * boxNum);
const color = "#e3e3e3";
const obj = {
x,
y,
color,
step: 0,
num: 2,
dx: width * x,
dy: width * y,
duration: 0,
direction: "",
};
const is = boxArr.find((item) => item.x === x && item.y === y);
if (!is) {
boxArr.push(obj);
} else {
x = x - 1 > 0 ? x - 1 : x + 1 < boxNum ? x + 1 : 0;
y = y - 1 > 0 ? y - 1 : y + 1 < boxNum ? y + 1 : 0;
obj.x = x;
obj.y = y;
obj.dx = width * x;
obj.dy = width * y;
boxArr.push(obj);
}
}
return boxArr;
};
// 绘制分数
drawScore = () => {
const scoreDom = document.querySelector("#score");
scoreDom.innerHTML = `分数:${this.score}`;
return true;
};
drawBox = (x = 0, y = 0, color = "#000000", num = 2) => {
const {
dom: { ctx },
width,
} = this.config;
ctx.beginPath();
ctx.fillStyle = color;
ctx.fillRect(x, y, width, width);
ctx.font = `${width / 3}px 微软雅黑`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#000000";
ctx.fillText(num, x + width / 2, y + width / 2);
ctx.closePath();
};
// 清除这个class
re = () => {
const buttonArr = document.querySelectorAll(".direction");
for (let i = 0; i < buttonArr.length; i++) {
buttonArr[i].removeEventListener("click", this.move);
}
this.config = null;
this.score = 0;
};
}
let game;
function start() {
if (game) {
game.re();
game = "";
}
game = new Game();
}
window.onload = () => {
game = new Game();
// 创建视频标签
const audio = document.querySelector("audio");
let eventArr = {
w: "up",
s: "down",
d: "right",
a: "left",
W: "up",
S: "down",
D: "right",
A: "left",
ArrowDown: "down",
ArrowUp: "up",
ArrowRight: "right",
ArrowLeft: "left",
};
document.onkeydown = (e) => {
if (e.key in eventArr) {
document.getElementById(eventArr[e.key]).click();
}
};
document.body.addEventListener("click", () => {
audio.play()
});
};
</script>
</html>