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

背景

前些日子當前端面試官,問了一個問題:「你瞭解過canvas嗎?」javascript

「這個我知道,我有作過DEMO,這個不難吧,看着它的api接口就能實現!」css

看着他這麼(蜜汁)自信,我決定深刻了解(爲難)一下他!html

「電商中大轉盤,九宮格,刮刮樂,如何使用canvas實現,講講你的思路?」前端

「二維碼的生成和掃碼識別如何實現?」java

「圖片的粒子爆炸效果呢?」android

「......」webpack


所以,打算寫一系列關於 canvas 的文章,探索學習提高本身的同時順便分享給你們。ios

二維碼的生成

二維碼的生成需藉助第三方庫,利用其算法對文本轉化成二維碼,並用 canvas 繪畫出來。利用 canvas.toDataURL('image/png') 獲取二維碼轉 base64 值,再將其賦值給 img 標籤的 src 屬性git

這裏我使用了一個庫,qrcodejs.github

可點擊 《Demo》 查看效果

使用方法以下:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Suporka Vue App</title>
    <style> .container { padding: 60px; margin: 0 auto; line-height: 50px; } input { display: inline-block; width: 200px; height: 32px; line-height: 1.5; padding: 4px 7px; font-size: 12px; border: 1px solid #dcdee2; border-radius: 4px; color: #515a6e; background-color: #fff; background-image: none; position: relative; cursor: text; transition: border 0.2s ease-in-out, background 0.2s ease-in-out, box-shadow 0.2s ease-in-out; } button { color: #fff; background-color: #19be6b; border-color: #19be6b; outline: 0; vertical-align: middle; line-height: 1.5; display: inline-block; font-weight: 400; text-align: center; -ms-touch-action: manipulation; touch-action: manipulation; cursor: pointer; background-image: none; border: 1px solid transparent; white-space: nowrap; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; padding: 5px 15px 6px; font-size: 12px; border-radius: 4px; transition: color 0.2s linear, background-color 0.2s linear, border 0.2s linear, box-shadow 0.2s linear; } #qrcode { margin-top: 20px; } </style>
  </head>
  <body>
    <div class="container">
      <input type="text" placeholder="請輸入您想轉化成二維碼的字符串" id="input" />
      <button onclick="creatQRcode();">一鍵生成</button>
      <div id="qrcode"></div>
    </div>
    <script src="https://zxpsuper.github.io/Demo/qrcode/qrcode-dev.js"></script>
    <script type="text/javascript"> var qrcode = null; function creatQRcode() { document.getElementById("qrcode").innerHTML = ""; qrcode = new QRCode(document.getElementById("qrcode"), { text: document.getElementById("input").value, width: 200, height: 200, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRCode.CorrectLevel.H }); } </script>
  </body>
</html>
複製代碼

options

  • 首參爲生成存放 img 標籤的父元素
  • 第二個參數爲 Object 類型的 options
屬性 類型 說明
text String 目標文本
width Number 圖片寬度
height Number 圖片高度
colorDark String 二維碼顏色
colorLight 默認QRCode.CorrectLevel.L 容錯級別,可設置爲:QRCode.CorrectLevel.L ,QRCode.CorrectLevel.M,QRCode.CorrectLevel.Q,QRCode.CorrectLevel.H

二維碼掃碼識別

這裏利用了一個庫 llqrcode.js, 使用 qrcode.decode() 對 id 爲 qr-canvascanvas 進行解碼.

先上 Demo項目源碼

咱們須要作的就是,調用設備的攝像頭(後置攝像頭優先),得到的畫面用 video 標籤實時顯示出來,再定時取畫面生成 canvas ,調用 qrcode.decode() 解密。

// variable.js
var gCtx = null; //canvas.ctx
var gCanvas = null; // qr-canvas
// var c = 0;
var stype = 0; // 識別流程 0未開始 1進行中 2已結束
var gUM = false;
var webkit = false;
var moz = false;
var v = null; // 存放視頻的變量
var scanCodeStart = false; // 開始掃碼
var mediaStreamTrack = null; // mediaStreamTrack 實現關閉攝像頭功能 mediaStreamTrack.stop()
var imghtml =
  '<div id="qrfile"><canvas id="out-canvas" width="320" height="240"></canvas>' +
  '<div id="imghelp">drag and drop a QRCode here' +
  "<br>or select a file" +
  '<input type="file" onchange="handleFiles(this.files)" id="upload-img"/>' +
  "</div>" +
  "</div>";

var vidhtml = '<video id="v" autoplay muted></video>';
複製代碼
// methods.js

function qrcodeScanLoad(width, height) {
  if (isCanvasSupported() && window.File && window.FileReader) {
    initCanvas(width, height);
    qrcode.callback = scanCodeCallback;
    document.getElementById("mainbody").style.display = "inline";
    setwebcam();
  } else {
    document.getElementById("mainbody").style.display = "inline";
    document.getElementById("mainbody").innerHTML =
      '<p id="mp1">QR code scanner for HTML5 capable browsers</p><br>' +
      '<br><p id="mp2">sorry your browser is not supported</p><br><br>';
  }
}

// 繪製遮罩層canvas
function setMask() {
  var canvas = document.querySelector("#scancode-mask");
  canvas.width =
    document.body.clientWidth > 1024 ? 1024 : document.body.clientWidth;
  canvas.height =
    document.body.clientWidth > 1024 ? 1136 : document.body.clientHeight;
  var ctx = canvas.getContext("2d");

  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.globalCompositeOperation = "destination-out";
  ctx.beginPath();
  let x1,
    y1,
    width = canvas.width * 0.6;
  x1 = (canvas.width - width) / 2;
  y1 = (canvas.height - width) / 2;
  ctx.fillRect(x1, y1, width, width);
  ctx.fill();
  ctx.save();

  ctx.globalCompositeOperation = "source-over";

  // 第二象限點
  ctx.moveTo(x1, y1 + 2);
  ctx.lineTo(x1 + 20, y1 + 2);
  ctx.moveTo(x1 + 2, y1);
  ctx.lineTo(x1 + 2, y1 + 20);

  // 第一象限點
  ctx.moveTo(x1 + width - 20, y1 + 2);
  ctx.lineTo(x1 + width, y1 + 2);
  ctx.moveTo(x1 + width - 2, y1 + 1);
  ctx.lineTo(x1 + width - 2, y1 + 20);

  // 第四象限點
  ctx.moveTo(x1 + width - 20, y1 + width - 2);
  ctx.lineTo(x1 + width, y1 + width - 2);
  ctx.moveTo(x1 + width - 2, y1 + width - 1);
  ctx.lineTo(x1 + width - 2, y1 + width - 20);

  // 第三象限點
  ctx.moveTo(x1 + 20, y1 + width - 2);
  ctx.lineTo(x1, y1 + width - 2);
  ctx.moveTo(x1 + 2, y1 + width - 2);
  ctx.lineTo(x1 + 2, y1 + width - 20);
  ctx.lineWidth = 4;
  ctx.strokeStyle = "green";
  ctx.stroke();
}
function setwebcam() {
  var options = true;
  if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
    try {
      navigator.mediaDevices.enumerateDevices().then(function(devices) {
        let video = [];
        devices.forEach(function(device) {
          if (device.kind === "videoinput") {
            video.push(device);
          }
        });
        // 調用設備的攝像頭,video[1] 爲後置攝像頭,或者label含有‘back’爲後置攝像頭
        if (video.length >= 2) {
          options = {
            deviceId: { exact: video[1].deviceId },
            facingMode: { exact: "environment" }
          };
        }
        video.forEach(item => {
          if (item.label.toLowerCase().search("back") > -1)
            options = {
              deviceId: { exact: device.deviceId },
              facingMode: { exact: "environment" }
            };
        });
        scanCodeStart = true;
        setwebcam2(options);
      });
    } catch (e) {
      console.error(e);
    }
  } else {
    console.log("no navigator.mediaDevices.enumerateDevices");
    setwebcam2(options);
  }
}

