Webcam with Frame Filter

3 분 소요


0. 들어가면서

브라우저 웹캠을 사용해 간단한 프레임을 입힌 이미지를 촬영해보았습니다. 코드에 대한 설명 후 코드펜(codepen)을 통해 실제로 동작하는 모습을 살펴보겠습니다. 코드가 긴 경우 가독성을 위해 설명이 필요한 부분에 주석을 작성하였습니다.

1. HTML Code

  • 프레임 필터를 변경할 수 있는 버튼들이 있습니다.
  • Take Photo 버튼을 누르면 이미지를 생성해 저장합니다.
  • 비디오 이미지 위에 위치할 프레임(frame) 캔버스가 존재합니다.
  • 비디오를 표시하기 위한 사진(photo) 캔버스가 존재합니다.
<div class="photobooth">
  <div class="buttons">
    <button onClick="takePhoto()">Take Photo</button>
    <button class="filter" onClick="changeFrame(1)">Frame-1</button>
    <button class="filter" onClick="changeFrame(2)">Frame-2</button>
    <button class="filter" onClick="removeFrame()">Remove Frame</button>
  </div>
  <div class="wrapper">
    <canvas class="frame"></canvas>
    <canvas class="photo"></canvas>
  </div>
  <div class="strip"></div>
</div>
<video class="player"></video>

2. CSS Code

html {
  box-sizing: border-box;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

html {
  font-size: 10px;
  background: #ffc600;
}

.photobooth {
  background: white;
  max-width: 100rem;
  margin: 2rem auto;
  border-radius: 2px;

  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.photobooth:after {
  content: "";
  display: block;
  clear: both;
}

.buttons {
  display: flex;
  gap: 10px;
}

.wrapper {
  position: relative;
  border: 1px solid lightgrey;
  border-radius: 5px;
}

.frame {
  width: 100%;
  position: absolute;
  z-index: 1;
  left: 0;
  top: 0;
  border-radius: 5px;
}

.photo {
  width: 100%;
  float: left;
  border-radius: 5px;
}

/* 실제 비디오 엘리먼트는 화면에서 숨김 */
.player {
  display: none;
}

/* 촬영한 카드가 수집되는 블럭 */
.strip {
  margin-top: 0.5rem;
  padding: 2rem;

  display: flex;
  justify-content: center;
  flex-wrap: wrap;
}
.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);
}

button {
  overflow: hidden;
  position: relative;
  margin: 10px auto;
  padding: 10px;

  outline: none;
  border-width: 0;
  border-radius: 2px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);

  color: #ecf0f1;
  background-color: #2ecc71;
  transition: background-color 0.3s;
}
button:hover,
button:focus {
  background-color: #27ae60;
}
button:before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;

  display: block;
  width: 0;
  padding-top: 0;
  border-radius: 100%;
  background-color: rgba(236, 240, 241, 0.3);
  -webkit-transform: translate(-50%, -50%);
  -moz-transform: translate(-50%, -50%);
  -ms-transform: translate(-50%, -50%);
  -o-transform: translate(-50%, -50%);
  transform: translate(-50%, -50%);
}
button:active:before {
  width: 120%;
  padding-top: 120%;
  transition: width 0.2s ease-out, padding-top 0.2s ease-out;
}

3. JavaScript Code

// 프레임 필터 타입을 설정
let frameTypeState = 0;

const video = document.querySelector(".player");
const frameCanvas = document.querySelector(".frame");
const photoCanvas = document.querySelector(".photo");
const frameCtx = frameCanvas.getContext("2d");
const photoCtx = photoCanvas.getContext("2d");
const strip = document.querySelector(".strip");

// 웹캠 사용에 대한 권한 신청
function getVedio() {
  navigator.mediaDevices
    .getUserMedia({ video: true, audio: false })
    .then((localMediaStream) => {
      // 권한에 대한 승인 후 비디오 실행
      video.srcObject = localMediaStream;
      video.play();
    })
    .catch((err) => console.error(err.message));
}

function paintToCanvas() {
  const width = video.videoWidth;
  const height = video.videoHeight;
  frameCanvas.width = width;
  frameCanvas.height = height;
  photoCanvas.width = width;
  photoCanvas.height = height;
  // 비디오 화면 모습을 캔버스에 그림
  // 60 프레임으로 동작하도록 16ms 마다 캔버스 다시 그림
  setInterval(() => {
    photoCtx.drawImage(
      video,
      0,
      0,
      photoCtx.canvas.width,
      photoCtx.canvas.height
    );
  }, 16);
}

function onloadToTakePhoto() {
  // 사진 캔버스에 필터 프레임 이미지를 오버라이드(override)
  photoCtx.drawImage(this, 0, 0, photoCtx.canvas.width, photoCtx.canvas.height);
  const data = photoCanvas.toDataURL("image/jpeg");
  const link = document.createElement("a");
  link.setAttribute("download", "hello");
  link.innerHTML = `<img src="${data}" alt="Hi" />`;
  // 아래 사진첩에 사진을 추가
  strip.insertBefore(link, strip.firstChild);
}

// 프레임 이미지 로딩
function loadFrame(onloadCallback) {
  const image = new Image();
  // CORS 문제 해결
  image.crossOrigin = "anonymous";
  // 프레임 이미지 로딩 콜백 등록
  image.onload = onloadCallback; 
  // 이미지 객체의 src 변경 시 onload 콜백 함수 실행
  if (frameTypeState === 1) {
    image.src = "https://junhyunny.github.io/images/webcam-filter-1.JPG";
  } else if (frameTypeState === 2) {
    image.src = "https://junhyunny.github.io/images/webcam-filter-2.JPG";
  }
}

// 사진 촬영
function takePhoto() {
  loadFrame(onloadToTakePhoto);
}

// 프레임 변경
function changeFrame(frameType) {
  removeFrame();
  frameTypeState = frameType;
  loadFrame(onload);
}

// 프레임 제거
function removeFrame() {
  frameTypeState = 0;
  photoCtx.clearRect(0, 0, frameCtx.canvas.width, frameCtx.canvas.height);
  frameCtx.clearRect(0, 0, frameCtx.canvas.width, frameCtx.canvas.height);
}

// 프레임 로드
function onload() {
  frameCtx.drawImage(this, 0, 0, frameCtx.canvas.width, frameCtx.canvas.height);
}

// 비디오 권한 신청
getVedio();

// 비디오 촬영 가능한 경우 사진 캔버스에 비디오 이미지를 갱신하는 콜백 함수 등록
video.addEventListener("canplay", paintToCanvas);

4. Result

See the Pen Webcam Frame Filters by Junhyunny (@Junhyunny) on CodePen.

REFERENCE

댓글남기기