Junhyunny's Devlogs

글 검색

기술 글 제목, 본문, 카테고리에서 검색합니다.

    이전 화면으로

    프레임 필터가 있는 웹캠 구현

    • html
    • css
    • javascript
    이 글의 목차

    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

    CodePen에서 예제 열기

    KEEP READING

    DISCUSSION

    댓글