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

1. 背景

以前寫過一篇文章 ES6 手寫一個「辨色」小遊戲, 感受好玩挺不錯。豈料評論區大神頻出,其中有人指出,打開控制檯,輸入如下代碼:javascript

setInterval( ()=>document.querySelector('#special-block').click(),1)
複製代碼

便可破解,分數蹭蹭上漲,這不就是bug嗎?同時評論區 【愛編程的李先森】建議,讓我用 canvas 來畫會更簡單,所以有了這篇文章。css

話很少說,先上 Demo項目源碼html

有趣的是,在我寫完這篇文章以後,發現【愛編程的李先森】也寫了一篇canvas手寫辨色力小遊戲,實現方式有所不一樣,能夠對比下。前端

項目預覽

2. 實現

本項目基於 typescriptcanvas 實現java

(1) 首先定義配置項

一個canvas標籤,遊戲總時長time, 開始函數start, 結束函數endwebpack

interface BaseOptions {
  time?: number;
  end?: Function;
  start?: Function;
  canvas?: HTMLCanvasElement;
}
複製代碼

定義類 ColorGame 實現的接口 ColorGameType, init()初始化方法,nextStep()下一步,reStart()從新開始方法git

interface ColorGameType {
  init: Function;
  nextStep: Function;
  reStart: Function;
}
複製代碼

定義一個座標對象,用於儲存每一個色塊的起始點github

interface Coordinate {
  x: number;
  y: number;
}
複製代碼

(2) 實現類 ColorGame

定義好了須要用到的接口,再用類去實現它web

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 事件。typescript

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

好了,等大家再次來破解,哈哈哈哈!!!😂😂😂😂😂

更多推薦

前端進階小書(advanced_front_end)

前端每日一題(daily-question)

webpack4 搭建 Vue 應用(createVue)

Canvas 進階(一)二維碼的生成與掃碼識別

Canvas 進階(二)寫一個生成帶logo的二維碼npm插件

相關文章
相關標籤/搜索