[JavaScript 30]Day19-Unreal Webcam Fun
今天要練習利用 JavaScript 來控制 webcam 記錄影像,並使用 canvas 進行拍照(影像截圖),以及製作濾境效果。
功能需求
- 驅動 webcam
- 製作拍照(影像截圖)功能
- 濾鏡效果
學習重點
- 取得鏡頭的權限:
navigator.mediaDevices.getUserMedia
- 顯示影像 :
video.srcObject = localMediaStream;
、video.play();
- 將影像繪製到 Canvas 畫布上 :
ctx.drawImage(video, 0, 0, width, height);
- 取得畫布上像素資訊 :
let pixels = ctx.getImageData(0, 0, width, height);
- 將像素資訊設定回畫布 :
ctx.putImageData(pixels, 0 , 0);
- 監聽 canplay 事件 :
video.addEventListener('canplay', paintToCanvas );
- 從 canvas 中取得截圖影像資訊 :
canvas.toDataURL('image/jpeg')
<a>
連結可加上 download 屬性 :.setAttribute('download', 'fileName');
觀察範例檔案初始狀態
初始畫面 :
左上角有一顆拍照按鈕,
以及許多可拖曳調整的 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 畫布上,並製作拍照功能
三、製作濾鏡效果 :
- 紅色特效 : redEffect
- rgb 分散特效 : rgbSplit
- 綠幕效果 : greenScreen
一、利用 JavaScript 驅動 webcam
1. 載入套件
由於我們要調用 getUserMedia()
這個 api,以取得使用者的相機權限,需要在以下這三種安全連線的情況下使用 :
- https
- file:///
- 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 物件,所以接著利用then
、catch
來處理。 - 接下來影片中的寫法 :
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 數值,讓畫面轉紅
2. rgb 分散特效 : rgbSplit
rgbSplit 則是將不同像素的數值向前或向後移動,來達到畫面中紅、藍、綠位置分散的效果。
還可以在套用 rgbSplit 效果後,設定 ctx.globalAlpha = 0.1;
來疊加透明度,製作出霧霧的、👻影幢幢的效果
3. 綠幕效果 : greenScreen
- 首先,取得當前頁面左上方 input 所調整的數值,存取於 levels 物件中。
- 接著跑迴圈去取得影像像素資訊 (pixels) 內的 rgba 數值
- 一一判斷,比對是否落在我們用 range input 所設定的最大、最小 rgba 數值 (剛剛已儲存在 levels 物件內) 區間,如果落在區間,就將該四個一組的最後數值(Alpha)設為 0。
就可以製做出綠幕效果 :
本次練習就到這邊結束囉~
由於使用到一些平常較少用到的影像、圖像、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);
參考資料
範例檔案下載 :
JavaScript 30 課程註冊連結(輸入 email 免費註冊) :
筆記參考 :