โจ ๊ฐ์
ํ์ฌ์์ ์ํผ ์์ ์๋ํฐ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์, ์์ ํ๋ฆฌ๋ทฐ ์์ ๋ค์ํ ์์(์์ ํด๋ฆฝ, ํ ์คํธ, ์ด๋ฏธ์ง, ๋ฐฐ๊ฒฝ ์์ , ํ ํ๋ฆฟ, ํฐํธ, ๋ธ๋ฌ ๋ฐ์ค ๋ฑ)๋ฅผ ํ์ํ ์ ์์ด์ผ ํ๋ค.
์ด๋ฅผ ์ํด 9:16 ๋น์จ์ ์ปจํ ์ด๋๋ฅผ ๊ตฌ์ฑํ๊ณ , ๊ทธ ์์ ์์๋ฅผ ๋ฐฐ์นํ์๋ค. ์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋๋๊ทธ ์ค ๋๋กญ(DnD)๊ณผ ๋ฆฌ์ฌ์ด์ง์ด ๊ฐ๋ฅํด์ผ ํ์ผ๋ฉฐ, ํ ์คํธ๋ ์ปจํ ์ด๋ ๋ด์์ ์ง์ ์์ ํ ์ ์์ด์ผ ํ๋ค.
์๋ง ํฌ๋งท์ WebVTT ๋์ .ass(Advanced SubStation Alpha)๋ฅผ ์ฌ์ฉํ์๋ค. WebVTT๋ ๋จ์ํ ํ ์คํธ + ๊ธฐ์กด์ ์ธ ์คํ์ผ ํํ๋ง ๊ฐ๋ฅํ์ง๋ง, ASS๋ ๊ธ๊ผด, ์์, ์์น, ์ ๋๋ฉ์ด์ ๋ฑ ๋ค์ํ ์คํ์ผ์ ์ง์ํ๋ค. ๋ํ, ์๋ฒ์์ ์ฌ์ฉํ๋ ffmpeg์์ ํธํ์ฑ์ ๊ณ ๋ คํ์ฌ ASS๋ฅผ ์ฑํํ์๋ค.
์ด๋ฌํ ๊ธฐ๋ฅ๋ค์ ๊ณ ๋ คํด์ ๋ค์ํ ๋ ํผ๋ฐ์ค (capcut, canva ๋ฑ ์น ๊ธฐ๋ฐ์..) ์ฐธ๊ณ ํ๊ณ ๋ชจ๋ canvas๋ฅผ ๋ฒ ์ด์ค๋ก ๊ฐ์ง๊ณ ๊ฐ๋ค๋ ๊ฒ์ ํ์ธํ๋ค. ๊ทธ ๋ค์์ผ๋ก๋ ์ด๋ค ๋ฐฉ์์ผ๋ก canvas๋ฅผ ํ์ฉํด์ผํ ๊น ๊ณ ๋ฏผํ๋ค.
๋จผ์ , ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ ์ค๋ฅธ ๊ฒ์ canvas api๋ฅผ ํ์ฉํ๋ ๊ฒ์ด์๋ค. ์ด๋ค ๋ฐฉ์์ผ๋ก crop์ด ๊ตฌํ๋ ๊น ํ์ ๋, ๊ฐ์ฅ ์๋ฃ๊ฐ ๋ง์์ ๊ฒ์ด๋ผ ์๊ฐํ๊ณ ์ญ์๋ ๊ทธ๋ฌํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ด์ ์ ๋ํ๊ต ๋ ๋ฏธ๋์ด์ 2์ฐจ ์ฅ์์ ์ํ draw ๊ธฐ๋ฐ์ ์น ์๋ํฐ๋ฅผ ๋ง๋ค ๋ ์ฌ์ฉํ๋ fabric์ด ๋ ์ฌ๋๋ค. (๋ฒกํฐ ๊ธฐ๋ฐ์ด๋ผ ๋ถ๋๋ฌ์ด drawing์ด ๊ฐ๋ฅํ๋ค ๋ผ๋ ๊ธฐ์ต์ด ์ค์ณ๊ฐ๋ค) ๋๋ฌด ์ค๋ ์ ์ ํ๋ ํ๋ก์ ํธ์ด๊ธฐ๋ ํ๊ณ crop์ ๊ตฌํํด๋ณธ์ ์ด ์์์ง๋ง, DnD๋ ๋ฆฌ์ฌ์ด์ง ๋ฑ ๋๋จธ์ง ๊ธฐ๋ฅ๋ค์ ๋ํด ๊ตฌํ์ด ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ ์๊ณ ์์๋ค. ๋ง์ง๋ง์ผ๋ก๋ ์ด๋ฒ์ ์๋กญ๊ฒ ์ฐพ์ pixi.js์ธ๋ฐ ๊ฒ์ ์์ ํธ์ง ํด dor๋ฅผ ๋ง๋ ๋๋ฅดํ์์ ์ด ๊ฐ๋ฐ์ํฐํด์ ๋ณด๊ณ ์๊ฒ ๋์๊ณ ๊ณต์๋ฌธ์์ ์ฌ๋ฌ ์๋ฃ๋ค์ ๋ณด๊ณ ์ถฉ๋ถํ ๊ฒ์ฆํด๋ณผ๋งํ๋ค ํ๋จํ๋ค.
๊ฒฐ๊ตญ ์ฌ๋ฌ ๊ธฐ๋ฅ์ ์ถฉ์กฑํ ์ ์๋ ์๋ฒฝํ ์๋ฃจ์ ์ ์์๊ณ , ๋ค์ํ ๊ธฐ์ ์ ์คํํ ๋์ fabric.js๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๊ตฌํํ๊ฒ ๋์๋ค. ๋ค๋ฅธ ๋ฐฉ์์ ํฌ๊ธฐํ ์ด์ ๋ crop ๊ธฐ๋ฅ ๋๋ฌธ์ด ์๋๋ผ ๊ธฐํ ๋ค๋ฅธ ๊ธฐ๋ฅ๋ค์ ๊ตฌํํ๋ค๊ณ ํ์ ๋ fabric.js๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋นํด ์ ์ ์น ์์๊ธฐ ๋๋ฌธ์ด๋ค.
์ด ๊ธ์์๋ crop ๊ธฐ๋ฅ์ ์ค์ฌ์ผ๋ก ์ธ ๊ฐ์ง ๊ธฐ์ ์ ๊ทผ ๋ฐฉ์์ ๋น๊ตํ๊ณ , ์ต์ข ์ ํ๊น์ง์ ๊ณผ์ ์ ๊ณต์ ํ๋ค.
๐ฆ Crop ๋ฐ์ดํฐ ๊ตฌ์กฐ
์๋ฒ์ ์ ๋ฌํ๋ crop ์ ๋ณด๋ ๋ค์ ๋ค ๊ฐ์ง ํ๋๋ก ๊ตฌ์ฑ๋๋ค.
- start_x : crop์ ์์ํ x ์ขํ
- start_y : crop์ ์์ํ y ์ขํ
- width : cropํ ๊ฐ๋ก ๊ธธ์ด
- height : cropํ ์ธ๋ก ๊ธธ์ด
์ด ๊ฐ๋ค์ ์ ๋ ํฝ์ ๊ฐ์ด ์๋๋ผ, ์๋ณธ ์์ ํด์๋๋ฅผ ๊ธฐ์ค์ผ๋ก ํ 0~1 ์ฌ์ด์ ๋น์จ ๊ฐ์ด๋ค.
์๋ฅผ ๋ค์ด, ํด์๋๊ฐ 1920ร1080์ธ ์์์์ ์ ์ฒด ์์ญ์ ์ง์ ํ๋ ค๋ฉด width: 1, height: 1์ ์ค์ ํ๋ค.
๐งช ๊ธฐ์ ๋ณ Crop ๋ฐฉ์ ๋น๊ต
๊ฐ ๊ธฐ์ ๋ค์ ๋ํ ๊ธฐ๋ณธ์ ์ธ ๋ ๋๋ง ๋ฐฉ์๊ณผ ๋ฐฐ๊ฒฝ ์ง์์ ์ถํ ์์ธํ ๋ค๋ฃฐ ์์ ์ด๋ค.
ํญ๋ชฉ | Canvas API | Pixi.js | Fabric.js |
---|---|---|---|
๋ ๋๋ง ๋ฐฉ์ | Canvas 2D | WebGL + Canvas | Canvas 2D |
์ ๋ฐ crop ์กฐ์ | ์ขํ ๊ธฐ๋ฐ ์๋ ๊ณ์ฐ ํ์ | mask๋ก ๊ฐ๋ฅ | ๊ฐ์ฒด์ ์ง์ cropRect ์ฃผ์ |
1๏ธโฃ Canvas API ํ์ฉ
HTML <video> ํ๊ทธ์ Canvas API, requestAnimationFrame()์ ํ์ฉํ ๋ฐฉ์์ด๋ค.
"use client";
import { forwardRef, useCallback, useEffect, useRef } from "react";
type Props = {
crop: { start_x: number; start_y: number; width: number; height: number };
url: string;
videoEl: HTMLVideoElement | null;
metadata: {
width: number;
height: number;
};
};
export default forwardRef<HTMLVideoElement, Props>(function VanillaVideoCanvas(
props,
ref
) {
const { crop, url, videoEl, metadata } = props;
// ์๋ฒ์์ ์ ๊ณตํด์ฃผ๋ metadata ๊ฐ
const w = metadata.width;
const h = metadata.height;
const canvasRef = useRef<HTMLCanvasElement>(null);
const requestRef = useRef<number>(0);
const cropRef = useRef(crop);
useEffect(() => {
// ๋งค ํ๋ ์๋ง๋ค render ํธ์ถ ์ crop์ ๋ณํ๊ฐ ์ฑ๋ฅ์ ์ํฅ์ ์์ฃผ๊ฒ๋ ํ ๋น
cropRef.current = crop;
}, [crop]);
const render = useCallback(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (!canvas || !ctx || !videoEl || videoEl.paused || videoEl.ended) return;
const crop = cropRef.current;
// ์ค์ง์ ์ผ๋ก crop ์์ญ์ ๊ณ์ฐํ๊ณ ๊ทธ๋ฆฌ๋ ๋ถ๋ถ
// canvas์ ๊ทธ๋ฆด pixel์ ๊ณ์ฐ
ctx.drawImage(
videoEl,
w * crop.start_x, // ๋น๋์ค ์๋ณธ width * crop์ x ๊ฐ
h * crop.start_y, // ๋น๋์ค ์๋ณธ height * crop์ y ๊ฐ
w * crop.width, // ๋น๋์ค ์๋ณธ width * crop์ width ๊ฐ
h * crop.height, // ๋น๋์ค ์๋ณธ height * crop์ height ๊ฐ
0, // canvas์ ๊ทธ๋ฆด x ์ขํ
0, // canvas์ ๊ทธ๋ฆด y ์ขํ
canvas.width, // cropํ ์ด๋ฏธ์ง๋ฅผ canvas์ ๊ทธ๋ฆด ๋ width
canvas.height // cropํ ์ด๋ฏธ์ง๋ฅผ canvas์ ๊ทธ๋ฆด ๋ height
);
requestRef.current = requestAnimationFrame(render);
}, [w, h, videoEl]);
useEffect(() => {
if (!videoEl) return;
const onPlay = () => {
render();
};
videoEl.addEventListener("play", onPlay);
return () => {
// ํด๋ฆฐ์
ํจ์๋ฅผ ํตํด ์ด๋ฒคํธ ์ค์ฒฉ๊ณผ ๋ฉ๋ชจ๋ฆฌ ๋์๋ฅผ ๋ฐฉ์ง
videoEl.removeEventListener("play", onPlay);
cancelAnimationFrame(requestRef.current);
};
}, [render, videoEl]);
return (
<>
<div className="absolute left-0 top-0 z-10 h-full w-full">
<canvas
ref={canvasRef}
width={w}
height={h}
style={{ backgroundColor: "white" }}
/>
</div>
<video
ref={ref}
className="invisible absolute left-0 top-0 h-full"
width={w}
height={h}
src={url}
/>
</>
);
});
drawImage() ์ ๋ํด์ฌ
drawImage๋ HTML5 Canvas์ 2D ๋ ๋๋ง ์ปจํ ์คํธ์์ ์ด๋ฏธ์ง, ๋น๋์ค, ๋ค๋ฅธ ์บ๋ฒ์ค ๋ฑ์ ๊ทธ๋ฆด ๋ ์ฌ์ฉํ๋ ๋ฉ์๋์ด๋ค ๋๊ธฐ๋ ์ธ์์ ์์ ๋ฐ๋ผ ๋๊ธฐ๋ ์ธ์๋ค์ด ๋ค๋ฅด๋ค.
drawImage(image, dx, dy);
drawImage(image, dx, dy, dWidth, dHeight);
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
์ธ์ | ์ค๋ช |
---|---|
image | ์บ๋ฒ์ค์ ๊ทธ๋ฆด ์๋ณธ ์ด๋ฏธ์ง ์์. ์ฌ์ฉ ๊ฐ๋ฅํ ํ์ ์ HTMLImageElement, HTMLVideoElement, HTMLCanvasElement, ImageBitmap, OffscreenCanvas, VideoFrame ๋ฑ |
sx (optional) | ์๋ณธ ์ด๋ฏธ์ง์์ ์๋ผ๋ผ ์์ญ์ ์ผ์ชฝ ์ X ์ขํ |
sy (optional) | ์๋ณธ ์ด๋ฏธ์ง์์ ์๋ผ๋ผ ์์ญ์ ์ผ์ชฝ ์ Y ์ขํ |
sWidth (optional) | ์๋ณธ ์ด๋ฏธ์ง์์ ์๋ผ๋ผ ์์ญ์ ๋๋น |
sHeight (optional) | ์๋ณธ ์ด๋ฏธ์ง์์ ์๋ผ๋ผ ์์ญ์ ๋์ด |
dx | ์๋ผ๋ธ ์ด๋ฏธ์ง๋ฅผ ์บ๋ฒ์ค์ ๊ทธ๋ฆด ์์น์ X ์ขํ |
dy | ์๋ผ๋ธ ์ด๋ฏธ์ง๋ฅผ ์บ๋ฒ์ค์ ๊ทธ๋ฆด ์์น์ Y ์ขํ |
dWidth | ์๋ผ๋ธ ์ด๋ฏธ์ง๋ฅผ ์บ๋ฒ์ค์ ๊ทธ๋ฆด ๋์ ๋๋น (์ถ์/ํ๋ ํจ๊ณผ) |
dHeight | ์๋ผ๋ธ ์ด๋ฏธ์ง๋ฅผ ์บ๋ฒ์ค์ ๊ทธ๋ฆด ๋์ ๋์ด (์ถ์/ํ๋ ํจ๊ณผ) |
์์ฝํ์๋ฉด
๋ชฉ์ | ์ฌ์ฉ ์ |
---|---|
์ด๋ฏธ์ง/๋น๋์ค์ ์ผ๋ถ๋ฅผ ์๋ผ์ ๊ทธ๋ฆฌ๊ธฐ | sx, sy, sWidth, sHeight |
์์น ์กฐ์ | dx, dy |
๋ฆฌ์ฌ์ด์ง (์ค์ผ์ผ๋ง) | dWidth, dHeight |
โ ์ฅ์
- crop ๊ธฐ๋ฅ ์์ฒด๋ ๊ฐ๋จํ๊ฒ ๊ตฌํ ๊ฐ๋ฅํ๋ค.
- ๋ธ๋ผ์ฐ์ ํธํ์ฑ๊ณผ ์ฑ๋ฅ ๋ชจ๋ ์ํธํ๋ค.
โ ํ๊ณ
- ๋ค๋ฅธ ์์์ธ ํ ์คํธ๋ฅผ ๊ตฌํํ๋ค๊ณ ํ์ ๋ ๋ธ๋ผ์ฐ์ ์์ ํ์ํ๋ text๋ก๋ ํ ์คํธ ์ธ๊ณฝ์ , ์์ ๋ฑ ์คํ์ผ ํํ์ ์ ์ฝ์ด ์๋ค.
- ํ์๋ผ์ธ์ ์ถ๊ฐ๋ ์์์ ์์ ๋ฐ๋ผ ์ปจํ ์ด๋์ ์์์ ๋ ธ์ถ ์ฌ๋ถ๋ฅผ ์ ์ดํ๋ ๋ก์ง์ด ๋ณต์กํ๊ณ ์์น์ ๊ฒฝ์ฐ transform๋ฅผ ์ฌ์ฉํ๊ธฐ๋ ํ์ง๋ง ์ด ์ฌ์ด์ฆ์ ๊ฒฝ์ฐ reflow๊ฐ ๋น๋ฒํ๊ฒ ์ผ์ด๋๋ ๋ํ ๋ถ๋ช ์ฑ๋ฅ์ ์ํฅ์ด ์๋ค๊ณ ํ๋จํ๋ค.
๐ ํ๋จ
Crop๋ง ๋๊ณ ๋ณด๋ฉด ๊ฐ๋จํ๊ณ ๋น ๋ฅด์ง๋ง, ์ ์ฒด์ ์ธ ๊ธฐ๋ฅ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ์ง ๋ชปํ์ฌ ๋์ ํ์ง ์์๋ค.
2๏ธโฃ Pixi.js ํ์ฉ
Pixi.js๋ WebGL ๊ธฐ๋ฐ์ 2D ๋ ๋๋ง ์์ง์ ๋๋ค. Canvas๋ณด๋ค ๊ณ ์ฑ๋ฅ์ด๋ฉฐ, DOM์ด ์๋ GPU๋ฅผ ์ฌ์ฉํ๋ ๋ ๋ ํธ๋ฆฌ ๊ธฐ๋ฐ ๊ตฌ์กฐ์ด๋ค.
์ฆ, DOM์ ์ฌ์ฉํ์ง ์๊ณ , ๋ธ๋ผ์ฐ์ ์ canvas๋ฅผ ์ด์ฉํด ๋ชจ๋ ์์๋ฅผ ํ ์ค์ฒ ๊ธฐ๋ฐ์ผ๋ก ์ง์ ๋ ๋๋งํ๋ค.
์ฌ๊ธฐ์๋ v8 ๊ธฐ์ค์ผ๋ก ์์ฑ๋์๋ค.
// PIXI.Application์ Pixi.js์ ์ต์์ ์ปจํธ๋กค๋ฌ ๊ฐ์ฒด
// Pixi ํ๋ก์ ํธ๋ฅผ ์คํํ๊ธฐ ์ํด ํ์ํ ์์๋ค์ ํ ๋ฒ์ ๊ตฌ์ฑํ๊ณ ๊ด๋ฆฌํด์ฃผ๋ ๊ฐ์ฒด์ด๋ค.
const app = new PIXI.Application();
await app.init({
// options...
});
const video: HTMLVideoElement;
video.src = videoUrl;
// 1. Container ์์ฑ
// Container๋ DOM์ <div>์ฒ๋ผ ์์ ๋์คํ๋ ์ด ์ค๋ธ์ ํธ๋ฅผ ๊ด๋ฆฌํ๋ ๊ณ์ธต ๊ตฌ์กฐ ๊ฐ์ฒด
// ๋ด๋ถ์ ์ผ๋ก children ๋ฐฐ์ด์ ๊ฐ์ง๊ณ , ์ขํ ๋ณํ ๋ฐ ๋ ๋ ์์๋ฅผ ์กฐ์
const container = new PIXI.Container({
label: "video-container",
});
// 2. ์์ฑํ ์ปจํ
์ด๋๋ฅผ app์ ์ถ๊ฐ
// app.stage๋ Pixi์์ ๋ ๋๋ง ํธ๋ฆฌ์ ์ต์์ ๋ฃจํธ์ด๋ค.
// HTML์ <body>์ฒ๋ผ, Pixi์์ ๋ชจ๋ ์๊ฐ์ ์์(Sprite, Container, Text ๋ฑ)๋ ๋ฌด์กฐ๊ฑด stage ์๋์ ์กด์ฌํด์ผ ํ๋ฉด์ ๊ทธ๋ ค์ง๋ค.
// ์์ ๋
ธ๋๋ฅผ ํธ๋ฆฌ ํํ๋ก ๊ณ์ธต์ ์ผ๋ก ๊ด๋ฆฌ
app.stage.addChild(container);
const videoLoadPromise: Promise<HTMLVideoElement> = new Promise((resolve) => {
// ๋น๋์ค ์ฌ์์ด ์ค๋จ ์์ด ๊ฐ๋ฅํ์ง ํ์ธ
video.addEventListener(
"canplaythrough",
() => {
resolve(video);
},
{ once: true }
);
});
// 3. ๋น๋์ค๊ฐ ์์ ํ ๋ก๋๋ ๋๊น์ง ๊ธฐ๋ค๋ฆผ
const loadedVideo = await videoLoadPromise;
// 4. ๋ก๋๋ ํ์๋ง ํ
์ค์ฒ ์์ฑ
// ๋ด๋ถ์ ์ผ๋ก WebGL TEXTURE_2D๋ก ์
๋ก๋
const videoTexture = PIXI.Texture.from(video);
// 5. ๋ ๋๋ฅผ ์ํ sprite ์์ฑ
// sprite๋ ํด๋น ํ
์ค์ฒ๋ฅผ ์ฌ์ฉํ๋ ๋ ๋ ๊ฐ๋ฅํ ๊ฐ์ฒด
// ๋ง์น <img> ํ๊ทธ์ฒ๋ผ Pixi ๋ด๋ถ์์ ๊ทธ๋ ค์ง ์ ์๋ ๋
ธ๋
const videoSprite = new PIXI.Sprite(videoTexture);
๐ ๋ด๋ถ ๋์ ์์ฝ: โข HTMLVideoElement์ current frame โ canvas โ WebGL texture๋ก ๋ณต์ฌ โข GPU์์ ์ด ํ ์ค์ฒ๋ฅผ ์ํ๋งํ์ฌ Sprite๋ก ๋ ๋
// videoWidth์ videoHeight ์ด๋์ ์ด๋ป๊ฒ ๊ทธ๋ฆฌ๋๋์ ๋ฐ๋ผ ๊ธฐ์ค์ด ๋ฌ๋ผ์ง๋ค.
// e.g.) ์์ ์๋ณธ ํฌ๊ธฐ ๊ธฐ์ค์ด๋, ์ปจํ
์ด๋์ ํฌ๊ธฐ ๊ธฐ์ค์ด๋
const cropX = crop.start_x * videoWidth;
const cropY = crop.start_y * videoHeight;
const cropWidth = crop.width * videoWidth;
const cropHeight = crop.height * videoHeight;
const mask = new PIXI.Graphics().rect(cropX, cropY, cropWidth, cropHeight);
videoSprite.setMask({
mask,
inverse: false,
});
// Container๋ ๋ชจ๋ ์์์ ๋ ๋ ํธ๋ฆฌ๋ฅผ ๊ด๋ฆฌ
// ์ดํ app.stage.addChild(container) ์ฒ๋ผ ๋ฃจํธ ์คํ
์ด์ง์ ๋ถ์ด๋ฉด ์ต์ข
์ ์ผ๋ก ๋ ๋๋จ
container.addChild(videoSprite, mask);
- ๋น๋์ค ์์๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ๋ก๋ํ๊ณ , canplaythrough ์์ ์ ๊ธฐ์ค์ผ๋ก ํ ์ค์ฒ๋ฅผ ์์ฑํ๋ค.
- crop ์ขํ๋ normalized ๊ฐ(0~1) ๊ธฐ์ค์ผ๋ก ๊ณ์ฐํ์ฌ ๋ง์คํฌ๋ฅผ ๊ตฌ์ฑํ๋ค.
- setMask๋ v8์์ ์ ๊ณต๋๋ฉฐ, v7 ์ดํ์์๋ .mask ์์ฑ์ผ๋ก ์ค์ ํด์ผ ํ๋ค.
- ์ฌ์์ ๊ฒฝ์ฐ react๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ref์ ํ ๋นํ์ฌ ์ ์ด ๊ฐ๋ฅํ๋ค
- Sprite๋ฅผ ํตํด ์ ๊ทผํ๊ณ ์ถ๋ค๋ฉด videoSprite.texture.source.resource
- Texture๋ฅผ ํตํด ์ ๊ทผํ๊ณ ์ถ๋ค๋ฉด videoTexture.source.resource
- resource๋ GPU์ ์ ๋ก๋๋ ๋ฆฌ์์ค์ด๋ฉฐ. ์ฌ๊ธฐ์ ImageBimt / Canvas / Video ๋ฑ์์ ํฝ์ ์ ๊ฐ์ ธ์จ๋ค
โ๏ธ setMask์ ๋์ ๋ฐฉ์
setMask ๋?
โข PIXI.DisplayObject์ ๋ฉ์๋๋ก, ํด๋น ๊ฐ์ฒด์ ๋ง์คํฌ๋ฅผ ์ค์ ํ๋ค. โข mask๋ PIXI.Graphics ๋๋ PIXI.Sprite ๋ฑ ๋ง์คํฌ๋ก ์ฌ์ฉํ ์ ์๋ ๋ ๋๋ฌ ๋์ โข inverse๋ ๋ง์คํฌ ๋ฐ์ ์ฌ๋ถ๋ฅผ ์ง์ (๊ธฐ๋ณธ๊ฐ: false)
๋์ ๋ฐฉ์
- ๋ ๋๋ง ์ ๋ง์คํฌ๊ฐ ๋จผ์ ๋ ๋๋ง๋จ โข GPU ์คํ ์ค ๋ฒํผ(stencil buffer)๋ฅผ ์ฌ์ฉํด ๋ง์คํน ์ฒ๋ฆฌ
- ๋ง์คํฌ ์์ญ ์ธ์ ํฝ์ ์ ์ํ๊ฐ 0์ผ๋ก ์ฒ๋ฆฌ
- inverse: true์ผ ๊ฒฝ์ฐ โ ๋ง์คํฌ ๋ฐ์ด ๋ณด์ด๊ณ , ์์ด ์๋ฆผ
โ ์ฅ์
- GPU ๊ฐ์์ ํตํด ์ฑ๋ฅ์ด ๋ฐ์ด๋๋ค.
- crop์ mask ๊ฐ์ฒด๋ฅผ ํ์ฉํ์ฌ ๊ตฌํํ ์ ์๋ค.
โ ํ๊ณ
- ๋ธ๋ฌ ํจ๊ณผ ๊ตฌํ์ด ์ด๋ ต๋ค.
- pixi์ filter์๋ blur๊ฐ ์์ง๋ง ์ด๋ blur๋ฅผ ์ ์ฉํ ํฝ์
๋ค์ด ํ์ํ๊ณ ์ด๋์์ค๋ค์ด ์์ด์ผํจ์ ๋งํ๋ค.
- ์ฌ๊ธฐ์์ ๋ธ๋ฌ ํจ๊ณผ๋ ๋น ๋ฐ์ค์ ๋ธ๋ฌ๋ฅผ ํํํ์ฌ ์ํ๋ ์์ญ์ ์์นํจ์ ์๋ฏธ
- ์ด ๋ธ๋ฌ ์์ญ ์๋์ ๋ค์ํ ์์๋ค์ด ๊ฒน์นจ
- css์ backdrop-filter๋ฅผ ํตํด ๊ฐ๋ฅํ์ง๋ง ์ด๋ ์์๊ฐ ๋ ์ด์ด ๊ด๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅ ํ๋ค.
- pixi์ filter์๋ blur๊ฐ ์์ง๋ง ์ด๋ blur๋ฅผ ์ ์ฉํ ํฝ์
๋ค์ด ํ์ํ๊ณ ์ด๋์์ค๋ค์ด ์์ด์ผํจ์ ๋งํ๋ค.
- ํ
์คํธ ์
๋ ฅ ์ฒ๋ฆฌ์์ ์ค๋ฐ๊ฟ, ์ปค์ ์์น, ๋ฆฌ์ฌ์ด์ง ๋ฑ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
- ์ด๋ฅผ ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก pixi/ui ๊ฐ ์์์ง๋ง ํ๊ณ๊ฐ ์์๋ค.
- ์ฌ์ฉ์๊ฐ ๋ง๋ ์ปค์คํ ํ ์คํธ ์ ๋ ฅ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์์ง๋ง ์ฌ์ฉํ๊ธฐ์๋ ๋ฌด๋ฆฌ๊ฐ ์์๋ค.
- ์ด๋ฅผ ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก pixi/ui ๊ฐ ์์์ง๋ง ํ๊ณ๊ฐ ์์๋ค.
- ์ปค์คํฐ๋ง์ด์ง์ด ๊ฐ๋ฅํ๋๋ผ๋ ์์ ์ฑ ํ๋ณด๊ฐ ์ด๋ ต๋ค.
- WebGL ๊ธฐ๋ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฅผ ๋ค๋ฃจ๋ ๊ฒ์ด ์ฒ์์ด์๊ธฐ์ ๋์ ๋ฌ๋์ปค๋ธ๊ฐ ์๊ตฌ๋์๊ณ , ์ ๋๋ก ๋ค๋ฃจ๊ธฐ ์ํด์๋ ์๊ฐ์ด ํ์ํ ๊ฒ์ ๋ถ๊ฐํผ ํ๋ค. ๋๋ฐ์ดํธ๋ฅผ ๊ณ ๋ ค ํ๋ค๋ค๊ณ ํ๋จ.
๐ ํ๋จ
๋ ๋๋ง ์ฑ๋ฅ์ ์ฐ์ํ์ง๋ง, ์์ ํธ์ง๊ธฐ ์๊ตฌ์ฌํญ์ ๋ง๋ ๊ธฐ๋ฅ์ ์์ ์ ์ผ๋ก ์ ๊ณตํ๊ธฐ ์ด๋ ค์ ๋์ ํ์ง ์์๋ค. ๋น ๋ฅด๊ฒ ๊ธฐ๋ฅ ๊ฒ์ฆํ๋๋ผ ๋์ณค๋ WebGL ๊ฐ๋ ์ ๋ํด ์ ๋๋ก ํ์ตํด๋ณด๊ณ ์ถ๋ค๋ ์๊ฐ๋ ๋ค์๋ค.
3๏ธโฃ Fabric.js ํ์ฉ (์ต์ข ์ ํ)
fabric.js๋ canvas ๊ธฐ๋ฐ์ ๊ฐ์ฒด ๋ชจ๋ธ์ ์ ๊ณตํ๋ฉฐ, ๋ค์ํ ์์๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ์กฐ์ํ ์ ์๋ค.
fabric ์์ฒด์์๋ crop์ ๊ตฌํํ ์ ์๋ ๊ธฐ๋ฅ์ด ์์๊ธฐ์ ๋ ํผ๋ฐ์ค๋ฅผ ์ฐพ์ ์ปค์คํ ํ ํ, ์ด๋ฅผ TypeScript ํ๊ฒฝ์ ๋ง์ถฐ ๋ฆฌํฉํฐ๋งํ์๋ค.
ํด๋น ๋ ํผ๋ฐ์ค๋ fabric 1.7.1 ๋ฒ์ ์ด์ด์ ๋๋ฌด ์ค๋๋ ๋ฒ์ ์ด๊ธฐ๋ ํ๊ณ ํ์ฌ๋ 6.x ๋ฒ์ ์ ์ฌ์ฉํ๊ณ ์๋ค v6๋ก ๋์ด์ค๋ฉด์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ด ๋ฉ์๋๋ค์ ๋ณํ๊ฐ ๋๋ฌด ๋ง์๊ณ ๋ฐ์ด๋ธ ์ฝ๋ฉ์ ํตํด ์ต๋ํ ํ์ฌ ๋ฒ์ ์ ํธํ๋๋ class๋ฅผ ๊ตฌํํ๋ค.
๋ฌด๋ ค 8๋ ์ !
type FabricImageProps = fabric.ImageProps & {
cropRect: { x: number; y: number; w: number; h: number };
};
// CropVideo ํด๋์ค ์ ์
class CropVideo extends fabric.FabricImage {
static type = "crop-video";
cropRect?: { x: number; y: number; w: number; h: number };
constructor(
element: HTMLVideoElement | HTMLImageElement,
options?: FabricImageProps
) {
const defaultOpts = {
// ...๊ธฐ๋ณธ ์ต์
};
super(element as any, Object.assign({}, defaultOpts, options));
this.cropRect = options?.cropRect;
}
_draw(ctx: CanvasRenderingContext2D) {
const element = this.getElement() as HTMLVideoElement;
const c = this.cropRect;
const d = {
x: -this.width / 2,
y: -this.height / 2,
w: this.width,
h: this.height,
};
if (c) {
ctx.drawImage(element, c.x, c.y, c.w, c.h, d.x, d.y, d.w, d.h);
} else {
ctx.drawImage(element, d.x, d.y, d.w, d.h);
}
}
_render(ctx: CanvasRenderingContext2D) {
this._draw(ctx);
}
}
(fabric as any).CropVideo = CropVideo;
class CropVideo extends fabric.FabricImage {
static type: string;
cropRect?: { x: number; y: number; w: number; h: number };
constructor(element: HTMLVideoElement | HTMLImageElement, options?: any);
_draw(ctx: CanvasRenderingContext2D, w: number, h: number): void;
_render(ctx: CanvasRenderingContext2D): void;
}
// ======= ์ค์ ์ฌ์ฉ =======
// CropVideo ํด๋์ค ์ง์ ์ฌ์ฉ
const fabricVideo = new fabric.CropVideo(videoElement, {
// option
});
// ์ฑ๋ฅ ์ต์ ํ: ์ขํ ์
๋ฐ์ดํธ
fabricVideo.setCoords();
// ์บ๋ฒ์ค์ ๋น๋์ค ๊ฐ์ฒด ์ถ๊ฐ
canvas.add(fabricVideo);
โ ์ฅ์
- ํ ์คํธ, ์ด๋ฏธ์ง, ์์ ๋ฑ ๋ค์ํ ์์๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ์ ์ดํ ์ ์๋ค.
- ์์ ๊ฐ ๋ ์ด์ด ์ฐ์ ์์ ๋ฐ ์ ๋ ฌ ์ฒ๋ฆฌ๊ฐ ๋ช ํํ๋ค.
- ์ปค์คํฐ๋ง์ด์ง ๋ฐ ํ์ฅ์ฑ์ด ๋ฐ์ด๋๊ณ , ์ ์ง๋ณด์๋ ์ฉ์ดํ๋ค.
โ ํ๊ณ
- ํ์ต ๊ณก์ ์ด ์ฝ๊ฐ ์กด์ฌํ๋ค.
- ์ผ๋ถ ๊ธฐ๋ฅ์ ์ง์ ๊ตฌํํด์ผ ํ๋ค.
- fabric์์๋ ๋ธ๋ฌ ํจ๊ณผ๋ฅผ ๊ตฌํํ๋ ๋ฐ์ ๋ฌธ์ ๊ฐ ์์๋ค.
- ๋ ์ด์ด ๊ด๋ฆฌ๋ฅผ ์ด๋ป๊ฒ ํ ๊นํ๋ค๊ฐ ๋ธ๋ฌ์ ๊ฐ์ฒด๊ฐ DnD, ๋ฆฌ์ฌ์ด์ง ์ค์ผ ๋๋ fabric ๋ด์ ํ์ ๊ฐ๋ฅํ ๊ฐ์ฒด(e.g. ๋ ธ๋์ ๋ฐ์ค)๋ก ๊ฐ ์์๋ค ๊ฐ์ ๋ ์ด์ด ์ฐ์ ์์๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ์ด๋ฒคํธ๊ฐ ๋๋ฌ์ ๋๋ backdrop-filter ๊ฐ ์ ์ฉ๋ ๋ฐ์ค๋ฅผ ๋ ๋ํ๋๋ก ํ๋ค.
- ์๋ฒฝํ ๋์์ ์๋๊ธฐ์ ์ถํ ๊ฐ์ ์ด ํ์ํ๋ค.\
๐ ํ๋จ
๊ฐ์ฅ ์์ ์ ์ผ๋ก ํ๋ก์ ํธ ์๊ตฌ์ฌํญ์ ๋ง์กฑํ์ฌ ์ต์ข ์ ํํ ๋ฐฉ์์ด๋ค.
๐ ๋ง๋ฌด๋ฆฌ
์ธ ๊ฐ์ง ๊ธฐ์ ์ ์ง์ ๊ตฌํํ๊ณ ๋น๊ตํ๋ฉด์ crop ๊ธฐ๋ฅ๋ฟ ์๋๋ผ, ์ ์ฒด ์๋ํฐ์ ๊ตฌ์กฐ์ ํ์ฅ์ฑ์ ๊ณ ๋ คํ ์์ฌ๊ฒฐ์ ์ ํ๊ฒ ๋์๋ค.
๋จ์ํ ์ ๋ณด์ด๋ UI๋ฅผ ๋์ด์, ์ฌ๋ฌ ๋น๋์ค ์์ค ๊ธฐ๋ฐ์ ์๊ฐ ๋ฐ ์ฌ์ ์ ์ด, ํ ์คํธ ์ ๋ ฅ, ๊ฐ ์์๋ค์ ์คํ์ผ ์ปค์คํฐ๋ง์ด์ง, ๋ ์ด์ด ์ฒ๋ฆฌ ๋ฑ ๋ณตํฉ์ ์ธ ์๊ตฌ๋ฅผ ์ถฉ์กฑํ๋ ค๋ฉด ๋ง์ ์คํ๊ณผ ์ํ์ฐฉ์ค๊ฐ ํ์ํ๋ค.
์์ง๋ ๋ง์ ์ํ์ฐฉ์ค๋ฅผ ๊ฒช๋ ์ค์ด๋ค...