iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0
Modern Web

JS30 x 鐵人30 x MDN doc系列 第 19

[Day19] - Webcam Fun(JS30 x 鐵人 30 x MDN)

  • 分享至 

  • xImage
  •  

做一個網頁攝像機,可套用濾鏡調整濾鏡色彩並提供下載功能

引用原作者圖片( https://javascript30.com/

看完 index-FINSIHED.html 該完成的成果後,覺得以自己現在的功力,要在一天內搞懂並做出相同的成品應該是不太可能,於是我決定照著影片的步驟動手做一遍,了解這個作品須應到到哪些 Web API 組合而成,並閱讀 MDN 看懂每一行程式碼的意義,那就足夠了。

實作開始

  1. 作者於scripts.js已經幫我們取得今天所需要操作到的各個節點
//  video元素
const video = document.querySelector(".player");
//  canvas畫布
const canvas = document.querySelector(".photo");
//  設定 canvas內容使用2d接口
const ctx = canvas.getContext("2d");
//  拍照\截圖暫存區
const strip = document.querySelector(".strip");
//  音檔節點(拍照聲)
const snap = document.querySelector(".snap");
  1. 獲取(相機)影像:那要如何讓網頁讀取我們的電腦攝像機影像呢,這就要使用到導航器 API(Navigator - Web APIs)的訪問裝置屬性(mediaDevices property)並調用獲取用戶影像方法(getUserMedia())來獲取相機的影像。因為影像涉及個人隱私,所以這個 API 只有在安全網域/環境( https:// wss://file:///localhost)下才允許存取,因此作者這邊才需要使用到browser-sync套件,並且瀏覽器都會跳出彈窗讓使用者確認,點擊允許後才會實際調用
getUserMedia();

function getUserMedia() {
  //    設定我們要獲取的影音條件
  const constraints = {
    audio: false, //  不調用聲音
    video: true, //  調用影像。另可設定影像分辨率、要調用前置、後置鏡頭等...
  };
  navigator.mediaDevices
    //  這個method會回傳一個promise 物件,因此要用promise寫法
    .getUserMedia(constraints)
    .then((mediaStream) => {
      //  於console中印出來 觀察看看
      console.log(mediaStream);
      //  將影片節點的來源物件屬性設定為這個影像流
      video.srcObject = mediaStream;
      //  控制影片節點開始播放
      video.play();
    })
    .catch((err) => console.log(err));
}
  1. 將影像傳入 canvas 畫布中顯示
  • 在將圖像傳進 canvas 畫布中顯示之前,要先確認攝像機是否已經啟動,換句話說就是<video>是不是已經有影像了?此時能利用到canplay event,當 video 元素已經載入了一定的來源能夠播放一小段時,就會觸發這個事件,另有canplaythrough event則代表影片來源已經全部加載完畢才會觸發,不會發生播到一半卡住等待加載的情形。
  • paintToCanvas函式中,把畫布的寬高套用影像來源寬高
  • 最後回傳一個 setInterval,每 16 毫秒(約 60FPS 的影格速率)以 video 中的影像更新至 canvas 畫布
//  將影片元素新增事件監聽器,
//  有影像才會執行paintToCanvas函式將畫面傳到canvas中
video.addEventListener("canplay", paintToCanvas);

function paintToCanvas() {
  const width = video.videoWidth;
  const height = video.videoHeight;
  canvas.width = width;
  canvas.height = height;

  return setInterval(() => {
    ctx.drawImage(video, 0, 0, width, height);
  }, 16);
}
  1. 拍照&下載功能:html 的<button>上已經幫我們預設綁上了點擊時要觸發的takephoto函式,因此我們以此命名,並寫其中的邏輯吧,
  • 播放快門聲:先把音檔節點的播放時間歸零,然後播放
  • 將 canvas 中影像轉為可供下載的<a><img/></a>結構:在 canvas 中要將圖像輸出成可供嵌入 or 下載的 url 有兩種方式,toDataURL()toBlob(),作者使用第一種方式,而我就以第二種方式來實作看看。
<button onClick="takePhoto()">Take Photo</button>
function takePhoto() {
  // 播放快門聲
  snap.currentTime = 0;
  snap.play();

  // 將canvas內容轉換為Blob物件
  canvas.toBlob((blob) => {
    const url = URL.createObjectURL(blob);
    //  <a>下載節點
    const link = document.createElement("a");
    link.href = url;
    link.setAttribute("download", "Selfie");
    //  <img>縮圖預覽
    const screenshot = document.createElement("img");
    screenshot.src = url;
    screenshot.alt = "screenshot";
    //  將<img>append在<a>中
    link.appendChild(screenshot);
    //  渲染於拍照\截圖暫存區最前面
    strip.insertBefore(link, strip.firstChild);
  }, "image/jpeg");
}

以上基本功能完成了,最後就是濾鏡功能。

首先我們先用getImageData()將整個 canvas 影像資料取出,並印於console中觀察

function paintToCanvas() {
  //          省略原程式碼...
  return setInterval(() => {
    ctx.drawImage(video, 0, 0, width, height);
    // take the pixels out
    let pixels = ctx.getImageData(0, 0, width, height);
    console.log(pixels);
  }, 16);
}
  • 可以發現 data 是一個長度多達1228800的陣列,這個數字哪裡來的呢?其實就是 width:640 x height:480 x 每個 pixel 的(RGBA)設定:4
  • 每 4 個數值一組 代表一個 pixel 的顏色(RGBA)

知道影像資料長怎樣之後,我們就可以寫一個函式,將取出的圖像依照你想要的樣子每個畫素更改之後再回傳顯示回 canvas 中。

function paintToCanvas() {
  const width = video.videoWidth;
  const height = video.videoHeight;
  canvas.width = width;
  canvas.height = height;

  return setInterval(() => {
    ctx.drawImage(video, 0, 0, width, height);
    // 從canvas取出圖像資料
    let pixels = ctx.getImageData(0, 0, width, height);

    // 修改各個畫素顏色,可替換成任意自己寫的濾鏡
    pixels = redEffect(pixels);

    // 將修改後的影像放回canvas中
    ctx.putImageData(pixels, 0, 0);
  }, 16);
}
function pinkEffect(pixels) {
  for (let i = 0; i < pixels.data.length; i += 4) {
    pixels.data[i + 0] = pixels.data[i + 0] + 255; // RED
    pixels.data[i + 1] = pixels.data[i + 1]; // GREEN
    pixels.data[i + 2] = pixels.data[i + 2] + 255; // Blue
  }
  return pixels;
}

另外作者有實作綠幕去背濾鏡greenScreen及霓虹燈濾鏡rgbSplit,原理都是更改特定的像素的值,詳細說明請看原作影片25:30 處。

👉Github Demo 頁面 👈

👉 好想工作室 15th 鐵人賽看板 👈

參考資料

  1. Javascript 30 官網
    https://javascript30.com/
  2. MDN 官網
    https://developer.mozilla.org/en-US/

上一篇
[Day18] - Adding Up Times with Reduce(JS30 x 鐵人 30 x MDN)
下一篇
[Day20] - Speech Detection(JS30 x 鐵人 30 x MDN)
系列文
JS30 x 鐵人30 x MDN doc30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言