JS30 day19 Webcam Fun

Posted by Anthony Chao on 2019-11-20

JS30 day19 - Webcam Fun

作業內容

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

學到什麼

  • JS

    1. 如何拿到 live 的攝影畫面

    要使用攝影機有一個內建的 js 方法navigator.mediaDevices.getUserMedia,要注意的是他回傳的是 Promise 物件,詳細內容可以直接看下面參考資料
    除此之外要注意 localMediaStream 是一個物件,我們必須利用一些特定方法把這個物件轉成 url 才能使用,在這裡使用的方法是設定 video 的 srcObject 屬性,等於把這個物件設成 url 的型態

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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> 如果有這個屬性就會強迫下載

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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)
    }

    別人寫的相關紀錄
    有關超連結的 download 屬性

    3. 把畫面呈現在畫布上

    之前有說過,畫圖不是直接畫在 canvas 而是畫在他的 context 上,所以這邊選擇的節點是 ctx 不是 canvas
    drawImage() 裡的參數為 image, x, y, width, height,最後兩個參數是縮放參數,非必填
    getImageData() 回傳的值為 ImageDate 物件,它代表著這個 canvas 上某部分的像素,這個像素使用 debugger 抓出來會長的像下面那樣,每個數字分別代表 RGBA 的數值:第一個數字代表紅色,第二個是綠色,第三個是藍色,第四個是 alpha 值,第五個是紅色… 以此類推

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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 放回去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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)
    }

    drawImage()
    getImageDate()

    其實最後有個 greenScreen 的效果還沒摸透怎麼回事 XD,等之後更熟悉前端之後再回來看可能會好點!

參考資料:
https://github.com/wesbos/JavaScript30

code 內容:
HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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)




prevent_hack