iT邦幫忙

2021 iThome 鐵人賽

DAY 27
1
Modern Web

Canvas 小錦囊系列 第 27

Day 27 - 用 canvas 模擬手機圖型解鎖

前述

今天來用 canvas 做一個 手機解鎖的模擬功能~

 onTouchStart={handleStart}
 onTouchMove={handleMove}
 onTouchEnd={handleEnd}

要注意的是! 因為是手機模擬功能
所以使用的 是 touchmove
而如果要用一般電腦裝置滑鼠效果的話,
要改成mousemove
然後將有判斷 event.touches[0] 的部分修改掉就好。

效果

codesendBox

Code

import React, { useEffect, useState, useRef } from "react";

const canvasSize = 500;
let linewidth = 2; //畫筆(線)寬度

let signal = 0;
let suceessCount = 0;
var initPosition = 0;
const radius = 0.05 * canvasSize; // 半徑
const wgap = (canvasSize - 3 * 2 * (radius + linewidth)) / 6;
const hgap = (canvasSize - 3 * 2 * (radius + linewidth)) / 6;

var position = [];
var touchPoints = [];

const CanvasLock = () => {
  const canvasRef = useRef(null);
  const [ninepoints, setNinepoints] = useState([]);
  const [result, setResult] = useState(null);

  useEffect(() => {
    if (canvasRef.current) {
      initCanvas();
    }
  }, [canvasRef]);

  const handleStart = (event) => {
    let targets = event.touches;
    if (targets.length === 1) {
      isPointselect(targets[0]);
    }
  };

  const handleMove = (event) => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    // event.preventDefault(); // 阻止默認行為
    var target = event.touches[0], // 單指
      touches = event.targetTouches;
    if (touches.length !== 1) return;
    // 點擊位置
    const point = getClientOffset(target);
    clearcanvas();
    let beforecurr;
    if (event) {
      // 已滑過的點
      let oldpoints = touchPoints;
      let oldlen = oldpoints.length;
      let maxlen;
      isPointselect(target);
      let newlen = touchPoints.length;
      if (initPosition === 0) {
        //加入第一個點
        position.push(ninepoints[touchPoints[0]]?.x);
        position.push(ninepoints[touchPoints[0]]?.y);
        initPosition++;
      }
      // 新的點
      if (newlen > oldlen) {
        maxlen = newlen - 1;
        beforecurr = touchPoints[maxlen - 1];
        let currlen = touchPoints[maxlen];
        position.push(ninepoints[currlen]?.x);
        position.push(ninepoints[currlen]?.y);
      }
      // 線跟著手指移動
      if (newlen === oldlen) {
        // 每次滑過都先清空畫布,再更新
        maxlen = newlen;
        beforecurr = touchPoints[maxlen - 1];
        setTimeout(ctx.clearRect(0, 0, canvasSize, canvasSize), 500);
        initCanvas();
        // 連接畫過的點,並劃上中心的小圓 add point

        for (let i = 0; i < position.length; i += 2) {
          ctx.beginPath();
          ctx.lineWidth = 6;
          ctx.strokeStyle = "white";
          ctx.arc(
            position[i],
            position[i + 1],
            (8 / 25) * radius,
            0,
            2 * Math.PI,
            false
          );

          ctx.fillStyle = "white";
          ctx.fill();
          ctx.moveTo(position[i], position[i + 1]);
          ctx.lineTo(position[i + 2], position[i + 3]);
          ctx.stroke();
        }

        // 跟著手指移動增加線條 add line
        ctx.beginPath();
        ctx.lineWidth = 6;
        ctx.strokeStyle = "white";
        ctx.moveTo(ninepoints[beforecurr]?.x, ninepoints[beforecurr]?.y);
        ctx.lineTo(point?.x, point?.y);
        ctx.stroke();
        ctx.closePath();
      }
    }
  };

  const handleEnd = (event) => {
    suceessCount++;
    //重置數據
    // position = [];
    initPosition = 0;
    // 設定密碼
    if (signal === 0) {
      if (touchPoints.length < 5) {
        suceessCount = 0;
        setTimeout(clearcanvas, 500);
      } else {
        if (suceessCount === 1) {
          //成功一次,清空重新初始化

          // setResult(touchPoints.join(""));
          setTimeout(clearcanvas, 500);
        } else if (suceessCount === 2) {
          //第二次
          if (result !== touchPoints.join("")) {
            // 如果兩次不一樣
            setResult(0);
            suceessCount = 0;
            setTimeout(clearcanvas, 500);
          } else if (result === touchPoints.join("")) {
            localStorage[0] = result;
            setResult(0);
            suceessCount = 0;
            setTimeout(clearcanvas, 500);
          }
        }
      }
    }
    // 驗證密碼
    else if (signal === 1) {
      suceessCount = 0;
      // result = touchPoints.join("");
      clearcanvas();
      if (localStorage[0] == null) {
        setTimeout(clearcanvas, 500);
      } else {
        if (localStorage[0] !== result) {
          setTimeout(clearcanvas, 500);
        } else if (localStorage[0] === result) {
        }
      }
    }
  };

  const isPointselect = (target) => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    let powx, powy, gap;
    // 點擊座標
    const point = getClientOffset(target);
    for (let i = 0; i < ninepoints.length; i++) {
      powx = Math.pow(ninepoints[i].x - point?.x, 2);
      powy = Math.pow(ninepoints[i].y - point?.y, 2);
      gap = Math.sqrt(powx + powy);
      if (gap < radius) {
        // 點擊到圓心距離小於半徑時,觸發事件
        if (touchPoints.indexOf(i) === -1) {
          // 值 i 是否存在陣列中
          // 沒有滑過的點保存
          // setTouchPoints((prev) => [...prev, i]);
          touchPoints.push(i);
          // 只點一下畫小圓
          ctx.beginPath();
          ctx.fillStyle = "white";
          ctx.arc(
            ninepoints[touchPoints[0]]?.x,
            ninepoints[touchPoints[0]]?.y,
            8,
            0,
            2 * Math.PI,
            false
          );
          ctx.fill();
        }
        break; //結束循環
      }
    }
  };

  /** 取得位置 */
  const getClientOffset = (event) => {
    let rect = canvasRef.current.getBoundingClientRect();
    const point = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top
    };
    return point;
  };

  /** 初始畫布 畫 9 個圓 */
  function initCanvas(w, h) {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    const arr = circleCenters(w, h);
    let len = arr.length;
    setNinepoints(arr);
    ctx.strokeStyle = "white";
    ctx.lineWidth = linewidth;
    for (let i = 0; i < len; i++) {
      ctx.beginPath();
      ctx.arc(arr[i].x, arr[i].y, radius, 0, Math.PI * 2, false);
      ctx.stroke();
    }
  }

  //計算圓心位置
  function circleCenters() {
    const points = [];
    for (let col = 0; col < 3; col++) {
      for (let row = 0; row < 3; row++) {
        const point = {
          x: wgap * 2 + (radius + linewidth) * (row * 2 + 1) + wgap * row,
          y: canvasSize / 6 + (radius + linewidth) * (col * 2 + 1) + hgap * col
        };
        points.push(point);
      }
    }
    return points;
  }

  /** 清空畫布 ,並重新繪製 */
  const clearcanvas = () => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    ctx.clearRect(0, 0, canvasSize, canvasSize);
    initCanvas();
  };

  return (
    <canvas
      ref={canvasRef}
      width={canvasSize}
      height={canvasSize}
      onTouchStart={handleStart}
      onTouchMove={handleMove}
      onTouchEnd={handleEnd}
    ></canvas>
  );
};

export default CanvasLock;

上一篇
Day 26 - 用canvas 做顏色遊戲
下一篇
Day 28 - 用 canvas 與 pdfjs 做文件簽名(上)
系列文
Canvas 小錦囊30

尚未有邦友留言

立即登入留言