function setwebcam2(options) {
  if (stype == 1) {
    setTimeout(captureToCanvas, 500);
    return;
  }

  var n = navigator;
  document.getElementById("outdiv").innerHTML = vidhtml;
  v = document.getElementById("v");
  try {
    if (n.mediaDevices && n.mediaDevices.getUserMedia) {
      n.mediaDevices
        .getUserMedia({ video: options, audio: false })
        .then(function(stream) {
          success(stream);
        })
        .catch(function(error) {
          error(error);
        });
    } else if (n.getUserMedia) {
      webkit = true;
      n.getUserMedia({ video: options, audio: false }, success, error);
    } else if (n.webkitGetUserMedia) {
      webkit = true;
      n.webkitGetUserMedia({ video: options, audio: false }, success, error);
    }
  } catch (err) {
    console.log(err);
  }
  stype = 1;
  if (getSystem() === "ios") {
    alert("您的設備暫不支持實時掃碼,請上傳圖片識別!");
    return;
  }
  if (
    (n.mediaDevices && n.mediaDevices.getUserMedia) ||
    n.getUserMedia ||
    n.webkitGetUserMedia
  )
    setTimeout(captureToCanvas, 500);
  else {
    alert("您的設備暫不支持實時掃碼,請上傳圖片識別!");
  }
}

