Theme NexT works best with JavaScript enabled

ShunNien's Blog

不積跬步,無以致千里;不積小流,無以成江海。

0%

JS30紀錄 19-Webcam Fun

目標

練習操作 Webcam 攝像頭,將攝像頭影像投射到 Video 上,並利用 canvas 來擷取圖片,練習濾鏡的操作。
target

Demo | Github

處理步驟

前置作業

必須先安裝 node.js,之後按照以下的設定,設定 package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "gum",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "browser-sync start --server --files \"*.css, *.html, *.js\""
},
"author": "",
"license": "ISC",
"devDependencies": {
"browser-sync": "^2.12.5"
}
}

由於此篇主題需要伺服器模擬,所以使用了 Browsersync ,其啟動方式透過

1
npm start

或是

1
browser-sync start --server --files "*.css, *.html, *.js"

步驟 1.

啟動視訊裝置,並寫入 video tag 呈現出來

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const video = document.querySelector(".player");
const canvas = document.querySelector(".photo");
const ctx = canvas.getContext("2d");
/**
* 啟動 webcam
*/
function getVideo() {
// 取得user的視訊裝置
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
// 把回傳的 MediaStream 寫進 html 的 video tag 中並播放
.then(localMediaStream => {
//console.log(localMediaStream);
video.src = window.URL.createObjectURL(localMediaStream);
video.play();
})
.catch(err => {
console.error(`OH NO!!!`, err);
});
}

/**
* 將 webcam 拍攝影像繪製到 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);
}

getVideo();
video.addEventListener("canplay", paintToCanvas);

參考資料
Navigator.mediaDevices
MediaDevices.getUserMedia()
URL.createObjectURL()
canplay
CanvasRenderingContext2D.drawImage()

步驟 2.

拍照截圖按鍵觸發功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 攝像頭截圖
*/
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");
// 設置連結位置為轉圖檔後的base64位置
link.href = data;
// 設置連結為下載並設定預設下載檔名
link.setAttribute("download", "handsome");
link.innerHTML = `<img src="${data}" alt="Handsome Man" />`;
// strip.firsChild 永遠為 null 因為 strip 為 const 所以無法得到最新的資料
strip.insertBefore(link, null);
}

參考資料
Node.insertBefore()

步驟 3.

添加畫面與色彩分離功能

split pixels

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 畫面色彩分割
* @param {*} 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 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);
//console.log(pixels);

pixels = rgbSplit(pixels);

// 重置分割畫面
ctx.putImageData(pixels, 0, 0);
}, 16);
}

參考資料
getImageData()
putImageData()

步驟 4.

添加紅色濾鏡功能

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 紅色濾鏡效果
* @param {*} pixels
*/
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;
}

步驟 5.

綠幕功能效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
}

筆記與備註事項

HTML 部分

a tag attribute download

此屬性告知瀏覽器下載這個連結,假如屬性有設定資料,則為下載檔案的預設檔名。

JavaScript 部分

唯讀屬性,取得 MediaDevices 物件,連接視訊裝置,包含攝影鏡頭與麥克風。

MediaDevices.getUserMedia()

MediaDevices.getUserMedia()方法提示用户允许使用一个视频和/或一个音频输入设备,例如相机或屏幕共享和/或麦克风。如果用户给予许可,就返回一个Promise 对象,MediaStream对象作为此Promise对象的Resolved[成功]状态的回调函数参数,相应的,如果用户拒绝了许可,或者没有媒体可用的情况下,PermissionDeniedError 或者NotFoundError作为此Promise的Rejected[失败]状态的回调函数参数。注意,由于用户不会被要求必须作出允许或者拒绝的选择,所以返回的Promise对象可能既不会触发resolve 也不会触发 reject。
資料來源 - MDN

URL.createObjectURL()

靜態方法 URL.createObjectURL() 用於建立一個帶有URL的 DOMString 以代表參數中所傳入的物件. 該URL的生命週期與創造它的window中的 document一致. 這個新的物件URL 代表了所指定的 File 物件 或是 Blob 物件.
資料來源 - MDN

canplay

播放媒體檔案時候觸發

CanvasRenderingContext2D.drawImage()

Canvas 2D API 中的方法,在 Canvas 上繪製圖案

CanvasRenderingContext2D.getImageData()

返回一个ImageData对象,用来描述canvas区域隐含的像素数据,这个区域通过矩形表示,起始点为(sx, sy)、宽为sw、高为sh。
資料來源 - MDN

CanvasRenderingContext2D.putImageData()

CanvasRenderingContext2D.putImageData() 是 Canvas 2D API 将数据从已有的 ImageData 对象绘制到位图的方法。 如果提供了一个绘制过的矩形,则只绘制该矩形的像素。此方法不受画布转换矩阵的影响。
資料來源 - MDN

Node.insertBefore()

在目前元素 ( DOM ) 中的子節點中插入一個子元素

1
2
3
4
5
6
7
8
9
10
// Get a reference to the element in which we want to insert a new node
var parentElement = document.getElementById('parentElement');
// Get a reference to the first child
var theFirstChild = parentElement.firstChild;

// Create a new element
var newElement = document.createElement("div");

// Insert the new element before the first child
parentElement.insertBefore(newElement, theFirstChild);

setInterval()

需要每隔一段時間就重複執行的 function 就可以呼叫此方法。

.globalAlpha

Canvas 2D API 設定透明度的屬性,設定區間在 0.01.0 之間。預設值為 1.0

參考資料

歡迎關注我的其它發布渠道