[JavaScript 30]Day19-Unreal Webcam Fun

Ivy Ho
IvyCodeFive
Published in
15 min readOct 8, 2022

今天要練習利用 JavaScript 來控制 webcam 記錄影像,並使用 canvas 進行拍照(影像截圖),以及製作濾境效果。

課程影片截圖 (Wes 大大本人)

功能需求

  1. 驅動 webcam
  2. 製作拍照(影像截圖)功能
  3. 濾鏡效果

學習重點

觀察範例檔案初始狀態

初始畫面 :

左上角有一顆拍照按鈕,

以及許多可拖曳調整的 input (將會用來控制最後的 greenScreen 效果)

HTML :

JS :

預先取得 DOM 節點

// 原始影像的位置
const video = document.querySelector('.player');
// 呈現濾淨效果的位置(canvas畫布)
const canvas = document.querySelector('.photo');
const ctx = canvas.getContext('2d');
// 產生截圖照片的位置
const strip = document.querySelector('.strip');
// 點擊照相功能時產生的音效
const snap = document.querySelector('.snap');

(最後完成的完整 JS 程式碼,以及檔案下載、課程連結,將放在最下方)

步驟拆解

一、利用 JavaScript 驅動 webcam

二、將取得的影像貼到 canvas 畫布上,並製作拍照功能

三、製作濾鏡效果 :

  1. 紅色特效 : redEffect
  2. rgb 分散特效 : rgbSplit
  3. 綠幕效果 : greenScreen

一、利用 JavaScript 驅動 webcam

1. 載入套件

由於我們要調用 getUserMedia() 這個 api,以取得使用者的相機權限,需要在以下這三種安全連線的情況下使用 :

  1. https
  2. file:///
  3. localhost

而 localhost 也是屬於安全的來源,所以範例中是利用npm來安裝 browser-sync 套件,在本地端架設 localhost server。

下載範例後,在終端機執行以下指令 :

npm install // 安裝 package.json 內的套件
npm run start // 啟動 browser-sync 套件,開啟頁面。

2. 開始利用 JavaScript 驅動 webcam

  • 利用 navigator.mediaDevices.getUserMedia({ video : true, audio : false}) 取得鏡頭權限,會返回一個 promise 物件,所以接著利用 thencatch 來處理。
  • 接下來影片中的寫法 :
    video.src = window.URL.createObjectURL(localMediaStream);
    目前 Chorme、firefox 已經不支援,改使用以下寫法 :
    video.srcObject = localMediaStream; 將回傳的 localMediaStream 物件設為媒體來源。
  • 透過 video.play() 來播放影片

結束這個步驟後,就能在瀏覽器頁面右上方看見鏡頭所拍攝的畫面了 :

課程影片截圖

二、將取得的影像貼到 canvas 畫布上,並製作拍照功能

1. 將取得的影像貼到 canvas 畫布上

ctx.drawImage(video, 0, 0, width, height);

ctx.drawImage() 能將影像貼到畫布,括號中使用的參數分別是 :

video — 影像的來源

0 — 影像左上角的X座標

0 — 影像左上角的Y座標

width — 影像寬度

height — 影像高度

到這個步驟,已經能在下方 canvas 畫布中,以每 16 毫秒顯示鏡頭錄製的影像 :

課程影片截圖 (後面有隻可愛的狗在睡覺,一開始以為牠是假的🥺)

2. 製作拍照功能

  • html 預設範例中的 .snap 是一個快門聲音效
<audio class="snap" src="./snap.mp3" hidden></audio>
  • canvas.toDataURL(type, encoderOptions) 可以將圖片轉成 base64,回傳含有圖像和參數設置特定格式的 data URIs (預設為 PNG,這邊將它設定為 jpeg 格式)。
  • 每拍一張照片,就生成一個 <a> 連結,並將連結的 href 設定為剛剛取得的圖像 data 資訊。
  • 同時,圖像 data 資訊也作為 <img> 標籤的 src 屬性值,並使用 innerHTML 塞進 <a> 標籤內。
  • html 已在按鈕綁定 takePhoto 的 click 事件 :
    <button onClick="takePhoto()">Take Photo</button>
  • .strip 即為照片顯示的區域,範例檔已預先寫好了照片連結的樣式,所以按下按鈕拍完照片後,會出現一張張照片,顯示在下方。
  • 由於在 <a> 連結加入了 download 屬性,所以點擊照片連結,即可將該張 handsome.jpg下載下來。=> link.setAttribute('download', 'handsome');
課程影片截圖

三、製作濾鏡效果

原理 :

取得畫布上的像素資訊 => 改變像素資訊 => 將像素資訊設定回畫布

回到 paintToCanvas () 這個 function 內,在這邊做以上的三步驟

