纯Canvas实现2048小游戏

125 阅读1分钟

看代码注释

完整代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>2048</title>
  <style>
    * {
      padding: 0;
      margin: 0;
    }

    #main {
      margin: 20px auto;
      width: 500px;
      height: 700px;
      /* border: 1px solid #000; */
    }
  </style>
</head>

<body>
  <div id="main"></div>
  <script>
    class Animate {
      startData = 0;// 起始数据
      endData = 0; // 终止数据
      tempData = 0; // 当前数据
      time = 1000; // 动画完成时间
      timer; // 内部定时器
      status = 0; // 状态 -- start: 1 -- pause: 2 -- stop: 0
      pauseTime = 0; // 暂停时间
      startTime = 0; // 开始时间
      tempPauseTime = 0; // 开始暂停的时间
      finishCallBack = () => { }; // 结束回调
      updateCallBack = () => { }; // 更新回调
      startCallBack = () => { }; // 开始回调
      ani = () => { };

      easeIn(startTime, nowTime, time, startNum, length) {
        const t = (nowTime - startTime) / time;
        const point = this.CalculateBezierPointForCube(t, [0, 0], [0, 0.4], [0.6, 1], [1, 1]);
        return startNum + length * point[0];
      }

      /**
       * 三阶贝塞尔曲线
       * B(t) = start * (1-t)^3 + 3 * p0 * t * (1-t)^2 + 3 * p1 * t^2 * (1-t) + end * t^3, t ∈ [0,1]
       * @param {Number} t  曲线长度比例  t ∈ [0,1]
       * @param {Object{x,y}} start 起始点
       * @param {Object{x,y}} p0 控制点1
       * @param {Object{x,y}} p1 控制点2
       * @param {Object{x,y}} end 终止点
       * @return t比例时对应的点 {x,y}
       */
      CalculateBezierPointForCube(t, start, p0, p1, end) {
        const temp = 1 - t;
        let point;
        if (start instanceof Array) {
          point = [];
          point[0] = start[0] * temp * temp * temp + 3 * p0[0] * t * temp * temp + 3 * p1[0] * t * t * temp + end[0] * t * t * t;
          point[1] = start[1] * temp * temp * temp + 3 * p0[1] * t * temp * temp + 3 * p1[1] * t * t * temp + end[1] * t * t * t;
        } else {
          point = {};
          point.x = start.x * temp * temp * temp + 3 * p0.x * t * temp * temp + 3 * p1.x * t * t * temp + end.x * t * t * t;
          point.y = start.y * temp * temp * temp + 3 * p0.y * t * temp * temp + 3 * p1.y * t * t * temp + end.y * t * t * t;
        }
        return point;
      }

      /**
       * 开始数值
       * @param {Number/Array} start 必填
       * @returns this
       */
      from(start) {
        this.startData = start;
        return this;
      }

      /**
       * 结束数值
       * @param {Number/Array} end 必填
       * @returns this
       */
      to(end) {
        this.endData = end;
        return this;
      }

      /**
       * 动画时间
       * @param {Number} time 选填
       * @returns this
       */
      setTime(time) {
        this.time = time;
        return this;
      }

      calculating(startTime, nowTime, time, startNum, length) {
        return this.easeIn(startTime, nowTime, time, startNum, length);
      }

      /**
       * 暂停
       * @returns this
       */
      pause() {
        if (this.status === 1) {
          this.tempPauseTime = Date.now();
          cancelAnimationFrame(this.timer);
          this.status = 2;
        }
        return this;
      }

      /**
       * 停止
       * @param {Function} stopCallBack ---- params(start, end, temp) 停止回调 选题
       * @returns this
       */
      stop(stopCallBack) {
        this.status = 0;
        this.pauseTime = 0;
        cancelAnimationFrame(this.timer);
        this.timer = null;
        stopCallBack instanceof Function && stopCallBack(this.startData, this.endData, this.tempData);
        return this;
      }

      /**
       * 继续
       * @returns this
       */
      continue() {
        if (this.status === 2) {
          this.pauseTime += Date.now() - this.tempPauseTime;
          this.status = 1;
          this.ani();
        }
        return this;
      }

      /**
       * 开始回调
       * @param {Function} startCallBack 开始回调
       * @returns 
       */
      onStart(startCallBack) {
        startCallBack instanceof Function && (this.startCallBack = startCallBack);
        return this;
      }

      onUpdate(updateCallBack) {
        updateCallBack instanceof Function && (this.updateCallBack = updateCallBack);
        return this;
      }

      finish() {
        this.finishCallBack(this.startData, this.endData);
      }

      /**
       * 结束回调
       * @param {Function} finishCallBack ---- (start, end) 结束回调
       * @returns 
       */
      onFinish(finishCallBack) {
        this.finishCallBack = finishCallBack;
        return this;
      }
    }

    class ArrayAnimation extends Animate {
      constructor() {
        super();
      }

      ani = () => {
        const nowDate = Date.now();
        if (nowDate - this.startTime - this.pauseTime >= this.time) {
          this.status = 0;
          this.updateCallBack(this.startData, this.endData, this.endData);
          this.finish();
        } else {
          this.tempData = [];
          for (let i = 0; i < this.startData.length; i++) {
            this.tempData[i] = this.calculating(this.startTime, nowDate - this.pauseTime, this.time, this.startData[i], this.length[i]);
          }
          this.updateCallBack(this.startData, this.endData, this.tempData);
          this.timer = requestAnimationFrame(this.ani);
        }
      }

      start() {
        this.frameCount = this.time / 1000 * this.fps;
        this.length = [];
        for (let i = 0; i < this.startData.length; i++) {
          this.length[i] = this.endData[i] - this.startData[i];
        }
        this.frameNow = 0;
        this.startCallBack();
        this.status = 1;
        this.startTime = Date.now();
        this.ani();
        return this;
      }
    }

    class Cell {
      constructor(id, position, x, y, size, value = 0) {
        this.id = id;
        this.position = position;
        this.x = x;
        this.y = y;
        this.size = size;
        this.value = value;
      }

      copy() {
        return new this.constructor(this.id, this.position, this.x, this.y, this.size, this.value);
      }
    }

    class Game2048 {
      canvas = document.createElement('canvas'); // 画布
      ctx = this.canvas.getContext('2d'); // 画笔
      cells = []; // 格子数据
      viewCells = []; // 格子视图数据
      animCells = []; // 动画中的格子
      cellFontSize = 30; // 数字尺寸
      #size = 4; // 尺寸
      get size() {
        return this.#size;
      }
      set size(value) {
        this.#size = value;
        this.cellFontSize = ~~(15 * (8 / value));
      }
      gamePadding = 20; // 游戏内边距
      cellMargin = 10; // 格子外边距
      gameWidth = 500; // 游戏尺寸
      header = 200; // 头部信息高度
      headerPadding = 10; // 头部内边距
      background = 'rgb(100,100,200)'; // 游戏背景
      cellTextColor = 'rgb(255,255,255)'; // 格子数字颜色
      titleColor = 'rgb(255,255,255)'; // 描述本文颜色
      gameStatus = 0; // 游戏状态 0:未开始   1:进行中   2:结束
      isGameOver = true;
      _scoreValue = 0;
      get scoreValue() { // 本次游戏总分
        return this._scoreValue;
      }
      set scoreValue(value) {
        this._scoreValue = value;
        if (this._scoreValue > this.bestScore) {
          this.bestScore = this._scoreValue;
        }
      }
      bestScore = 0;// 最佳得分
      hover; // 高亮的按钮
      hoverColor = 'rgba(0,0,0, 0.2)'; // 按钮hover背景
      startButtonSize = { // 开始按钮定位
        id: 'start',
        x: 5,
        y: 150,
        width: 80,
        height: 40
      };
      resetButtonSize = { // 重置按钮定位
        id: 'reset',
        x: 103,
        y: 150,
        width: 80,
        height: 40
      };
      sizeAddButtonSize = { // 增加尺寸按钮定位
        id: 'sizeAdd',
        x: 75,
        y: 103,
        width: 30,
        height: 30
      };
      sizeSubtractButtonSize = { // 减小尺寸按钮定位
        id: 'sizeSubtract',
        x: 135,
        y: 103,
        width: 30,
        height: 30
      };
      gameContentSize = { // 游戏内容尺寸
        x: 0,
        y: 200,
        size: 500
      }
      gameOverImg = { // 游戏结束图形定位
        x: 250,
        y: 450,
        width: 500,
        height: 500,
        radius: 200
      };

      shouldCreateRandomValue = false; // 本次操作是否生成新数字

      buttons = [this.startButtonSize, this.resetButtonSize, this.sizeAddButtonSize, this.sizeSubtractButtonSize];

      animSum = 0; // 当前执行的动画个数
      animTime = 60; // 每次操作动画执行时间

      isChange = false; // 是否通过操作改变数据
      canChange = true; // 是否可以操作

      touchstart = null;
      isTouchmove = false;

      cellColorList = new Map([ // 格子颜色组
        [0, 'rgba(50, 50, 50,0.3)'],
        [2, 'rgb(140, 200, 130)'],
        [4, 'rgb(140, 200, 130)'],
        [8, 'rgb(90, 160, 255)'],
        [16, 'rgb(140, 90, 180)'],
        [32, 'rgb(230, 200, 80)'],
        [64, 'rgb(200, 60, 50)'],
        [128, 'rgb(90, 120, 100)'],
        [256, 'rgb(190, 120, 60)'],
        [512, 'rgb(70, 120, 120)'],
        [1024, 'rgb(120, 80, 130)'],
        [2048, 'rgb(120, 40, 100)'],
        [4096, 'rgb(120,180,190)'],
        [8192, 'rgb(80,90,110)'],
        [8192, 'rgb(60,70,80)'],
        [16384, 'rgb(40,40,50)'],
      ]);

      constructor(el) {
        this.el = el;
        this.init();
      }

      // 初始化
      init() {
        this.initCanvas();
        this.el.appendChild(this.canvas);
        this.initData();
        this.initEvent();

        this.ani();
      }

      // 初始化canvas
      initCanvas() {
        this.canvas.width = this.gameWidth;
        this.canvas.height = this.gameWidth + this.header;
        this.canvas.style.background = this.background;
        this.cellSize = (this.canvas.width - this.gamePadding * 2 - (this.size - 1) * this.cellMargin) / this.size;
      }

      // 初始化数据
      initData() {
        for (let i = 0; i < this.size; i++) {
          this.cells[i] = [];
          this.viewCells[i] = [];
          for (let j = 0; j < this.size; j++) {
            const x = this.gamePadding + this.cellMargin * j + j * this.cellSize;
            const y = this.header + this.gamePadding + this.cellMargin * i + i * this.cellSize;
            this.cells[i][j] = new Cell(i + j, [i, j], x, y, this.cellSize);
            this.viewCells[i][j] = new Cell(i + j, [i, j], x, y, this.cellSize);
          }
        }
      }

      // 获取颜色
      getCellColor(value) {
        return this.cellColorList.get(value) || 'rgb(30,30,40)';
      }

      draw() {
        this.drawHeader();
        this.drawCell();
        this.drawAnimCell();
        this.drawGameOverWarp();
      }

      drawGameOverWarp() {
        if (this.gameStatus === 2) {
          const { x, y, width, height, radius } = this.gameOverImg;
          this.ctx.beginPath();
          this.ctx.fillStyle = 'rgb(0,0,0,0.2)';
          this.ctx.fillRect(0, this.header, this.gameWidth, this.gameWidth);
          this.ctx.closePath();
          this.ctx.beginPath();
          this.ctx.fillStyle = 'rgb(0,0,0,0.3)';
          this.ctx.arc(x, y, radius, 0, Math.PI * 2);
          this.ctx.fill();
          this.ctx.closePath();

          this.ctx.beginPath();
          this.ctx.font = `60px cursive`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'center';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText('GAME OVER', x, y);
          this.ctx.closePath();
        }
      }

      drawHeader() {
        if (this.hover) {
          const { x, y, width, height } = this.hover;
          this.ctx.fillStyle = this.hoverColor;
          this.ctx.fillRect(x, y, width, height);
          this.canvas.style.cursor = 'pointer';
        } else {
          this.canvas.style.cursor = 'default';
        }

        {
          // 绘制title
          this.ctx.beginPath();
          this.ctx.font = `70px fantasy`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'center';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText('2048', 250, 50);
          this.ctx.closePath();

          // 绘制size
          this.ctx.beginPath();
          this.ctx.font = `20px cursive`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'left';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText('Size:', this.headerPadding + 5, 120);
          this.ctx.closePath();

          // 绘制size ++
          this.ctx.beginPath();
          this.ctx.font = `30px cursive`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'center';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText('+', this.headerPadding + 80, 120);
          this.ctx.closePath();

          // 绘制size
          this.ctx.beginPath();
          this.ctx.font = `30px cursive`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'center';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText(this.size, this.headerPadding + 110, 120);
          this.ctx.closePath();

          // 绘制size --
          this.ctx.beginPath();
          this.ctx.font = `30px cursive`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'center';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText('-', this.headerPadding + 140, 120);
          this.ctx.closePath();

          // 绘制Play
          this.ctx.beginPath();
          this.ctx.font = `30px fangsong`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'left';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText('Play', this.headerPadding + 8, 170);
          this.ctx.closePath();

          // 绘制reset
          this.ctx.beginPath();
          this.ctx.font = `30px fangsong`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'left';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText('Reset', this.headerPadding + 100, 170);
          this.ctx.closePath();

          // 绘制Score title
          this.ctx.beginPath();
          this.ctx.font = `20px cursive`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'right';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText('Score:', 400, 170);
          this.ctx.closePath();

          // 绘制Score
          this.ctx.beginPath();
          this.ctx.font = `22px cursive`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'left';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText(this.scoreValue, 410, 120);
          this.ctx.closePath();

          // 绘制Best Score title
          this.ctx.beginPath();
          this.ctx.font = `20px cursive`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'right';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText('BestScore:', 400, 120);
          this.ctx.closePath();

          // 绘制Best Score
          this.ctx.beginPath();
          this.ctx.font = `22px cursive`;
          this.ctx.fillStyle = this.titleColor;
          this.ctx.textAlign = 'left';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText(this.bestScore, 410, 170);
          this.ctx.closePath();
        }
      }

      drawCell() {
        for (let i = 0; i < this.size; i++) {
          for (let j = 0; j < this.size; j++) {
            this.drawRect(this.viewCells[i][j]);
            this.drawText(this.viewCells[i][j]);
          }
        }
      }

      // 绘制移动中的格子
      drawAnimCell() {
        for (let i = 0; i < this.animCells.length; i++) {
          this.drawRect(this.animCells[i]);
          this.drawText(this.animCells[i]);
        }
      }

      drawRect(cell) {
        const { x, y, value } = cell;
        this.ctx.beginPath();
        this.ctx.moveTo(x, y);
        this.ctx.lineTo(x + this.cellSize, y);
        this.ctx.lineTo(x + this.cellSize, y + this.cellSize);
        this.ctx.lineTo(x, y + this.cellSize);
        this.ctx.lineTo(x, y);
        this.ctx.fillStyle = this.getCellColor(value);
        this.ctx.fill();
        this.ctx.closePath();
      }

      drawText(cell) {
        const { x, y, value } = cell;
        if (value) {
          this.ctx.beginPath();
          this.ctx.font = `${this.cellFontSize}px Verdana`;
          this.ctx.fillStyle = this.cellTextColor;
          this.ctx.textAlign = 'center';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText(value, x + this.cellSize / 2, y + this.cellSize / 2);
          this.ctx.closePath();
        }
      }

      clear() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      }

      // 判断游戏状态并生成新的数字
      randomEmptyCellValue() {
        const emptyCells = [];
        for (let i = 0; i < this.size; i++) {
          for (let j = 0; j < this.size; j++) {
            const cell = this.cells[i][j];
            if (cell.value === 0) {
              emptyCells.push(cell);
            }
          }
        }
        // 没有空格子进入判断游戏状态
        if (emptyCells.length) {
          if (this.shouldCreateRandomValue) {
            this.isChange = true;
            emptyCells[this.random(emptyCells.length - 1)].value = Math.random() > 0.75 ? 4 : 2;
          }
        } else {
          this.isGameOver = true;
          this.isGameOver && this.moveUp(true);
          this.isGameOver && this.moveDown(true);
          this.isGameOver && this.moveLeft(true);
          this.isGameOver && this.moveRight(true);
          if (this.isGameOver) {
            console.log('game over');
            this.gameStatus = 2;
          }
        }
      }

      random(max, min = 0) {
        return ~~(Math.random() * (max - min) + min);
      }

      initEvent() {
        document.addEventListener('keydown', (e) => {
          if (this.isChange) {
            return;
          }
          if (this.gameStatus === 1) {
            // 操作方法合并变量过多,故分开处理
            switch (e.keyCode) {
              case 37:
                this.moveLeft();
                this.randomEmptyCellValue();
                break;
              case 38:
                this.moveUp();
                this.randomEmptyCellValue();
                break;
              case 39:
                this.moveRight();
                this.randomEmptyCellValue();
                break;
              case 40:
                this.moveDown();
                this.randomEmptyCellValue();
                break;

              default:
                break;
            }
          }
        });

        this.canvas.addEventListener('click', (e) => {
          if (this.hover) {
            switch (this.hover.id) {
              case 'start':
                this.start();
                break;
              case 'reset':
                this.reset();
                break;
              case 'sizeAdd':
                if (this.size < 8) {
                  this.size++;
                }
                this.reset();
                break;
              case 'sizeSubtract':
                if (this.size > 4) {
                  this.size--;
                }
                this.reset();
                break;

              default:
                break;
            }
          }
        });

        this.canvas.addEventListener('mousemove', (e) => {
          const { offsetX, offsetY } = e;
          let i = this.buttons.length;
          this.hover = null;
          while (i--) {
            const button = this.buttons[i];
            if (
              offsetX > button.x
              && offsetX < button.x + button.width
              && offsetY > button.y
              && offsetY < button.y + button.height
            ) {
              this.hover = button;
              break;
            }
          }
        });

        this.canvas.addEventListener('touchstart', (e) => {
          if (this.gameStatus !== 1) {
            return;
          }
          const { x, y, size } = this.gameContentSize;
          if (e.changedTouches.length) {
            const { clientX, clientY } = e.changedTouches[0];
            if (clientX > x && clientX < x + size && clientY > y && clientY < y + size) {
              this.touchstart = [clientX, clientY];
            } else {
              this.touchstart = null;
            }
          }
        });

        this.canvas.addEventListener('touchmove', (e) => {
          if (this.gameStatus !== 1) {
            return;
          }
          const { x, y, size } = this.gameContentSize;
          if (e.changedTouches.length) {
            const { clientX, clientY } = e.changedTouches[0];
            if (clientX > x && clientX < x + size && clientY > y && clientY < y + size) {
              e.preventDefault();
            }
          }
          this.isTouchmove = true;
        });

        this.canvas.addEventListener('touchend', (e) => {
          setTimeout(() => { // 取消按钮点击效果
            this.hover = null;
          }, 200);
          if (this.gameStatus !== 1) {
            return;
          }

          if (this.isTouchmove) {
            if (this.touchstart) {
              if (e.changedTouches.length) {
                const [x, y] = this.touchstart;
                const { clientX, clientY } = e.changedTouches[0];
                const disX = clientX - x;
                const disY = clientY - y;
                if (Math.abs(disX) > Math.abs(disY)) { // 横向
                  if (disX > 0) {
                    this.moveRight();
                  } else {
                    this.moveLeft();
                  }
                } else { // 纵向
                  if (disY > 0) {
                    this.moveDown();
                  } else {
                    this.moveUp();
                  }
                }
                this.randomEmptyCellValue();
              }
            }
          }
          this.touchstart = null;
          this.isTouchmove = false;
        });

      }

      // 创建动画
      createAnim(start, end) {
        const animCell = start.copy();
        this.animCells.push(animCell);
        new ArrayAnimation().from([start.x, start.y]).to([end.x, end.y]).setTime(this.animTime).onStart(() => {
          this.animSum++;
        }).onUpdate((s, e, t) => {
          animCell.x = t[0];
          animCell.y = t[1];
        }).onFinish(() => {
          this.animSum--;
          this.animCells = this.animCells.filter(item => item.id !== animCell.id);
        }).start();
      }

      // 从上往下比较合并
      moveUp(check) {
        this.shouldCreateRandomValue = false;
        for (let j = 0; j < this.size; j++) { // 每列
          let row = 0; // 目标元素所在行
          let i = row + 1; // 对比元素所在行
          while (i < this.size) {
            if (row === this.size - 1) break; // 目标元素为倒数第二行时终止
            const topCell = this.cells[row][j]; // 目标元素
            const cell = this.cells[i][j];
            const viewCell = this.viewCells[i][j];
            if (topCell.value === 0) { // 目标元素为0时将当前列数据赋值给目标元素
              topCell.value = cell.value;
              if (topCell.value !== 0) {
                this.shouldCreateRandomValue = true;
                // 创建动画并执行
                this.createAnim(cell, topCell);
              }
              cell.value = 0;
              viewCell.value = 0;
              i++;
            } else if (topCell.value === cell.value) { // 相等时顶个目标元素值 * 2
              if (check) { // 是否为游戏结束检查状态
                this.isGameOver = false;
                return false;
              } else {
                this.scoreValue += topCell.value;
                this.shouldCreateRandomValue = true;
                topCell.value *= 2;
                // 创建动画并执行
                this.createAnim(cell, topCell);
              }
              row++; // 修改目标元素
              i = row + 1;
              cell.value = 0;
              viewCell.value = 0;
            } else if (cell.value !== 0 && cell.value !== topCell.value) { // 目标元素有值,且当前列有值
              row++;
              i = row + 1;
            } else {
              i++;
            }
          }
        }
      }

      moveDown(check) {
        this.shouldCreateRandomValue = false;
        for (let j = this.size; j--;) { // 每列
          let row = this.size - 1;
          let i = row - 1;
          while (i >= 0) {
            if (row === 0) break; // 目标元素为最后行时终止
            const topCell = this.cells[row][j]; // 目标元素
            const cell = this.cells[i][j];
            const viewCell = this.viewCells[i][j];
            if (topCell.value === 0) { // 目标元素为0时将当前列数据赋值给目标元素
              topCell.value = cell.value;
              if (topCell.value !== 0) {
                this.shouldCreateRandomValue = true;
                // 创建动画并执行
                this.createAnim(cell, topCell);
              }
              cell.value = 0;
              viewCell.value = 0;
              i--;
            } else if (topCell.value === cell.value) { // 相等时顶个目标元素值 * 2
              if (check) {
                this.isGameOver = false;
                return false;
              } else {
                this.scoreValue += topCell.value;
                this.shouldCreateRandomValue = true;
                topCell.value *= 2;
                // 创建动画并执行
                this.createAnim(cell, topCell);
              }
              row--; // 修改目标元素
              i = row - 1;
              cell.value = 0;
              viewCell.value = 0;
            } else if (cell.value !== 0 && cell.value !== topCell.value) { // 目标元素有值,且当前列有值
              row--;
              i = row - 1;
            } else {
              i--;
            }
          }
        }
      }

      moveLeft(check) {
        this.shouldCreateRandomValue = false;
        for (let i = 0; i < this.size; i++) { // 每行
          let row = 0;
          let j = row + 1;
          while (j < this.size) {
            if (row === this.size - 1) break; // 目标元素为倒数第二行时终止
            const topCell = this.cells[i][row]; // 目标元素
            const cell = this.cells[i][j];
            const viewCell = this.viewCells[i][j];
            if (topCell.value === 0) { // 目标元素为0时将当前列数据赋值给目标元素
              topCell.value = cell.value;
              if (topCell.value !== 0) {
                this.shouldCreateRandomValue = true;
                // 创建动画并执行
                this.createAnim(cell, topCell);
              }
              cell.value = 0;
              viewCell.value = 0;
              j++;
            } else if (topCell.value === cell.value) { // 相等时顶个目标元素值 * 2
              if (check) {
                this.isGameOver = false;
                return false;
              } else {
                this.scoreValue += topCell.value;
                this.shouldCreateRandomValue = true;
                topCell.value *= 2;
                // 创建动画并执行
                this.createAnim(cell, topCell);
              }
              row++; // 修改目标元素
              j = row + 1;
              cell.value = 0;
              viewCell.value = 0;
            } else if (cell.value !== 0 && cell.value !== topCell.value) { // 目标元素有值,且当前列有值
              row++;
              j = row + 1;
            } else {
              j++;
            }
          }
        }
      }

      moveRight(check) {
        this.shouldCreateRandomValue = false;
        for (let i = this.size; i--;) { // 每列
          let row = this.size - 1;
          let j = row - 1;
          while (j >= 0) {
            if (row === 0) break; // 目标元素为最后行时终止
            const topCell = this.cells[i][row]; // 目标元素
            const cell = this.cells[i][j];
            const viewCell = this.viewCells[i][j];
            if (topCell.value === 0) { // 目标元素为0时将当前列数据赋值给目标元素
              topCell.value += cell.value;
              if (topCell.value !== 0) {
                this.shouldCreateRandomValue = true;
                // 创建动画并执行
                this.createAnim(cell, topCell);
              }
              cell.value = 0;
              viewCell.value = 0;
              j--;
            } else if (topCell.value === cell.value) { // 相等时顶个目标元素值 * 2
              if (check) {
                this.isGameOver = false;
                return false;
              } else {
                this.scoreValue += topCell.value;
                this.shouldCreateRandomValue = true;
                topCell.value *= 2;
                // 创建动画并执行
                this.createAnim(cell, topCell);
              }
              row--; // 修改目标元素
              j = row - 1;
              cell.value = 0;
              viewCell.value = 0;
            } else if (cell.value !== 0 && cell.value !== topCell.value) { // 目标元素有值,且当前列有值
              row--;
              j = row - 1;
            } else {
              j--;
            }
          }
        }
      }

      reset() {
        this.cells = [[]];
        this.viewCells = [[]];
        // 可能有动画遗留问题 --- 暂不想解决
        this.initCanvas();
        this.initData();
        this.gameStatus = 0;
        this.scoreValue = 0;
      }

      start() {
        if (this.gameStatus === 0) {
          this.gameStatus = 1;
          this.shouldCreateRandomValue = true;
          this.randomEmptyCellValue();
        }
      }

      updateData() {
        if (!this.animSum && this.isChange) {
          for (let i = 0; i < this.size; i++) {
            for (let j = 0; j < this.size; j++) {
              this.viewCells[i][j].value = this.cells[i][j].value;
            }
          }
          this.isChange = false;
        }
      }

      ani = () => {
        this.clear();
        this.updateData();
        this.draw();
        requestAnimationFrame(this.ani);
      }
    }

    const game2048 = new Game2048(document.getElementById('main'));
  </script>
</body>

</html>