
ํ์ฌ์์ ์ํผ ์์ ์๋ํฐ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์, ์์ ํ๋ฆฌ๋ทฐ ์์ ๋ค์ํ ์์(์์ ํด๋ฆฝ, ํ ์คํธ, ์ด๋ฏธ์ง, ๋ฐฐ๊ฒฝ ์์ , ํ ํ๋ฆฟ, ํฐํธ, ๋ธ๋ฌ ๋ฐ์ค ๋ฑ)๋ฅผ ํ์ํ ์ ์์ด์ผ ํ๋ค.
์ด๋ฅผ ์ํด 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 ์ ๋ณด๋ ๋ค์ ๋ค ๊ฐ์ง ํ๋๋ก ๊ตฌ์ฑ๋๋ค.
start_x : crop์ ์์ํ x ์ขํ start_y : crop์ ์์ํ y ์ขํwidth : cropํ ๊ฐ๋ก ๊ธธ์ดheight : cropํ ์ธ๋ก ๊ธธ์ด์ด ๊ฐ๋ค์ ์ ๋ ํฝ์ ๊ฐ์ด ์๋๋ผ, ์๋ณธ ์์ ํด์๋๋ฅผ ๊ธฐ์ค์ผ๋ก ํ 0~1 ์ฌ์ด์ ๋น์จ ๊ฐ์ด๋ค.
์๋ฅผ ๋ค์ด, ํด์๋๊ฐ 1920ร1080์ธ ์์์์ ์ ์ฒด ์์ญ์ ์ง์ ํ๋ ค๋ฉด width: 1, height: 1์ ์ค์ ํ๋ค.
๊ฐ ๊ธฐ์ ๋ค์ ๋ํ ๊ธฐ๋ณธ์ ์ธ ๋ ๋๋ง ๋ฐฉ์๊ณผ ๋ฐฐ๊ฒฝ ์ง์์ ์ถํ ์์ธํ ๋ค๋ฃฐ ์์ ์ด๋ค.
| ํญ๋ชฉ | Canvas API | Pixi.js | Fabric.js |
|---|---|---|---|
| ๋ ๋๋ง ๋ฐฉ์ | Canvas 2D | WebGL + Canvas | Canvas 2D |
| ์ ๋ฐ crop ์กฐ์ | ์ขํ ๊ธฐ๋ฐ ์๋ ๊ณ์ฐ ํ์ | mask๋ก ๊ฐ๋ฅ | ๊ฐ์ฒด์ ์ง์ cropRect ์ฃผ์ |
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๋ 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๋ง ๋๊ณ ๋ณด๋ฉด ๊ฐ๋จํ๊ณ ๋น ๋ฅด์ง๋ง, ์ ์ฒด์ ์ธ ๊ธฐ๋ฅ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ์ง ๋ชปํ์ฌ ๋์ ํ์ง ์์๋ค.
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);
videoSprite.texture.source.resourcevideoTexture.source.resourceresource๋ GPU์ ์
๋ก๋๋ ๋ฆฌ์์ค์ด๋ฉฐ. ์ฌ๊ธฐ์ ImageBimt / Canvas / Video ๋ฑ์์ ํฝ์
์ ๊ฐ์ ธ์จ๋คโข PIXI.DisplayObject์ ๋ฉ์๋๋ก, ํด๋น ๊ฐ์ฒด์ ๋ง์คํฌ๋ฅผ ์ค์ ํ๋ค.
โข mask๋ PIXI.Graphics ๋๋ PIXI.Sprite ๋ฑ ๋ง์คํฌ๋ก ์ฌ์ฉํ ์ ์๋ ๋ ๋๋ฌ ๋์
โข inverse๋ ๋ง์คํฌ ๋ฐ์ ์ฌ๋ถ๋ฅผ ์ง์ (๊ธฐ๋ณธ๊ฐ: false)
inverse: true์ผ ๊ฒฝ์ฐ โ ๋ง์คํฌ ๋ฐ์ด ๋ณด์ด๊ณ , ์์ด ์๋ฆผ๋ ๋๋ง ์ฑ๋ฅ์ ์ฐ์ํ์ง๋ง, ์์ ํธ์ง๊ธฐ ์๊ตฌ์ฌํญ์ ๋ง๋ ๊ธฐ๋ฅ์ ์์ ์ ์ผ๋ก ์ ๊ณตํ๊ธฐ ์ด๋ ค์ ๋์
ํ์ง ์์๋ค.
๋น ๋ฅด๊ฒ ๊ธฐ๋ฅ ๊ฒ์ฆํ๋๋ผ ๋์ณค๋ WebGL ๊ฐ๋
์ ๋ํด ์ ๋๋ก ํ์ตํด๋ณด๊ณ ์ถ๋ค๋ ์๊ฐ๋ ๋ค์๋ค.
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);
๊ฐ์ฅ ์์ ์ ์ผ๋ก ํ๋ก์ ํธ ์๊ตฌ์ฌํญ์ ๋ง์กฑํ์ฌ ์ต์ข ์ ํํ ๋ฐฉ์์ด๋ค.
์ธ ๊ฐ์ง ๊ธฐ์ ์ ์ง์ ๊ตฌํํ๊ณ ๋น๊ตํ๋ฉด์ crop ๊ธฐ๋ฅ๋ฟ ์๋๋ผ, ์ ์ฒด ์๋ํฐ์ ๊ตฌ์กฐ์ ํ์ฅ์ฑ์ ๊ณ ๋ คํ ์์ฌ๊ฒฐ์ ์ ํ๊ฒ ๋์๋ค.
๋จ์ํ ์ ๋ณด์ด๋ UI๋ฅผ ๋์ด์, ์ฌ๋ฌ ๋น๋์ค ์์ค ๊ธฐ๋ฐ์ ์๊ฐ ๋ฐ ์ฌ์ ์ ์ด, ํ ์คํธ ์ ๋ ฅ, ๊ฐ ์์๋ค์ ์คํ์ผ ์ปค์คํฐ๋ง์ด์ง, ๋ ์ด์ด ์ฒ๋ฆฌ ๋ฑ ๋ณตํฉ์ ์ธ ์๊ตฌ๋ฅผ ์ถฉ์กฑํ๋ ค๋ฉด ๋ง์ ์คํ๊ณผ ์ํ์ฐฉ์ค๊ฐ ํ์ํ๋ค.
์์ง๋ ๋ง์ ์ํ์ฐฉ์ค๋ฅผ ๊ฒช๋ ์ค์ด๋ค...