// 取得畫布上的像素資訊
let pixels = ctx.getImageData(0, 0, width, height);
// 改變像素資訊 (套用濾鏡效果的地方)
pixels = redEffect(pixels);
// 將像素資訊設定回畫布
ctx.putImageData(pixels, 0 , 0);
  • 利用 ctx.getImageData(0, 0, width, height) 取得像素資訊。
  • 製作不同濾鏡效果,即是撰寫不同 function (如下一個步驟要寫的 redEffect、rgbSplit 和 greenScreen)來改變像素資訊數值。
  • ctx.putImageData 可將像素資訊設定回 canvas 畫布中。

1. 紅色特效 : redEffect

剛剛利用 ctx.getImageData(0, 0, width, height) 所取得的像素資訊,會是一個非常大的陣列 — Uint8ClampedArray(8位無符號整型固定數組)。陣列裡面的數值會以 4 個為一組,分別代表 rgba

  • 紅(Red)
  • 綠(Green)
  • 藍(Blue)
  • Alpha通道(Alpha)

透過改變這些數值,即可做出不同濾鏡效果。

紅色特效 redEffect 的製作方法,即是調整 rgb 數值,讓畫面轉紅

課程影片截圖 - 套用 redEffect 效果

2. rgb 分散特效 : rgbSplit

rgbSplit 則是將不同像素的數值向前或向後移動,來達到畫面中紅、藍、綠位置分散的效果。

還可以在套用 rgbSplit 效果後,設定 ctx.globalAlpha = 0.1; 來疊加透明度,製作出霧霧的、👻影幢幢的效果

課程影片截圖 - rgbSplit 加上 ctx.globalAlpha = 0.1; 之前
課程影片截圖 - rgbSplit 加上 ctx.globalAlpha = 0.1; 之後

3. 綠幕效果 : greenScreen

頁面左上方的 range input
  • 首先,取得當前頁面左上方 input 所調整的數值,存取於 levels 物件中。
  • 接著跑迴圈去取得影像像素資訊 (pixels) 內的 rgba 數值
  • 一一判斷,比對是否落在我們用 range input 所設定的最大、最小 rgba 數值 (剛剛已儲存在 levels 物件內) 區間,如果落在區間,就將該四個一組的最後數值(Alpha)設為 0。

就可以製做出綠幕效果 :

課程影片截圖 — 套用 greenScreen 效果

本次練習就到這邊結束囉~

由於使用到一些平常較少用到的影像、圖像、CANVAS 相關的用法,感覺十分陌生,所以相較於其他練習,也花了較多的時間去熟悉、進入狀況。

但很開心又完成了一個有趣的 JS 小練習^^

完整 JS 程式碼

const video = document.querySelector('.player');
const canvas = document.querySelector('.photo');
const ctx = canvas.getContext('2d');
const strip = document.querySelector('.strip');
const snap = document.querySelector('.snap');
function getVideo() {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(localMediaStream => {
console.log(localMediaStream);
video.srcObject = localMediaStream;
video.play();
})
.catch(err => {
console.error(`OH NO!!!`, err);
});
}
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);
// take the pixels out
let pixels = ctx.getImageData(0, 0, width, height);
// mess with them
// pixels = redEffect(pixels);

pixels = rgbSplit(pixels);
// ctx.globalAlpha = 0.8;

// pixels = greenScreen(pixels);
// put them back
ctx.putImageData(pixels, 0, 0);
}, 16);
}
function takePhoto() {
// played the sound
snap.currentTime = 0;
snap.play();

// take the data out of the canvas
const data = canvas.toDataURL('image/jpeg');
const link = document.createElement('a');
link.href = data;
link.setAttribute('download', 'handsome');
link.innerHTML = `<img src="${data}" alt="Handsome Man" />`;
strip.insertBefore(link, strip.firstChild);
}
function redEffect(pixels) {
for (let i = 0; i < pixels.data.length; i+=4) {
pixels.data[i + 0] = pixels.data[i + 0] + 200; // RED
pixels.data[i + 1] = pixels.data[i + 1] - 50; // GREEN
pixels.data[i + 2] = pixels.data[i + 2] * 0.5; // Blue
}
return pixels;
}
function rgbSplit(pixels) {
for (let i = 0; i < pixels.data.length; i+=4) {
pixels.data[i - 150] = pixels.data[i + 0]; // RED
pixels.data[i + 500] = pixels.data[i + 1]; // GREEN
pixels.data[i - 550] = pixels.data[i + 2]; // Blue
}
return pixels;
}
function greenScreen(pixels) {
const levels = {};
document.querySelectorAll('.rgb input').forEach((input) => {
levels[input.name] = input.value;
});
for (i = 0; i < pixels.data.length; i = i + 4) {
red = pixels.data[i + 0];
green = pixels.data[i + 1];
blue = pixels.data[i + 2];
alpha = pixels.data[i + 3];
if (red >= levels.rmin
&& green >= levels.gmin
&& blue >= levels.bmin
&& red <= levels.rmax
&& green <= levels.gmax
&& blue <= levels.bmax) {
// take it out!
pixels.data[i + 3] = 0;
}
}
return pixels;
}
getVideo();video.addEventListener('canplay', paintToCanvas);

--

--

Ivy Ho
IvyCodeFive

"You don't have to be great to start, but you have to start to be great."