引用原作者圖片( https://javascript30.com/ )
看完 index-FINSIHED.html 該完成的成果後,覺得以自己現在的功力,要在一天內搞懂並做出相同的成品應該是不太可能,於是我決定照著影片的步驟動手做一遍,了解這個作品須應到到哪些 Web API 組合而成,並閱讀 MDN 看懂每一行程式碼的意義,那就足夠了。
實作開始
scripts.js
已經幫我們取得今天所需要操作到的各個節點// video元素
const video = document.querySelector(".player");
// canvas畫布
const canvas = document.querySelector(".photo");
// 設定 canvas內容使用2d接口
const ctx = canvas.getContext("2d");
// 拍照\截圖暫存區
const strip = document.querySelector(".strip");
// 音檔節點(拍照聲)
const snap = document.querySelector(".snap");
https://
wss://
、file:///
、localhost
)下才允許存取,因此作者這邊才需要使用到browser-sync
套件,並且瀏覽器都會跳出彈窗讓使用者確認,點擊允許後才會實際調用。getUserMedia();
function getUserMedia() {
// 設定我們要獲取的影音條件
const constraints = {
audio: false, // 不調用聲音
video: true, // 調用影像。另可設定影像分辨率、要調用前置、後置鏡頭等...
};
navigator.mediaDevices
// 這個method會回傳一個promise 物件,因此要用promise寫法
.getUserMedia(constraints)
.then((mediaStream) => {
// 於console中印出來 觀察看看
console.log(mediaStream);
// 將影片節點的來源物件屬性設定為這個影像流
video.srcObject = mediaStream;
// 控制影片節點開始播放
video.play();
})
.catch((err) => console.log(err));
}
<video>
是不是已經有影像了?此時能利用到canplay event,當 video 元素已經載入了一定的來源能夠播放一小段時,就會觸發這個事件,另有canplaythrough event則代表影片來源已經全部加載完畢才會觸發,不會發生播到一半卡住等待加載的情形。paintToCanvas
函式中,把畫布的寬高套用影像來源寬高// 將影片元素新增事件監聽器,
// 有影像才會執行paintToCanvas函式將畫面傳到canvas中
video.addEventListener("canplay", paintToCanvas);
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);
}
<button>
上已經幫我們預設綁上了點擊時要觸發的takephoto
函式,因此我們以此命名,並寫其中的邏輯吧,<a><img/></a>
結構:在 canvas 中要將圖像輸出成可供嵌入 or 下載的 url 有兩種方式,toDataURL()及toBlob(),作者使用第一種方式,而我就以第二種方式來實作看看。<button onClick="takePhoto()">Take Photo</button>
function takePhoto() {
// 播放快門聲
snap.currentTime = 0;
snap.play();
// 將canvas內容轉換為Blob物件
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
// <a>下載節點
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "Selfie");
// <img>縮圖預覽
const screenshot = document.createElement("img");
screenshot.src = url;
screenshot.alt = "screenshot";
// 將<img>append在<a>中
link.appendChild(screenshot);
// 渲染於拍照\截圖暫存區最前面
strip.insertBefore(link, strip.firstChild);
}, "image/jpeg");
}
以上基本功能完成了,最後就是濾鏡功能。
首先我們先用getImageData()將整個 canvas 影像資料取出,並印於console
中觀察
function paintToCanvas() {
// 省略原程式碼...
return setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
// take the pixels out
let pixels = ctx.getImageData(0, 0, width, height);
console.log(pixels);
}, 16);
}
知道影像資料長怎樣之後,我們就可以寫一個函式,將取出的圖像依照你想要的樣子每個畫素更改之後再回傳顯示回 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);
// 從canvas取出圖像資料
let pixels = ctx.getImageData(0, 0, width, height);
// 修改各個畫素顏色,可替換成任意自己寫的濾鏡
pixels = redEffect(pixels);
// 將修改後的影像放回canvas中
ctx.putImageData(pixels, 0, 0);
}, 16);
}
function pinkEffect(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i + 0] = pixels.data[i + 0] + 255; // RED
pixels.data[i + 1] = pixels.data[i + 1]; // GREEN
pixels.data[i + 2] = pixels.data[i + 2] + 255; // Blue
}
return pixels;
}
另外作者有實作綠幕去背濾鏡greenScreen
及霓虹燈濾鏡rgbSplit
,原理都是更改特定的像素的值,詳細說明請看原作影片25:30 處。