以下動作建議在 localHost 操作,不然可能發生無法正常運作
成品連結:Webcam Fun、操作前程式碼、完成後程式碼、HTML 程式碼、CSS 程式碼
今天要做的作品是使用 web cam、加上濾鏡效果並提供使用者下載圖像。
今天要做的項目可以說是至今最困難的了,途中遇到很多瓶頸也看了影片才學會做出成品,但在那之前花了許多時間研究 Canvas 以及瀏覽器使用 web cam 的方法,使得看影片時有茅塞頓開的感覺,所以也鼓勵你先試著自己查資料做做看,做出成品後會非常有成就感!
我們一步一步開始吧!
首先要先取用 web cam 並播放至 HTML 中的 video
tag,這段程式碼我是參考 MDN 文件的。使用 navigator.mediaDevices.getUserMedia(..)
時要傳入 video & audio 的參數(例如 true
或是 false
或是 video 的尺寸),並要使用 then
與 catch
指定成功與失敗時的動作
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();
由於無法直接在 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 當中供使用者下載
這裡再細分成幾個子項目
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,我們就是要用這點來做調整
目的是在 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);
就可以看到效果啦!