专栏首页小皮咖Canvas 进阶(三)ts + canvas 重写”辨色“小游戏

Canvas 进阶(三)ts + canvas 重写”辨色“小游戏

1. 背景

之前写过一篇文章 ES6 手写一个“辨色”小游戏, 感觉好玩挺不错。岂料评论区大神频出,其中有人指出,打开控制台,输入以下代码:

setInterval( ()=>document.querySelector('#special-block').click(),1)
复制代码

即可破解,分数蹭蹭上涨,这不就是bug吗?同时评论区 【爱编程的李先森】建议,让我用 canvas 来画会更简单,因此有了这篇文章。

话不多说,先上 Demo项目源码

有趣的是,在我写完这篇文章之后,发现【爱编程的李先森】也写了一篇canvas手写辨色力小游戏,实现方式有所不同,可以对比下。

2. 实现

本项目基于 typescriptcanvas 实现

(1) 首先定义配置项

一个canvas标签,游戏总时长time, 开始函数start, 结束函数end

interface BaseOptions {
  time?: number;
  end?: Function;
  start?: Function;
  canvas?: HTMLCanvasElement;
}

定义类 ColorGame 实现的接口 ColorGameType, init()初始化方法,nextStep()下一步,reStart()重新开始方法

interface ColorGameType {
  init: Function;
  nextStep: Function;
  reStart: Function;
}

定义一个坐标对象,用于储存每个色块的起始点

interface Coordinate {
  x: number;
  y: number;
}

(2) 实现类 ColorGame

定义好了需要用到的接口,再用类去实现它

class ColorGame implements ColorGameType {
  option: BaseOptions;
  step: number; // 步
  score: number; // 得分
  time: number; // 游戏总时间
  blockWidth: number; // 盒子宽度
  randomBlock: number; // 随机盒子索引
  positionArray: Array<Coordinate>; // 存放色块的数组
  constructor(userOption: BaseOptions) {
    // 默认设置
    this.option = {
      time: 30, // 总时长
      canvas: <HTMLCanvasElement>document.getElementById("canvas"),
      start: () => {
        document.getElementById("result").innerHTML = "";
        document.getElementById("screen").style.display = "block";
      },
      end: (score: number) => {
        document.getElementById("screen").style.display = "none";
        document.getElementById(
          "result"
        ).innerHTML = `<div class="result" style="width: 100%;">
        <div class="block-inner" id="restart"> 您的得分是: ${score} <br/> 点击重新玩一次</div>
      </div>`;
        // @ts-ignore
        addEvent(document.getElementById("restart"), "click", () => {
          this.reStart();
        });
      } // 结束函数
    };
    this.init(userOption); // 初始化,合并用户配置
  }
  init(userOption: BaseOptions) {
  }
  nextStep() {}
  // 重新开始其实也是重新init()一次
  reStart() {
    this.init(this.option);
  }
}
复制代码

(3)实现 init() 方法

init() 方法实现参数初始化,执行 start() 方法,并在最后执行 nextStep() 方法,并监听 canvasmousedowntouchstart 事件。

这里用到 canvas.getContext("2d").isPointInPath(x, y) 判断点击点是否处于最后一次绘画的矩形内,因此特殊颜色的色块要放在最后一次绘制

init(userOption: BaseOptions) {
    if (this.option.start) this.option.start();
    this.step = 0; // 步骤初始化
    this.score = 0;// 分数初始化
    this.time = this.option.time; // 倒计时初始化
    // 合并参数
    if (userOption) {
      if (Object.assign) {
        Object.assign(this.option, userOption);
      } else {
        extend(this.option, userOption, true);
      }
    }
    
    // 设置初始时间和分数
    document.getElementsByClassName(
      "wgt-score"
    )[0].innerHTML = `得分:<span id="score">${this.score}</span>
    时间:<span id="timer">${this.time}</span>`;

    // 开始计时
    (<any>window).timer = setInterval(() => {
      if (this.time === 0) {
        clearInterval((<any>window).timer);
        this.option.end(this.score);
      } else {
        this.time--;
        document.getElementById("timer").innerHTML = this.time.toString();
      }
    }, 1000);
    
    this.nextStep(); // 下一关
    ["mousedown", "touchstart"].forEach(event => {
      this.option.canvas.addEventListener(event, e => {
        let loc = windowToCanvas(this.option.canvas, e);
        // isPointInPath 判断是否在最后一次绘制矩形内
        if (this.option.canvas.getContext("2d").isPointInPath (loc.x, loc.y)) {
          this.nextStep();
          this.score++;
          document.getElementById("score").innerHTML = this.score.toString();
        }
      });
    });
  }
复制代码

(4)实现 nextStep() 方法

nexStep() 这里实现的是每一回合分数增加,以及画面的重新绘画,这里我用了 this.blockWidth 存放每一级色块的宽度, this.randomBlock 存放随机特殊颜色色块的index, this.positionArray 用于存放每个色块的左上角坐标点,默认设置色块之间为2像素的空白间距。