// 獲取操做系統
function getSystem() {
  var u = navigator.userAgent;
  var isAndroid = u.indexOf("Android") > -1 || u.indexOf("Linux") > -1; //g
  var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios終端
  if (isAndroid) {
    //這個是安卓操做系統
    return "android";
  } else if (isIOS) {
    //這個是ios操做系統
    return "ios";
  } else {
    return "other";
  }
}

// 選擇圖片上傳
function setimg($event) {
  qrcode.callback = scanCodeCallback;
  $event && $event.preventDefault();
  stype = 2;
  let file = document.getElementById("upload-img");
  file.click();
}

// 上傳成功的回調
function scanCodeCallback(a) {
  var html = htmlEntities(a);
  stype = 0;
  alert(html);
}

// 處理上傳文件識別
function handleFiles(f) {
  var o = [];

  for (var i = 0; i < f.length; i++) {
    var reader = new FileReader();
    reader.onload = (function(theFile) {
      return function(e) {
        gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
        qrcode.decode(e.target.result);
      };
    })(f[i]);
    reader.readAsDataURL(f[i]);
  }
}

function initCanvas(w, h) {
  gCanvas = document.getElementById("qr-canvas");
  gCanvas.style.width = w + "px";
  gCanvas.style.height = h + "px";
  gCanvas.width = w;
  gCanvas.height = h;
  gCtx = gCanvas.getContext("2d");
  gCtx.clearRect(0, 0, w, h);
}

// 畫面轉 canvas
function captureToCanvas() {
  if (stype != 1) return;
  if (gUM && scanCodeStart) {
    try {
      gCtx.drawImage(v, 0, 0);
      try {
        qrcode.decode(); // 默認爲id=qr-canvas的canvas轉成圖片的base64
      } catch (e) {
        console.log(e);
        setTimeout(captureToCanvas, 500);
      }
    } catch (e) {
      console.log(e);
      setTimeout(captureToCanvas, 500);
    }
  }
}

// 對特殊符號進行處理
function htmlEntities(str) {
  return String(str)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

// 判斷是否支持canvas
function isCanvasSupported() {
  var elem = document.createElement("canvas");
  return !!(elem.getContext && elem.getContext("2d"));
}

function success(stream) {
  // mediaStreamTrack 實現關閉攝像頭功能
  if (stream)
    mediaStreamTrack =
      typeof stream.stop === "function" ? stream : stream.getTracks()[0];

  v.srcObject = stream;
  if (scanCodeStart) {
    v.play();
    gUM = true;
    setTimeout(captureToCanvas, 500);
  } else {
  }
}

function error(error) {
  gUM = false;
  return;
}
複製代碼

其中 setMask 爲繪製遮罩層方法,qrcodeScanLoad 爲初始化加載方法。

<body>
    <div class="body">
      <div id="mainbody" style="display: inline;">
        <div id="outdiv" autoplay muted></div>
      </div>
      <canvas id="qr-canvas" width="800" height="600"></canvas>
      <canvas id="scancode-mask"></canvas>
      <div class="scancode-tips-group" id="scancode-tips-group">
        <span class="tips">將QR Code 放入框內,便可自動掃描</span>
        <div class="upload-my-code" onClick="setimg()">個人QR Code</div>
      </div>

      <div id="img-upload-container" style="display: none">
        <div id="qrfile">
          <canvas id="out-canvas" width="320" height="240"></canvas>
          <div id="imghelp">
            drag and drop a QRCode here
            <br />or select a file
            <input type="file" onchange="handleFiles(this.files)" id="upload-img" />
          </div>
        </div>
      </div>
    </div>
    <script src="./llqrcode.js"></script>
    <script src="./variable.js"></script>
    <script src="./methods.js"></script>
    <script> qrcodeScanLoad(320, 400); setMask(); // 對個人 QR Code 進行定位顯示 if (document.body.clientWidth < 1025) { document.getElementById("scancode-tips-group").style.top = (document.body.clientHeight - document.body.clientWidth) / 2 + document.body.clientWidth * 0.9 - 10 + "px"; } else { document.getElementById("scancode-tips-group").style.top = "720px"; } </script>
  </body>
複製代碼

這裏作了適配,當超過 1024 時,canvas 寬度就設爲 1024。詳細在代碼中已經作了註釋。

更多推薦

前端進階小書(advanced_front_end)

前端每日一題(daily-question)

webpack4 搭建 Vue 應用(createVue)

相關文章
相關標籤/搜索