在今天的課題當中,我們要將鏡頭攝影機所取得的影像資料,轉到 canvas 的畫布上,再將 canvas 上的像素資料透過一些方法,將我們的圖片加上特效。[1]
實作連結-正常版
實作連結-幽靈版
實作連結-消失版-可調整上方的bar來變化消失程度哦!
首先利用 mediaDevices.getUserMedia()
方法,詢問使用者是否允許使用視訊或音訊的輸入設備,例如鏡頭或麥克風。許可的話,就回傳一 Promise
對象,之後將 MediaStream
作為此 Promise 的回傳函数参數。[2]
再來利用 window.URL.createObjectURL()
將輸入的資訊建立成一個帶有 URL 的 DOMString 以代表參數中所傳入的物件。[3]
最後將我們所建立的 <video>
元素來源,指向前面所建立的 URL 物件,就可以將我們的鏡頭錄到的影片於<video>
中播放出來:
function getVideo() {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(localMediaStream => {
video.src = window.URL.createObjectURL(localMediaStream);
video.play();
})
.catch(err => {
window.alert(`OH~ No camera!!!`);
});
};
再來要將取得的影像於 <canvas>
呈現,因此要來介紹下幾個 canvas image 的方法:
drawImage()
:此方法可以用來在畫布上畫影像。[4]ctx.drawImage(image, dx, dy);
ctx.drawImage(image, dx, dy, dWidth, dHeight);
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
getImageData()
:此方法會將目前矩形 canvas
元素上每個像素的數據以 RGBA
格式複製出來,然後回傳一個 ImageData
物件,此物件的 Data
屬性中為包含所有像素資料的陣列。[5]R - 紅 (0-255)
G - 綠 (0-255)
B - 藍 (0-255)
A - 透明色版 (0-255; 0 是透明的,255 是完全不透明)
[R1, G1, B1, A1, R2, G2, B2, A2,....., Rn, Gn, Bn, An]
putImageData()
:此方法能將已有的 ImageData 物件資料,繪製到 <canvas>
元素上。ctx.putImageData(imagedata, dx, dy);
imgData:指定的 ImageData 物件。
dx:定義 ImageData 物件左上角原點的X座標。
dy:定義 ImageData 物件左上角原點的Y座標。
toDataURL()
:此方法會回傳含有圖像和參數設置特定格式的 data URIs。(URI 統一資源標識符:是一個用於標識某一網際網路資源名稱的字符串。)首先在函式中,將 <canvas>
元素的寬、高設定與 <video>
元素的寬、高相同,避免 <canvas>
元素中印出來的圖形失去原本的比例。然後透過 setInterval()
方法,以每秒16毫秒執行一次將 <video>
元素目前的截圖,透過 drawImage()
方法繪製到 <canvas>
元素上。
function normalMode() {
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
let painting = setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
}, 16);
return painting;
};
在拍照按鈕觸發事件的函式當中,先將目前 <canvas>
元素的圖形利用 toDataURL()
方法,轉成我們想要的圖檔,然後將目前的 URL
指定給新產生的 <a>
元素,並賦予其 download
屬性,讓 <a>
元素被點擊的時候,就下載 URL
所指的資料:
function takePhoto() {
// 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.firsChild);
};
以上是最基礎的作法,了解之後再來我們要來玩弄一下取得的圖像。
在 setInterval()
方法中,透過重組 imageData
中的色素陣列,我們可以將取得的影像處理完之後,在印製到 <canvas>
中,如此一來就可以創造出特殊效果的影像。
let painting = setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
// take the pixels out
let pixels = ctx.getImageData(0, 0, width, height);
// 執行重組 imageData 中的色素陣列的函式
pixels = rgbSplit(pixels);
// 印製到 <canvas> 上
ctx.putImageData(pixels, 0, 0);
}, 16);
然後將陣列中的像素元素,以4個為一組,分別將 R、G、B、A 像素資訊依照我們想做出的特效排列,最後再回傳出來,就可以達成特效!
幽靈版:
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;
};
今天學到以下的方法:
結合以上的方法,我們將影像繪製在 canvas 元素上,並做出特殊鏡頭的效果,透過修改 imageData 中的像素資料,我們可以做出更多酷炫的濾鏡特效,就讓我們一起來發揮創意吧!