有一个特殊的地方是在清除画布时ctx.clearRect(0, 0, canvas.width, canvas.width);,需要先 ctx.beginPath();清除之前记忆的路径。否则会出现以下的效果:

nextStep() {
    // 记级
    this.step++;
    let col: number; // 列数
    if (this.step < 6) {
      col = this.step + 1;
    } else if (this.step < 12) {
      col = Math.floor(this.step / 2) * 2;
    } else if (this.step < 18) {
      col = Math.floor(this.step / 3) * 3;
    } else {
      col = 16;
    }
    let canvas = this.option.canvas;
    let ctx = canvas.getContext("2d");
    ctx.beginPath();
    ctx.clearRect(0, 0, canvas.width, canvas.width); // 清除画布
    ctx.closePath();
    // 小盒子宽度
    this.blockWidth = (canvas.width - (col - 1) * 2) / col;
    // 随机盒子index
    this.randomBlock = Math.floor(col * col * Math.random());
    // 解构赋值获取一般颜色和特殊颜色
    let [normalColor, specialColor] = getColor(this.step);

    this.positionArray = [];
    for (let i = 0; i < col ** 2; i++) {
      let row = Math.floor(i / col);
      let colNow = i % col;
      let x = colNow * (this.blockWidth + 2),
        y = row * (this.blockWidth + 2);

      this.positionArray.push({
        x,
        y
      });
      if (i !== this.randomBlock)
        drawItem(ctx, normalColor, x, y, this.blockWidth, this.blockWidth);
    }

    ctx.beginPath();
    drawItem(
      ctx,
      specialColor,
      this.positionArray[this.randomBlock].x,
      this.positionArray[this.randomBlock].y,
      this.blockWidth,
      this.blockWidth
    );
    ctx.closePath();
  }

drawItem()用于绘制每一个色块, 这里需要指出的是,isPointInPath 是判断是否处于矩形的路径上,只有使用 context.fill() 才能使整个矩形成为判断的路径。

function drawItem(
  context: Context,
  color: string,
  x: number,
  y: number,
  width: number,
  height: number
): void {
  context.fillStyle = `#${color}`;
  context.rect(x, y, width, height);
  context.fill(); //替代fillRect();
}
复制代码

(5) 其他共用方法 gameMethods.tsutils.ts

// gameMethods.ts
/**
 * 根据关卡等级返回相应的一般颜色和特殊颜色
 * @param {number} step 关卡
 */
export function getColor(step: number): Array<string> {
  let random = Math.floor(100 / step);
  let color = randomColor(17, 255),
    m: Array<string | number> = color.match(/[\da-z]{2}/g);
  for (let i = 0; i < m.length; i++) m[i] = parseInt(String(m[i]), 16); //rgb
  let specialColor =
    getRandomColorNumber(m[0], random) +
    getRandomColorNumber(m[1], random) +
    getRandomColorNumber(m[2], random);
  return [color, specialColor];
}
/**
 * 返回随机颜色的一部分值
 * @param num 数字
 * @param random 随机数
 */
export function getRandomColorNumber(
  num: number | string,
  random: number
): string {
  let temp = Math.floor(Number(num) + (Math.random() < 0.5 ? -1 : 1) * random);
  if (temp > 255) {
    return "ff";
  } else if (temp > 16) {
    return temp.toString(16);
  } else if (temp > 0) {
    return "0" + temp.toString(16);
  } else {
    return "00";
  }
}
// 随机颜色 min 大于16
export function randomColor(min: number, max: number): string {
  var r = randomNum(min, max).toString(16);
  var g = randomNum(min, max).toString(16);
  var b = randomNum(min, max).toString(16);
  return r + g + b;
}
// 随机数
export function randomNum(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min) + min);
}

复制代码
// utils.ts
/**
 * 合并两个对象
 * @param o 默认对象
 * @param n 自定义对象
 * @param override 是否覆盖默认对象
 */
export function extend(o: any, n: any, override: boolean): void {
  for (var p in n) {
    if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override)) o[p] = n[p];
  }
}

/**
 *   事件兼容方法
 * @param element dom元素
 * @param type 事件类型
 * @param handler 事件处理函数
 */
export function addEvent(element: HTMLElement, type: string, handler: any) {
  if (element.addEventListener) {
    element.addEventListener(type, handler, false);
    // @ts-ignore
  } else if (element.attachEvent) {
    // @ts-ignore
    element.attachEvent("on" + type, handler);
  } else {
    // @ts-ignore
    element["on" + type] = handler;
  }
}

/**
 * 获取点击点于canvas内的坐标
 * @param canvas canvas对象
 * @param e 点击事件
 */
export function windowToCanvas(canvas: HTMLCanvasElement, e: any) {
  let bbox = canvas.getBoundingClientRect(),
    x = IsPC() ? e.clientX || e.clientX : e.changedTouches[0].clientX,
    y = IsPC() ? e.clientY || e.clientY : e.changedTouches[0].clientY;

  return {
    x: x - bbox.left,
    y: y - bbox.top
  };
}

