fabric.js 와 svelte를 사용하여 이미지 에디터를 만들어 보았는데 그 과정을 정리할 필요성을 느끼게 되었다.
이 포스팅에서는 svelte는 제외한 typescript위주로 각각 기능의 제작과정을 정리할 예정이다.
이번 포스팅에서는 간단하게 input file change 또는 드래그 앤 드롭으로 에디터에 사용할 이미지를 캔버스에 넣어보도록하자
interface CustomCanvas extends fabric.Canvas {
imageSize?: {
width: number
height: number
}
cornerSize?: number
imagePath?: string
}
우선 fabric의 기본으로 정의된 member이외의 별도의 데이터를 추가하기 위해 type을 확장하여 타입을 정의하였다
fabric.js 기본 member ( http://fabricjs.com/docs/fabric.html )
JSDoc: Namespace: fabric
Source: Classes ActiveSelection BaseBrush Canvas Circle CircleBrush Color Ellipse Gradient Group Image Intersection IText Line Object Path Pattern PatternBrush PencilBrush Point Polygon Polyline Rect Shadow SprayBrush StaticCanvas Text Textbox Triangle Nam
fabricjs.com
그 후 html은 다음과 같이 입력해준다
<input type="file" onchange="mountFile(event)" />
<div id="canvas-container">
<canvas id="canvas" width="1280" height="720">
</div>
width는 1280 height는 720으로 지정하고(유튜브 사이즈 참고) 드래그앤 드롭을 구현하기 위해 container div에 id도 입력해주자
const canvas: CustomCanvas = new fabric.Canvas('canvas')
canvas를 정의된 CustomCanvas타입으로 선언해준다
const element: HTMLDivElement | null = document.querySelector('#canvas-container')
window.addEventListener('drop', (event) => {
const target = event.target as HTMLCanvasElement
if (target.tagName == 'CANVAS') {
event.preventDefault()
}
})
element?.addEventListener('drop', (event) => {
const file = event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files[0] : null
if (file) loadImage(file)
})
이후 드래그앤 드롭을 구현하기 위해 element를 선언한다.
객체의 존재여부를 ts는 알 수 없기때문에 타입을 HTMLDivElement | null으로 설정해준 뒤 선언해준다
window에서 drop이벤트를 받으면 항상 새창으로 이미지를 띄워버리기 때문에 드랍 대상이 canvas일 경우 event.preventDefault로 이벤트를 제한해준다
drag, drop 이벤트는 dataTransfer에 데이터가 보유된다
https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer
DataTransfer - Web APIs | MDN
The DataTransfer object is used to hold the data that is being dragged during a drag and drop operation. It may hold one or more data items, each of one or more data types. For more information about drag and drop, see HTML Drag and Drop API.
developer.mozilla.org
optional chaining으로 element에도 event listener를 추가해준뒤 dataTransfer에 첨부된 파일을 가져온다
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
Optional chaining (?.) - JavaScript | MDN
The optional chaining (?.) operator accesses an object's property or calls a function. If the object accessed or function called using this operator is undefined or null, the expression short circuits and evaluates to undefined instead of throwing an error
developer.mozilla.org
값이 있다면 file에 할당 후 loadImage 함수에 값을 넘겨주도록 하자
function mountFile (event: Event) {
const target = event.target as HTMLInputElement
const file = target.files ? target.files[0] : null
if (file) loadImage(file)
}
html input file의 onchage이벤트가 일어났을때 할당된 함수도 위와같이 추가해준다 이때는 drag와 다르게 event.target.files다
input change event때 타입을 선언하는게 조금 난감할 수도 있는데 다음 내용을 참고하여 타입을 지정해준다
https://github.com/microsoft/TypeScript/issues/31816
HTMLInputElement type "file" change event.target type (EventTarget) has no files. · Issue #31816 · microsoft/TypeScript
Is there an event type for the HTMLInputElement change event? Currently is automatically resolved as general Event > EventTarget. Which has no files property. see FileList. https://developer.moz...
github.com
불러온 파일을 canvas에 전달하기 위해서는 이미지로 만들어줄 필요성이 있는데 우선 new FileReader()로 base64 인코딩을 해준다
async function fileLoad(file: File) {
return new Promise<ProgressEvent<FileReader>>((resolve, reject) => {
const reader = new FileReader()
reader.onload = async (event) => resolve(event)
reader.onerror = (event) => reject(event)
reader.readAsDataURL(file)
})
}
promise는 FileReader를 반환하고 각각 reader가 onload또는 onerror가 발생했을때 해당 이벤트를resolve reject을 반환해준다
async function loadImage(image: File) {
const event = await fileLoad(image)
const imgObj = new Image()
if (event.target?.result && typeof event.target?.result == 'string') {
// fileLoad() 에서 전달받은 값을 할당해준다
imgObj.src = event.target.result
}
imgObj.onload = async () => {
// 이미지가 정상적으로 로드되었다면 캔버스초기화
canvas.clear()
canvas.imagePath = ''
canvas.imagePath = image.name
const img = new fabric.Image(imgObj)
if (img.width && img.height) {
// 캔버스 container의 크기가 너무 크지않게 조정(내부 이미지 사이즈는 원본유지하기위함)
canvasResize(img.width, img.height)
}
canvas.centerObject(img)
canvas.add(img)
}
imgObj.onerror = error => {
console.log(error)
alert('이미지를 로드할 수 없습니다')
}
}
비동기로 fileLoad() 함수를 호출하고 전달받은 event의 target.result를 new Image()의 src에 할당해준다
이때도 optional chaining으로 event.target.result가 있는지 체크해주고 typescript는 file reader의 result값을 FileReader.result: string | ArrayBuffer 으로 정의하고 있기 때문에 typeof string체크도 같이 해준다
이후 이미지가 onload되면 fabric.js의 clear()함수를 사용하여 캔버스를 클리어해주고 imagePath는 image의 name의 값으로 할당해준다(동일한 이름으로 다운로드하기위함)
이후 fabric.Image클래스의 constructor로 로드한 이미지를 할당해주고
// 객체
IObjectOptions.width?: number | undefined
IObjectOptions.height?: number | undefined
이미지의 width, height는 undefined일 수 있기 때문에 if문 체크를 넣어주도록하자
그 뒤 canvasResize함수에 이미지의 width와 height를 전달하고 centerObject함수로 가운데 정렬해준뒤 canvas에 추가해준다
이미지 로드실패시 예외처리는 onerror에서 해주도록한다
이미지 또는 canvas의 직접적인 width, height나 scaleX, scaleY를 변경하면 원본 사이즈도 동일하게 변경되므로 유지하여야한다
그렇다면 이미지가 너무 크다면 영역을 한참 벗어나버린다
아래 이미지는 3712x5568 크기의 이미지를 각각 조정한 결과값이다
이미지와 캔버스는 원본비율을 유지하고 크기만 제한해야하기 때문에 canvas를 감싸고있는 container와 canvas의 max-width, max-height를 조정하도록 하자
fabric.js는 canvas를 생성할때 <div class="canvas-container">, <canvas class="lower-canvas">, <canvas class="upper-canvas">세가지 element를 생성하기 때문에 세가지 모두 값을 지정해주도록 하자
추후 다른기능에도 사용할 수 있게 별도의 함수로 만들어주도록한다
function canvasResize (w:number, h:number) {
canvas.imageSize = {
width: w,
height: h,
}
let width = 0
let height = 0
if ((w >= 1280 || h >= 720)) {
// 둘중 하나가 1280 720보다 높은경우
if (w > h) {
// 가로가 더 넓은경우
width = 1280
height = h / (w / 1280)
canvas.cornerSize = 15 * w / 1280
} else {
// 세로가 더 넓은경우
width = w / (h / 720)
height = 720
canvas.cornerSize = 15 * h / 720
}
} else {
width = w
height = h
canvas.cornerSize = 15
}
canvas.setWidth(w)
canvas.setHeight(h)
canvasMaxWidth(width, height)
}
canvas에 이미지 원본사이즈를 저장해주고 canvas의 비율을 계산해준다
가로와 세로의 최대값은 최초 설정한 1280 or 720이고 계산식은 낮은값 / (높은값 / 최대값) 으로 해준다
높은값이 최대값의 n배라면 낮은값을 그만큼 나눠서 비율을 유지하기 위함이다
cornerSize는 높은값의 배율 * 15로 맞춰준다
캔버스의 사이즈는 이미지와 동일하게 맞춰주고 max-width와 max-height를 조정하는 함수에 값을 반환한다
function canvasMaxWidth (width:number, height: number) {
(document.querySelector('.lower-canvas') as HTMLCanvasElement).style.maxWidth = width + 'px';
(document.querySelector('.lower-canvas') as HTMLCanvasElement).style.maxHeight = height + 'px';
(document.querySelector('.upper-canvas') as HTMLCanvasElement).style.maxWidth = width + 'px';
(document.querySelector('.upper-canvas') as HTMLCanvasElement).style.maxHeight = height + 'px';
(document.querySelector('.canvas-container') as HTMLDivElement).style.maxWidth = width + 'px';
(document.querySelector('.canvas-container') as HTMLDivElement).style.maxHeight = height + 'px';
}
각각 element를 직접 지정하기위해 위와같이 element를 선택 정의 그리고 스타일 변경을 해준다
이로써 이미지에디터의 기초가되는 canvas에 이미지를 넣어주는 기능을 만들었다!
fabric.js Image Editor - (4) filter (0) | 2023.03.31 |
---|---|
fabric.js Image Editor - (3) Crop (0) | 2023.03.20 |
fabric.js Image Editor - (2) Shape (0) | 2023.03.19 |
댓글 영역