1. 개요
웹캠을 만들고, 스크린 샷 기능, 화면상에 효과를 주는 기능을 만든다.
2. HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Get User Media Code Along!</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="photobooth">
<div class="controls">
<button onClick="takePhoto()">Take Photo</button>
</div>
<canvas class="photo"></canvas>
<video class="player"></video>
<div class="strip"></div>
</div>
<audio class="snap" src="./snap.mp3" hidden></audio>
<script src="scripts.js"></script>
</body>
</html>
3. 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); }
4. Javascript
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');
먼저 위와 같이 HTML 요소들을 선택한다. getContext 매소드는 캔버스의 드로잉 컨텍스트를 반환한다. 컨텍스트 타입 2d는 2차원 렌더링 컨텍스트를 나타낸다.
function getVideo() {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(localMediaStream => {
video.srcObject = localMediaStream;
video.play();
})
.catch(err => {
console.error(`OH NO!!!`, err);
});
}
getVideo();
navigator.mediaDevices는 읽기 전용 속성으로, 카메라, 마이크, 화면 공유와 같은, 현재 연결된 미디어 입력 장치에 접근할 수 있는 MediaDevices 객체를 반환한다. getUserMedia 매소드는 사용자에게 미디어 입력 장치 사용 권한을 요청하는데, 요청이 수락되면 MediaStream 객체를 Promise 형태로 가져온다. getUserMedia는 매개변수로 video와 audio를 가질 수 있는데, 여기서는 video만을 가져오기 위해 video에만 true를 설정하였다.
프로미스는 비동기 매소드의 실행 결과에 따라 then, catch, finally를 실행한다. 위 코드에서는 프로미스가 정상적으로 실행되었을 때, 즉 비동기 매소드인 getUserMedia가 정상적으로 실행되면, srcObject를 통해 video와 연결된 미디어의 소스 역할을 하는 객체를 반환하고 play한다. THMLMediaElement.srcObject는 THMLMediaElement와 연결된 미디어의 소스 역할을 하는 객체를 설정, 반환하는 프로퍼티이다.
즉, getUserMedia 매소드로 사용자의 비디오를 가져오고, 정상적으로 가져온 경우에 이 비디오를 HTML의 비디오 요소의 srcObject 속성으로 설정, 재생하는 것이다. 정상적으로 실행되지 않는 경우에는 에러를 콘솔창에 출력한다.
이 코드를 통해 웹캡이 실행되고 화면 우측 상단에 비디오가 재생된다.
function paintToCanvas() {
const width = video.videoWidth;
const height = video.videoHeight;
console.log(width, height);
canvas.width = width;
canvas.height = height;
return setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
let pixels = ctx.getImageData(0, 0, width, height);
// ↓↓↓↓↓ red ↓↓↓↓↓
pixels = redEffect(pixels);
// ↓↓↓↓↓ rgb ↓↓↓↓↓
pixels = rgbSplit(pixels);
// ↓↓↓↓↓ green ↓↓↓↓↓
pixels = greenScreen(pixels);
ctx.globalAlpha = 0.8;
ctx.putImageData(pixels, 0, 0);
}, 16);
}
video.addEventListener('canplay', paintToCanvas);
canplay 이벤트는 유저 에이전트가 미디어를 재생할 수 있을 때 발생된다. video에 canplay 이벤트가 발생하면, 즉 위의 getVideo 함수를 통해 미디어가 재생되면, paintToCanvas가 실행된다.
paintToCanvas는 먼저 캔버스의 높이와 너비를 비디오의 높이, 너비에 각각 맞춘다. 그 후 setInterval 매소드를 이용해 16ms의 간격을 두고 콜백함수를 실행시킨다. 콜백함수의 내용은 기본적으로 캔버스에 video의 이미지를 drawImage 매소드를 통해 옮기는 것이다. drawImage는 drawImage(image, dx, dy, dwidth, dheight) 형식으로 사용했다. image는 video의 이미지, dx, dy는 각가 x축, y축이 시작하는 좌표값으로, (0, 0)을 주었으므로 화면의 좌상단 끝이 된다. 너비와 높이는 위에서 지정한대로 width, height를 할당한다.
getImageData 매소드는 캔버스의 지정된 부분에 대한 기본 픽셀 데이터를 반환하는데, getImageData(sx, sy, sw, sh) 형태로 사용하여 데이터를 추출할 사각형의 왼쪽 위 시작점인 sx, sy, 너비와 높이인 sw, sh를 나타낸다. 위 코드에서는 0, 0에서 시작하여 너비와 높이만큼을 의미하게 되므로, pixels는 캔버스 전체에 대한 픽셀 데이터라고 볼 수 있다.
이를 매개변수로 이용하여 각각 redEffect, rgbSplit, greenScreen을 시행하고 pixels에 할당하도록 한다.
이렇게 변형시킨 이미지 데이터가 담긴 변수 pixels를, putImageData(image, dx, dy) 매소드의 첫 번째 매개변수로 할당함으로써 (0, 0)을 시작으로 캔버스에 표시되도록 한다. globalAlpha는 이미지의 투명도를 나타내는 것으로 0.0은 완전히 투명, 1.0은 완전히 불투명한 상태를 나타낸다.
function redEffect(pixels) {
for(let i = 0; i < pixels.data.length; i+=4) {
pixels.data[i + 0] = pixels.data[i + 0] + 200;
pixels.data[i + 1] = pixels.data[i + 1] - 100;
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 - 450] = pixels.data[i + 2];
}
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;
}
redEffect, rgbSplit, greenScreen은 위와 같이 pixels의 이미지 데이터를 변형하는 것으로 이루어진다.
function takePhoto() {
// play the sound
snap.currentTime = 0;
snap.play();
// take the data out of the camera
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.firstChild);
}
마지막으로 화면을 캡쳐하는 기능이다. take photo 버튼을 누르면 효과음과 함께 해당 시간의 화면이 캡쳐되도록 하였다. snap는 효과음이 담긴 오디오 파일이다. 오디오 요소의 currentTime = 0으로 하면 오디오 재생이 끝나지 않은 상태에서 다시 실행시켰을 때 처음부터 다시 재생된다. 드럼 킷을 참조하면 좋다.
toDataURL 매소드는 매개변수로 지정한 이미지 포맷에 따라, 캔버스에 이미지 데이터를 반환한다. data 변수를 설정하여 캔버스에 jpeg 이미지 데이터를 반환하도록 하고, link 변수로 HTML 문서에 a 태그를 만들도록 하였다. 이 a태그의 출처를 data로 하고, setAttribute로 a 태그의 속성을 download로 설정하여 클릭되면 이미지가 handsome 파일명으로 다운로드 되도록 한다. setAttribute(name, value) 매소드는 매개변수로 속성명(name)과 속성값(value)을 가진다. 또, 해당 a태그에는 innerHTML을 이용해 이미지를 미리 볼 수 있도록 image 태그를 삽입하고, 출처를 data로 설정한다.
insertBefore 매소드는 참조된 노드 앞에 특정 부모 노드의 자식 노드를 삽입한다. ParentNode.insertBefore(newNode, referenceNode) 형태로 사용되므로, link는 빈 div 태그인 strip을 부모로 하는 자식 노드가 되는 것이다. firstChild의 앞에 위치하도록 하였으므로, 사진이 찍힐 때마다 새 사진이 가장 앞에 오도록 표시된다.
위 함수는 HTML의 onClick으로 실행되는데, 이는 그다지 추천되는 방법은 아니다.
[Javascript] 22. Follow Along Link Highlighter (0) | 2021.05.05 |
---|---|
[Javascript] 20. Speech Detection (0) | 2021.05.04 |
[Javascript] 18. Adding Up Times with Reduce (0) | 2021.05.01 |
[Javascript] 17. Sort Without Articles (0) | 2021.04.30 |
[Javascript] 16. Mouse Move Shadow (0) | 2021.04.28 |
댓글 영역