Canvas 進階(三)ts + canvas 重寫」辨色「小遊戲

  • 2019 年 11 月 5 日
  • 筆記

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繪製九宮格》也會用到此方法,敬請期待!

好了,等你們再次來破解,哈哈哈哈!!!?????