iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 19
1

以下動作建議在 localHost 操作,不然可能發生無法正常運作

成品連結:Webcam Fun操作前程式碼完成後程式碼HTML 程式碼CSS 程式碼

今天要做的作品是使用 web cam、加上濾鏡效果並提供使用者下載圖像。

今天要做的項目可以說是至今最困難的了,途中遇到很多瓶頸也看了影片才學會做出成品,但在那之前花了許多時間研究 Canvas 以及瀏覽器使用 web cam 的方法,使得看影片時有茅塞頓開的感覺,所以也鼓勵你先試著自己查資料做做看,做出成品後會非常有成就感!

我們一步一步開始吧!

播放 web cam 影片

首先要先取用 web cam 並播放至 HTML 中的 video tag,這段程式碼我是參考 MDN 文件的。使用 navigator.mediaDevices.getUserMedia(..) 時要傳入 video & audio 的參數(例如 true 或是 false 或是 video 的尺寸),並要使用 thencatch 指定成功與失敗時的動作

function getVideo() {
  navigator.mediaDevices.getUserMedia({video: true, audio: false} )
  .then((stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = function(e) {
      video.play();
    };
  })
  .catch(function(err) { console.log(err.name + ": " + err.message); }); // always check for errors at the end.
}

接著可以直接在全域執行,使開啟網頁時就使用 web cam

// global scope
getVideo();

將影片印至 Canvas

由於無法直接在 web cam 的內容無法直接儲存或操作,若要進一步使用需要先將相片/影片印至 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);  // 每 16 毫秒將攝影機畫面「印」至 canvas
  }, 16)
}

首先先將 canvas 寬、高設定成 video 的寬高(這裡已在 CSS 將 canvas 寬設定 100%,所以看起來很寬),並以每 16 毫秒的頻率將圖像印至 canvas,如果不用 setInteval(..),則只會是靜態的一張圖像

最後要綁定事件,在取得 web cam 使用權並在 video 播放時執行 paintToCanvas

video.addEventListener('canplay', paintToCanvas);  // 當影片可播放時執行

拍照功能

接下來要能「拍下」 canvas 的圖像並放在 strip tag 當中供使用者下載
這裡再細分成幾個子項目

  • 播放音效(HTML 已有 audio
  • 取得圖像
  • 新增至 .strip
    • 將圖像存至 a tag(當點擊 a 時下載圖像)、a 當中再包著 img
    • 新增至 .strip

播放音效

如同第一天做的,播放音效/影片前要先把時間設為 0,否則預設會播放完才播放第二次,這裡已預先把 click 事件綁定在 button

function takePhoto() {
  // 播放音效
  snap.currentTime = 0;
  snap.play();
}

取得圖像

要取得圖像要使用 canvas 的方法 toDataURL(..),這會 return 一個 data: 開頭的連結,我想要儲存成 png 檔案,所以寫成:

function takePhoto() {
  // 上略
  
  // 取得相圖像連結
  const data = canvas.toDataURL("image/png");
}

新增至 .strip

在 HTML 創建新的 a,並結連結設定成剛剛產生的連結

function takePhoto() {
  // 上略
  
  // 取得相圖像連結
  const data = canvas.toDataURL("image/png");
  const link = document.createElement('a');
  link.href = data;
}

建立 a 下載時的檔名,並在 a tag 當中放入圖片,並放到 .strip 當中

function takePhoto() {
  // 上略
  
  // 取得相圖像連結
  const data = canvas.toDataURL("image/png");
  const link = document.createElement('a');
  link.href = data;
  link.setAttribute('download', 'Handsome.png');  // 下載時的檔名
  link.innerHTML = `<img src="${link}" alt="handsome guy/girl"/>` // 在 a 當中新增 img
  strip.insertBefore(link, strip.firstChild);  // 最新的照片會在最前面,使用 appendChild 會放在最後面
}

這裡看到最後將 a 加進 .strip 的方法是 insertBefore,這能使新的圖像永遠在第一位;如要把新的項目放在最後則使用 appendChild

製作濾鏡

大致功能完成了,剩下濾鏡的部分了。這裡用的方法是 ctx.getImageData(..) 用這個方法可以取出相片每個像素的 RGB,我們就是要用這點來做調整

取得圖像 RGB

目的是在 canvas 顯示圖像時同步套用濾鏡,因此要在剛剛的 setInteval 印畫面後套用濾鏡

function paintToCanvas() { 
  // 上略
  
  return setInterval(() => {
    ctx.drawImage(video, 0, 0, width, height);  // 每 16 毫秒將攝影機畫面「印」至 canvas
    // 取得圖像資訊,imgData.data 會是一類陣列,imgData.data[0] => red, imgData.data[1] => green, imgData.data[2] => blue, imgData.data[3] => alpha 以此四個一組類推
    let pixels = ctx.getImageData(0, 0, width, height);
  }, 16)
}

pixels 中的 data 可以看到一個巨大的 array-like,這就是我們要的東西

如同上面註解所說,data 中第一項是紅色、第二項是綠色、第三項是藍色、第四項是透明度,以此四個一組類推

接下來使用迴圈改變 RGB 排列順序或值(在全域宣告)

function invertEffect(pixels) {
  for (let i = 0; i < pixels.data.length; i+=4) {
    pixels.data[i] = 255 - pixels.data[i];         // RED
    pixels.data[i + 1] = 255 - pixels.data[i + 1]; // GREEN
    pixels.data[i + 2] = 255 - pixels.data[i + 2]; // BLUE
    pixels.data[i + 3] = 255;
  }
  return pixels;
}

這個效果會反轉原本圖像的顏色排序(RGB 的值從 0~255),要注意 i 一次是加 4 而不是加 1

接著在 setInteval 中執行並使用 ctx.putImageData(..)覆寫原本的顏色

function paintToCanvas() { 
  // 上略
  
  return setInterval(() => {
    ctx.drawImage(video, 0, 0, width, height);  // 每 16 毫秒將攝影機畫面「印」至 canvas
    // 從 (0, 0) 開始複製,範圍為 canvas.width & canvas.height
    // 取得圖像資訊,imgData.data 會是一類陣列,imgData.data[0] => red, imgData.data[1] => green, imgData.data[2] => blue, imgData.data[3] => alpha 以此四個一組類推
    let pixels = ctx.getImageData(0, 0, width, height);
    // 加上濾鏡
    pixels = invertEffect(pixels);
    // 輸出至 canvas
    ctx.putImageData(pixels, 0, 0);  // 從 (0,0) 開始寫入
  }, 16)
}

接著再說明成品 RGB 分離的 function

function rgbSplit(pixels) {
  for (let i = 0; i < pixels.data.length; i+=4) {
    pixels.data[i - 150] = pixels.data[i];         // RED
    pixels.data[i + 500] = pixels.data[i + 1]; // GREEN
    pixels.data[i - 550] = pixels.data[i + 2]; // BLUE
  }
  return pixels;
}

與上一個 function invertEffect 相同,只是在操縱 RGB 的數值而已,這裡把當前的 R、G、B換成前/後的顏色。把 pixels = invertEffect(pixels); 換成 pixels = rgbSplit(pixels); 就可以看到效果啦!

Reference


上一篇
JS30 Day 18 - Adding Up Times with Reduce
下一篇
JS30 Day 20 - Speech Detection
系列文
一起挑戰 JavaScript 30 吧!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言