/**
 * 判断是否为 PC 端,若是则返回 true,否则返回 flase
 */
export function IsPC() {
  let userAgentInfo = navigator.userAgent,
    flag = true,
    Agents = [
      "Android",
      "iPhone",
      "SymbianOS",
      "Windows Phone",
      "iPad",
      "iPod"
    ];

  for (let v = 0; v < Agents.length; v++) {
    if (userAgentInfo.indexOf(Agents[v]) > 0) {
      flag = false;
      break;
    }
  }
  return flag;
}
复制代码

3. 使用

将代码打包构建后引入 html 后,新建 new ColorGame(option) 即可实现。前提是页面结构如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>canvas 辨色小游戏</title>
    <link
      rel="stylesheet"
      href="https://zxpsuper.github.io/Demo/color/index.css"
    />
  </head>
  <body>
    <div class="container">
      <div class="wgt-home" id="page-one">
        <h1>辨色力测试</h1>
        <p>找出所有色块里颜色不同的一个</p>
        <a id="start" class="btn btn-primary btn-lg">开始挑战</a>
      </div>
      <header class="header">
        <h1>辨色力测试</h1>
      </header>

      <aside class="wgt-score"></aside>

      <section id="screen" class="screen">
        <canvas id="canvas" width="600" height="600"></canvas>
      </section>
      <section id="result"></section>

      <footer>
        <div>
          <a href="http://zxpsuper.github.io" style="color: #FAF8EF">
            my blog</a
          >
        </div>
        ©<a href="https://zxpsuper.github.io">Suporka</a> ©<a
          href="https://zxpsuper.github.io/Demo/advanced_front_end/"
          >My book</a
        >
        ©<a href="https://github.com/zxpsuper">My Github</a>
      </footer>
    </div>
    <script src="./ColorGame2.js"></script>
    <script>
      function addEvent(element, type, handler) {
        if (element.addEventListener) {
          element.addEventListener(type, handler, false);
        } else if (element.attachEvent) {
          element.attachEvent("on" + type, handler);
        } else {
          element["on" + type] = handler;
        }
      }
      window.onload = function() {
        addEvent(document.querySelector("#start"), "click", function() {
          document.querySelector("#page-one").style.display = "none";
          new ColorGame({
            time: 30
          });
        });
      };
    </script>
  </body>
</html>
复制代码

总结

这里主要是对 isPointInPath 的使用实践,在之后的文章《canvas绘制九宫格》也会用到此方法,敬请期待!

好了,等你们再次来破解,哈哈哈哈!!!?????

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Node.js服务端开发教程 (一):NestJS框架0到1

    要做Node.js编程嘛,Node.js是必须安装的,大家可以到官网(https://nodejs.org)下载安装,推荐安装LTS版本。

    一斤代码
  • Angular 6.x 基础教程

    若想进一步了解 Angular CLI 的详细信息,请参考 Angular CLI 终极指南。

    semlinker
  • 《Vue3.0抢先学》系列之:一个简单的例子

    书接上文:你被我撩拨了一下,从Github上下载了Vue3.0的源码。然后呢,你是不是已经迫不及待的想知道到底怎么样快速的把这个源代码用起来呢?

    一斤代码
  • NC |SCALE准确鉴定单细胞ATAC-seq数据中染色质开放特征

    SCALE全称是Single-Cell ATAC-seq analysis vie Latent feature Extraction, 从名字中就能知道这个软...

    生信宝典
  • Serverless 最佳实践之数据库的连接和查询

    在第一讲云函数的生命周期中,我们已经提到了在云函数 Mount 阶段创建数据库连接带来的两方面好处:

    朱峰
  • 从零开始构建 vue3

    2019年10月5日凌晨,Vue 的作者尤雨溪公布了 Vue3 的源代码。当然,它暂时还不是完整的 Vue3,而是 pre-alpha 版,只完成了一些核心功能...

    我是一条小青蛇
  • 《Vue3.0抢先学》系列之:网友们都惊呆了!

    今天开始,我想给大家讲点新东西。大家不用大喊学不动,请放松心情随意观看,我也讲不出什么很深奥难学的东西,本系列文章都会是些比较浅显易懂的家常内容。

    一斤代码
  • 《Vue3.0抢先学》系列之:使用Composition API

    在上一篇文章中,我们大致了解了如何使用Vue3.0编写一个简单的计数器程序。不过,正如熟悉Vue2.x的朋友所看到的,我们用Vue3.0实现出来的代码和Vue2...

    一斤代码
  • typescript 高级技巧

    用了一段时间的 typescript 之后,深感中大型项目中 typescript 的必要性,它能够提前在编译期避免许多 bug,如很恶心的拼写问题。

    夜尽天明
  • 为什么不学基于TypeScript的Node.js服务端开发?

    我们早就知道,如今的JavaScript已经不再是当初那个在浏览器网页中写写简单的表单验证、没事弹个alert框吓吓人的龙套角色了。借助基于v8引擎的Node....

    一斤代码

扫码关注云+社区

领取腾讯云代金券