캔버스에서 비디오 재생를 재생해야 하는 경우가 있다고 한다. 하지만 비디오 태그가 있는데 굳이 그래야할까...?
그 이유는 캔버스에서는 픽셀단위로 조정이 가능하기때문에 캔버스에서 비디오를 사용하면 비디오의 배경을 바꾼다던지 뒤섞는다던지 등이 가능하기 때문이다.
이를 이용하면 우리가 영상편집에서 보는것처럼 먼지처럼 사라지는 효과를 구현할 수 있다고 한다.
장점 | 단점 |
---|---|
javascript로 직접 조작할 수 있어 다양한 시각 효과를 적용할 수 있음, 필터 효과, 이미지 합성, 애니메이션 등 | 비디오 재생과 관련된 모든 기능을 직접 구현해야하기 때문에 개발의 복잡성이 높아짐 (재생 제어, 버퍼링, 오류처리 등) |
불필요한 기능을 제거할 수 있어 성능을 최적화 할 수 있음 | 비디오 및 오디오 코덱 지원이 없어 브라우저에 내장된 코덱에 의존해야함 |
브라우저 호환성에 좋음, 모든 최신 브라우저에서 지원되므로 플랫폼에 구애받지 않음 | 접근성을 고려하여 직접 구현해야하기때문에 접근성에 문제가 있음 |
따라서 비디오를 재생하는 것은 시각적 효과와 성능 최적화가 필요한 경우에 적합하지만, 단순히 비디오를 재생하기만 한다면 video태그를 사용하는것이 더 간단하다.
캔버스에 비디오를 실행하는 방법 비디오 태그를 이용하여 비디오를 연결해주어야한다.
비디오의 autoplay가 있을때 muted 옵션을 추가해야지 자동재생이되는데, 크롬에서는 소리가 나는 영상은 자동재생이 되지 않도록 정책을 설정해놓았다. 즉 비디오를 자동재생하려면 소리를 없애야한다.
사실 크롬의 설정에 들어가서 소리를 나게 할 수 있지만 모든 사용자에게 설정을 해줘야 한다....
자세한 내용은 자동재생 정책을 살펴보자
캔버스에서 비디오를 그리는 방식은 비디오 소스를 잡아놓고 requestAnimationFrame을 이용해서 이미지 대신에 비디오를 반복적으로 그리는 방식이라고 생각하면된다.
이미지 그릴때 load 이벤트를 사용한것처럼 비디오는 canplaythrough 이벤트를 사용하는데 이는 비디오 재생준비가 됬을때 이벤트를 발생한다고 생각하면 된다.
<!DOCTYPE html>
<html>
<head>
<title>Canvas</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
canvas {
background: #eee;
}
video {
position: absolute;
width: 0;
height: 0;
}
</style>
</head>
<body>
<h1>Video</h1>
<video class="video" src="../images/video.mp4" autoplay muted loop></video>
<canvas class="canvas" width="600" height="400"
>이 브라우저는 캔버스를 지원하지 않습니다.</canvas
>
<script>
const canvas = document.querySelector(".canvas");
const ctx = canvas.getContext("2d");
let canPlayState = false;
ctx.textAlign = "center";
ctx.fillText("비디오 로딩 중..", 300, 200);
const render = () => {
ctx.drawImage(videoElem, 0, 0, 600, 400);
requestAnimationFrame(render);
};
const videoElem = document.querySelector(".video");
videoElem.addEventListener("canplaythrough", render);
</script>
</body>
</html>
아래와 같이 비디오가 정상적으로 재생되는것을 확인할 수 있다.
사실 이정도는 html태그를 이용해서 텍스트를 표시해도 상관이 없다고 한다.
매 프레임마다 requestAnimationFrame을 이용해서 전체를 지우고 다시 그리는 방식이다.
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
context.font = "bold 50px serif";
context.fillStyle = "black";
const messages = [
{ time: 1, message: "1 ㅋㅋ", x: 100, y: 100 },
{ time: 3, message: "3 ㅎㅎ", x: 300, y: 300 },
{ time: 5, message: "5 ㅊㅊ", x: 400, y: 200 },
];
const videoElem = document.querySelector("video");
const render = () => {
context.drawImage(videoElem, 0, 0, 600, 400);
for (let i = 0; i < messages.length; i++) {
if (videoElem.currentTime > messages[i].time) {
context.fillText(messages[i].message, messages[i].x, messages[i].y);
}
}
requestAnimationFrame(render);
};
videoElem.addEventListener("canplaythrough", render);
결과물은 다음과 같다.
<!DOCTYPE html>
<html>
<head>
<title>Canvas</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
canvas {
background: #eee;
}
video {
position: absolute;
width: 0;
height: 0;
}
</style>
</head>
<body>
<h1>Video</h1>
<video class="video" src="../images/video.mp4" autoplay muted loop></video>
<canvas class="canvas" width="600" height="400"
>이 브라우저는 캔버스를 지원하지 않습니다.</canvas
>
<div class="btns">
<button class="btn" data-color="red">R</button>
<button class="btn" data-color="green">G</button>
<button class="btn" data-color="blue">B</button>
<button class="btn" data-color="">Reset</button>
</div>
<script>
// 색상 변환
const canvas = document.querySelector(".canvas");
const context = canvas.getContext("2d");
const videoElem = document.querySelector(".video");
const btnsElem = document.querySelector(".btns");
let imageData;
const particles = [];
let particle;
let colorValue;
let length;
const render = () => {
context.drawImage(videoElem, 0, 0, 600, 400);
// 이미지 데이터라는 각 픽셀의 색상정보를 가지고 있음
// 600x400이라 픽셀의 갯수는 24만개, 하지만 imageData안의 데이터는 96만개임
// 그이유는 4개의 배열의 값이 4개로 이루어져있기 때문에 rgba형식을 이루고있음
imageData = context.getImageData(0, 0, 600, 400);
length = imageData.data.length / 4;
for (let i = 0; i < length; i++) {
switch (colorValue) {
case "red":
imageData.data[i * 4 + 0] = 255;
break;
case "green":
imageData.data[i * 4 + 1] = 255;
break;
case "blue":
imageData.data[i * 4 + 2] = 255;
break;
}
}
context.putImageData(imageData, 0, 0);
requestAnimationFrame(render);
};
videoElem.addEventListener("canplaythrough", render);
btnsElem.addEventListener("click", (e) => {
colorValue = e.target.getAttribute("data-color");
});
</script>
</body>
</html>
위의 코드를 보면 가장 중요한 부분이 imageData부분인데 이부분을 조금 설명하고자 한다.
저 imageData를 찍어보면 다음과 같은 형식의 데이터가 출력된다.
여기서 data안에 있는 내용이 중요한데 이는 각 픽셀의 색상정보를 나타낸다. 현재 canvas사이즈는 600x400 사이즈라서 때문에 24만개의 픽셀로 이루어져 있다.
하지만 data의 개수가 96만개인데 이는 캔버스의 이미지는 한 픽셀을 rbga 형식으로 구성하고 있는데 쉽게 생격하면 4개의 배열이 하나의 픽셀이라고 생각하면 된다.
즉 [빨강, 초록, 파랑, 투명도]의 형식을 1개의 픽셀로 구성하고 있기때문에 96만개의 데이터를 표시한다. 쉽게 이해할 수 있도록 아래 그림을 첨부한다.
이를 확인하려면 투명도가 존재하는 배열의 3, 7, 11번째를 보면 계속 255가 반복되는 것을 확인 할 수있다.
결과물은 다음과 같다.
색상필터 추가시 putImageData의 dx, dy의 위치를 옮기면 어떻게 표시가 될지 궁금해서 테스트를 진행해보았는데 putImageData는 기존 캔버스 위에 캔버스를 추가로 올려 표시하는 식으로 구현이 되고있었다.
쉽게 생각하면 포토샵에서 원본 layer를 나눠서 작업한 다음 합성하는 식을 생각하면 될거같다. 이부분이 헷갈리면 투명 셀로판지에 작업을 하고 올린다고 생각하면 될듯하다..