前言
JS 30 是由加拿大的全端工程師 Wes Bos 免費提供的 JavaScript 簡單應用課程,課程主打 No Frameworks、No Compilers、No Libraries、No Boilerplate 在30天的30部教學影片裡,建立30個JavaScript的有趣小東西。
另外,Wes Bos 也很無私地在 Github 上公開了所有 JS 30 課程的程式碼,有興趣的話可以去 fork 或下載。
本日目標
取得使用者的鏡頭影像,藉此實作出網頁版的相機以及影像濾鏡效果。

設定本地伺服器
這次的範例會需要取得鏡頭的存取權限,而在取得過程必須是透過https或是localhost這類的secure origin才行,以下我們透過npm install和npm start架起自己的 little server (localhost)。
如果發現下面的指令無效的話,表示沒有安裝Node.js,可以點這邊下載 LTS 的版本,都用預設的安裝就好。
- 
第一步 : 先移動到編輯檔案的工作目錄,接著輸入 npm install,它會幫你安裝一些套件
  
- 
第二步 : 輸入 npm start,它會去執行package.json裡的start,開始運行一個 little server。(紅色框是目前的網頁位置)
  
解析程式碼
HTML 部分
div(.controls) : 裡面放置用來拍照的按鈕和調整綠幕效果的按鈕。
canvas(.photo) : 用來放入鏡頭的影像,之後會搭配一些濾鏡。
video(.player) : 固定在右上角的小影像視窗。
div(.strip) : 用來放入擷取下來的圖片。
audio(.snap) : 放入按下拍照按鈕時要撥放的音效。
| 1 | <div class="photobooth"> | 

JS 部分
老樣子,我們要先取得所有要用到的元素。
.player 是小鏡頭畫面。
canvas 是可以套上濾鏡的大鏡頭畫面。
ctx 是canvas的渲染環境。
strip 是放照片的容器。
snap 是拍照的音效。
| 1 | const video = document.querySelector('.player'); | 
先來處理取得鏡頭影像的部分 :
navigator.mediaDevices.getUserMedia(),用來取得使用者的媒體裝置,因為我們只需要取得影像,所以指定{video:true,audio:false}不存取音訊,最後回傳一個Promise。
我們用then()繼續進行處理,因為不能直接將取得的MediaStream指定為video的來源(它看不懂QQ),還需要透過window.URL.createObjectURL()把MediaStream換成video可以理解的URL,然後video.play()開始播放影像。
到這邊,我們還需要用catch()來處理例外發生的狀況,當無法順利取得媒體裝置或是媒體裝置不存在,就會在 console 上印出錯誤訊息。
| 1 | function getVideo(){ | 
把影像放到畫布(canvas)上 :
為了讓畫布的大小和取得的影像大小一致,使用 video.videoWidth、video.videoHeight取得影像的寬、長,然後修改畫布的寬(canvas.width)、長(canvas.height)。
setInterval(),設定每隔一段時間就把影像更新到畫布(這邊是設定16毫秒)。
ctx.drawImage(),把影像畫到畫布(canvas)上。
video.addEventListener('canplay',paintToCanvas),如果影像現在是可以正常播放的話,就持續將影像輸出到畫布上。
| 1 | function paintToCanvas(){ | 
按下Take Photo時的快門音效、把擷取下來的圖片放入strip內供下載 :
snap.currentTime = 0,確保每一次都是從頭開始播放音效,snap.play()開始播放。
canvas.toDataURL('image/jpeg')將canvas上的影像轉換成image/jpeg格式的URL檔案連結。
const link = document.createElement('a'),在文件上新增一個<a>標籤。
link.href = data,將標籤連結指定為取得的影像圖檔連結。
link.setAttribute('download','handsome'),設定這個連結是可被點擊下載,同時下載的檔案名稱為handsome。
link.innerHTML = <img src="${data}" alt="handsome man" />,在<a>內部放入我們取得的圖片,現在只要點擊圖片就會把圖片下載下來。
strip.insertBefore(link,strip.firstChild),將整個<a><img></a>(擷取的影像圖)插入到.strip裡面並且是第一個位置。
| 1 | function takePhoto(){ | 
影像的濾鏡效果 :
不同的濾鏡效果其實只是將CanvasRenderingContext2D.getImageData()取得的畫布像素(pixels)數據以每四個為一組(R-G-B-A)的方式修改,然後再將修改完的像素用CanvasRenderingContext2D.putImageData()放回畫布。
1. 紅色濾鏡
增強紅色並減弱綠、藍色
| 1 | function redEffect(pixels){ | 
2. 色彩分離
實際上是讓色板產生位移 (這部分不太好理解@@)
| 1 | function rgbSplit(pixels){ | 
3. 綠幕
讓一定數值範圍內的 R、G、B 消失。
建立一個空物件levels,接著放入每一個 range 的名稱和數值。
以每四個為一組的方式取得畫布像素的 R、G、B 數值,接著把顏色進行比對,舉紅色為例,如果像素的 R 值處在 rmin 和 rmax 之間,就把該像素的透明度設定為0(在畫面上消失)。
| 1 | function greenScreen(pixels) { | 
套用濾鏡 :
(以套用紅色濾鏡為例,其他濾鏡也是同理)
| 1 | function paintToCanvas(){ | 
這次的練習是目前為止最複雜的,連我自己本身也花了非常多的時間查資料,但仍然沒有辦法把細節交代清楚。
所以大家可能要多花些精力在學習這次的課程內容上,大家加油~~~
補充資料:
Enabling the Microphone/Camera in Chrome for (Local) Unsecure Origins
Navigator
Navigator.mediaDevices
MediaDevices.getUserMedia()
URL.createObjectURL()
CanvasRenderingContext2D.drawImage()
HTMLCanvasElement.toDataURL()
Document.createElement()
HTMLMediaElement
Element.setAttribute()
Node.insertBefore()
debugger
CanvasRenderingContext2D.putImageData()
CanvasRenderingContext2D.getImageData()
範例網頁請點此
ps. 這次的網頁比較特殊,如果打開鏡頭仍然無法看到效果的話,可能就要自己 fork 程式碼到本地端測試~