JS30 day19 Webcam Fun
JS30 day19 - Webcam Fun
作業內容
今天的難度又突然提升了,主要內容在玩弄攝影機的畫面,並改造成類似靈異現象的效果,像下面這張圖這樣

學到什麼
-
JS
1. 如何拿到 live 的攝影畫面
要使用攝影機有一個內建的 js 方法
navigator.mediaDevices.getUserMedia,要注意的是他回傳的是 Promise 物件,詳細內容可以直接看下面參考資料 除此之外要注意localMediaStream是一個物件,我們必須利用一些特定方法把這個物件轉成 url 才能使用,在這裡使用的方法是設定 video 的srcObject屬性,等於把這個物件設成 url 的型態function getVideo(){ navigator.mediaDevices.getUserMedia({ video: true, audio: false }) .then(localMediaStream => { video.srcObject = localMediaStream; video.play() }) // 使用 catch 來捕捉例外 .catch(err => { console.log('Oh No!!', err); }) }srcObject navigator.mediaDevices.getUserMedia
2. 拍照功能:
toDataURL()是只有 HTMLCanvasElement 這個元素可以使用的方法,他可以把圖片轉換成Base64編碼,裡面可以放兩個值,第一個是圖像格式,預設為image/png,第二個是圖像品質,介於 0 - 1之間,兩個都是非必填參數 轉 Base64 通常用在小圖片,因為大圖片編碼起來會很長,好處是可以把編碼包到 HTML / CSS /JS 中,比較方便,壞處是圖片不會被快取起來 超連結有個屬性叫做 download<a href="..." download="檔名.txt">點選下載</a>如果有這個屬性就會強迫下載function takePhoto(){ // 發出聲音 snap.currentTime = 0 snap.play() // 拍照 // 利用toDataURL把canvas的內容轉為base64的圖檔資訊 const data = canvas.toDataURL('image/jpeg') const link = document.createElement('a') link.href = data link.setAttribute('download', 'hansome') link.textContent = 'Download Image' //insertBefore 在 Jquery 等同於 prepend strip.insertBefore(link, strip.firstChild) }3. 把畫面呈現在畫布上
之前有說過,畫圖不是直接畫在 canvas 而是畫在他的 context 上,所以這邊選擇的節點是 ctx 不是 canvas
drawImage()裡的參數為 image, x, y, width, height,最後兩個參數是縮放參數,非必填getImageData()回傳的值為 ImageDate 物件,它代表著這個 canvas 上某部分的像素,這個像素使用 debugger 抓出來會長的像下面那樣,每個數字分別代表 RGBA 的數值:第一個數字代表紅色,第二個是綠色,第三個是藍色,第四個是 alpha 值,第五個是紅色… 以此類推function paintToCanvas(){ const width = video.videoWidth const height = video.videoHeight canvas.width = width canvas.height = height return setInterval(function(){ ctx.drawImage(video, 0, 0, width, height) const pixels = ctx.getImageData(0, 0, width, height) console.log(pixels) debugger }, 16) }
了解 pixels 排列方式之後,就可以來改變輸出的影像了!
不管怎麼改造,其實做的順序都一樣:把 pixels 取出,讓他經過 function 變化之後再把改造完的 pixels 放回去function paintToCanvas(){ const width = video.videoWidth const height = video.videoHeight canvas.width = width canvas.height = height return setInterval(function(){ ctx.drawImage(video, 0, 0, width, height); //把 pixels 取出 let pixels = ctx.getImageData(0, 0, width, height); // 用 function redEffect處裡 // pixels = redEffect(pixels); // 用 function rgbSplit處裡 pixels = rgbSplit(pixels); // 用 globalAlpha 屬性改變透明度 ctx.globalAlpha = 0.8 // 放回去 ctx.putImageData(pixels, 0, 0); }, 16) }其實最後有個 greenScreen 的效果還沒摸透怎麼回事 XD,等之後更熟悉前端之後再回來看可能會好點!
參考資料: https://github.com/wesbos/JavaScript30
code 內容: 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="http://wesbos.com/demos/photobooth/snap.mp3" hidden></audio>
CSS:
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
html {
font-size: 10px;
background: #ffc600;
}
.photobooth {
background: white;
max-width: 150rem;
margin: 2rem auto;
border-radius: 2px;
}
/*clearfix*/
.photobooth:after {
content: '';
display: block;
clear: both;
}
.photo {
width: 100%;
float: left;
}
.player {
position: absolute;
top: 20px;
right: 20px;
width:200px;
}
/*
Strip!
*/
.strip {
padding: 2rem;
}
.strip img {
width: 100px;
overflow-x: scroll;
padding: 0.8rem 0.8rem 2.5rem 0.8rem;
box-shadow: 0 0 3px rgba(0,0,0,0.2);
background: white;
}
.strip a:nth-child(5n+1) img { transform: rotate(10deg); }
.strip a:nth-child(5n+2) img { transform: rotate(-2deg); }
.strip a:nth-child(5n+3) img { transform: rotate(8deg); }
.strip a:nth-child(5n+4) img { transform: rotate(-11deg); }
.strip a:nth-child(5n+5) img { transform: rotate(12deg); }
JS:
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');
function getVideo(){
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(localMediaStream => {
console.log(localMediaStream);
video.srcObject = localMediaStream;
video.play()
})
.catch(err => {
console.log('Oh No!!', err);
})
}
function paintToCanvas(){
const width = video.videoWidth
const height = video.videoHeight
canvas.width = width
canvas.height = height
return setInterval(function(){
ctx.drawImage(video, 0, 0, width, height);
//把 pixels 取出
let pixels = ctx.getImageData(0, 0, width, height);
// 用 function redEffect處裡
// pixels = redEffect(pixels);
// 用 function rgbSplit處裡
pixels = rgbSplit(pixels);
ctx.globalAlpha = 0.8
// 放回去
ctx.putImageData(pixels, 0, 0);
}, 16)
}
function takePhoto(){
// 發出聲音
snap.currentTime = 0
snap.play()
// 拍照
const data = canvas.toDataURL('image/jpeg')
const link = document.createElement('a')
link.href = data
link.setAttribute('download', 'hansome')
link.innerHTML= `<img src="${data}" alt="Handsome Man">`
strip.insertBefore(link, strip.firstChild)
}
function redEffect(pixels){
for(let i = 0; i < pixels.data.length; i+=4){
pixels.data[i + 0] = pixels.data[i + 0] + 100; //紅色
pixels.data[i + 1] = pixels.data[i + 1] - 50; //綠色
pixels.data[i + 2] = pixels.data[i + 2] * 0.5; //藍色
}
return pixels;
}
function rgbSplit(pixels){
for(let i = 0; i < pixels.data.length; i+=4){
pixels.data[i - 150] = pixels.data[i + 0]; //紅色
pixels.data[i + 100] = pixels.data[i + 1]; //綠色
pixels.data[i - 550] = pixels.data[i + 2]; //藍色
}
return pixels;
}
getVideo()
video.addEventListener('canplay', paintToCanvas)