Skip to main content

[JS30] Day19: Unreal Webcam Fun

這一天的目標是要透過使用者的鏡頭、video 元素、Canvas 元素來完成,會看到自己的視訊,並可以加上不同的濾鏡,以及有截圖下載的功能。

要使用到使用者硬體設備,需要透過navigator這個介面指令,我們看到 MDN 上的解釋

Navigator

The Navigator interface represents the state and the identity of the user agent. It allows scripts to query it and to register themselves to carry on some activities.

這告訴我們navigator會有使用者的各項設施的狀態,我們透過 script 去要求他們提供資訊、進而去操作那些設施。

HTML

<div class="photobooth">
<div class="controls">
<button onClick="takePhoto()">Take Photo</button>
<div class="rgb">
<label for="rmin">Red Min:</label>
<input type="range" min="0" max="255" name="rmin" />
<label for="rmax">Red Max:</label>
<input type="range" min="0" max="255" name="rmax" />

<br />

<label for="gmin">Green Min:</label>
<input type="range" min="0" max="255" name="gmin" />
<label for="gmax">Green Max:</label>
<input type="range" min="0" max="255" name="gmax" />

<br />

<label for="bmin">Blue Min:</label>
<input type="range" min="0" max="255" name="bmin" />
<label for="bmax">Blue Max:</label>
<input type="range" min="0" max="255" name="bmax" />
</div>
</div>

<canvas class="photo"></canvas>
<video class="player"></video>
<div class="strip"></div>
</div>

<audio class="snap" src="./snap.mp3" hidden></audio>

JavaScript

首先,我們要先建立一個 server,而要取得使用者的錢鏡頭視訊資訊,必須要在secure origin的 server 才能執行,因此我們先要下載browser-sync簡單的 server

npm init -y // 建立npm專案
npm i --save-dev browser-sync

--save-dev: https://medium.com/itsems-frontend/nodejs-npm-dependencies-devdependencies-8934f641c8ef

再到package.json 加入啟動 server 的指令

"scripts": {
"start": "browser-sync start --server --files \"*.css, *.html, *.js\""
}, // npm run start

變數抓取,canvas 元素抓取後,透過getContext("2d")才可以進行 2d 的 API 操作。

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"); // 喀擦聲

在有了簡單的 server 之後,我們就來要求使用者提供視訊資訊吧

function getVideo() {
navigator.mediaDevices
.getUserMedia({ video: true, audio: false }) // 參數僅帶入影像資訊,getUserMedia會回傳promise
.then((localMediaStream) => {
console.log(localMediaStream);
video.srcObject = localMediaStream; // 放入video元素
video.play(); // 播放視訊
})
.catch((err) => {
console.error(`OH NO!!!`, err);
});
}

getVideo();

canvas

如此一來,我們就成功在螢幕上顯示出我們的前鏡頭視訊,接下來要透過 canvas 的 API 把視訊放到 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);
}

video.addEventListener("canplay", paintToCanvas); // paintToCanvas will trigger when the video inits

透過getImageDataputImageData進行調色

// take the pixels out
let pixels = ctx.getImageData(0, 0, width, height);

// mess with them
//
// pixels = redEffect(pixels);

// pixels = rgbSplit(pixels);

// ghosting
// ctx.globalAlpha = 0.1;

// pixels = greenScreen(pixels);

// put them back
ctx.putImageData(pixels, 0, 0);

定義調色的函式

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) {
// hold minimum and maximum rgb;
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! By setting alpha, the fourth pixel to 0!
pixels.data[i + 3] = 0;
}
}

截圖函式(takePhoto)

<button onClick="takePhoto()">Take Photo</button>
function takePhoto() {
// play 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"); // 下載的檔名為handsome
link.innerHTML = `<img src="${data}" alt="Handsome Man" />`; // data-url可以被放入img的src屬性之中
strip.insertBefore(link, strip.firstChild);
}