fabric.js로 이미지 에디터 만들기

milmil·2023년 10월 8일
1

리액트와 나

목록 보기
5/5
post-thumbnail
post-custom-banner

http://fabricjs.com/
fabric.js는 canvas를 쉽게 다룰 수 있는 많은 기능들을 제공하고 자유도가 높지만, 한땀 한땀 개발해야 한다는 단점이 있다.
직접 디자인을 커스텀 하고 싶어하는 회사가 있기 때문에 오히려 장점일 수 있다.
그냥 날것의 canvas를 다루는 것보다는 당연히 훨씬 낫지만(그건... 좀...)
모든 개발이 그러하듯 기본적으로 이미지 에디터에 들어있어서 그다지 깊게 생각하지 않았던 확대,축소, 드래그 기능들을 구현하면 꽤나 머리가 아프다.

그나마 다행인 건 얼마전에 패*캠** 에서 (바이럴아님 내돈내산임) 인터랙티브 웹 강의를 좀 들었다는 것이다. 2D는 한번 듣긴 들었고 three.js 천천히 듣는 중인데 도움이 많이 되지만 물리가 너무 어려움!!

기본 구조

처음에는 모든 fabric을 다루는 요소와 리액트 컴포넌트 훅을 한 파일에 다 넣었는데 그러면 구조가 복잡해지기 때문에 fabric을 별도의 클래스로 만들었다.

class FabricCanvas {
  private canvas: fabric.Canvas | undefined = undefined;
  private history: FabricJson[] = [];
  private currentIndex = -1;
  private isHistoryLocked = false;
  // 중략...
    constructor(targetElement: HTMLCanvasElement, imageDataUrl: string, maxWidth: number, maxHeight: number) {
     // 중략 ...

    this.canvas.on('object:modified', this.saveHistory);
    this.canvas.on('object:added', this.saveHistory);
    this.canvas.on('object:removed', this.saveHistory);
    this.canvas.on('mouse:down', this.handleMouseDown);
      
    // 후략

이런 식으로 안에 기능들을 다 넣어준다

영역 확대 축소


포토샵 같은 프로그램 처럼 뷰포트 영역 안에서 이미지를 확대 축소 가능하게 만들어야 하기 때문에 조금 계산을 해야 한다

canvas의 사이즈는 이미지 원본 사이즈와 동일하게 하고 css 사이즈를 실제로 브라우저에서 보이는 영역과 동일하게 한다.
그리고 fabric.js에서는 확대 축소를 기본으로 제공하기 때문에(휴...) 잘 조절해 주면 되며
우리가 따로 해야 할 것은 화면 맞춤-가로나 세로가 화면에 꽉 차도록-이 되는 기본 scale factor를 계산하는 것과, 축소했을 때 가운데 정렬이 되게 하는 것이다.

말은 쉽지만, 처음 프론트엔드 개발을 하느라 CRUD 같은 것을 하다가 이렇게 좌표 같은 것을 계산하면 어렵다.

sclae factor를 구하기


이미지 비율을 유지하면서 축소를 해야 한다
이미지가 뷰포트보다 크기가 작으면 그냥 원본 사이즈 그대로, 축소 할 것 없이 중앙에 띄우면 되고
위의 그림 처럼 이미지 세로와 가로의 최대 길이는 정해져있다.
(css로 치면 object-fit: contain;)

  const widthRatio = this.maxWidth / this.originalImgSize.width;
  const heightRatio = this.maxHeight / this.originalImgSize.height;

  this.scaleFactor = Math.min(1, Math.min(widthRatio, heightRatio));

그럼 비율을 구해보면 된다.
가로 세로 모두 최대 길이에서 이미지 사이즈를 나눈다.
그리고 1, 가로 비율, 세로 비율 중에 가장 작은 값을 가져오면 그게 contain에 해댱하는 scale factor가 된다.
(이건 그냥 공식처럼 외우는 게 나을지도..)

가운데 정렬

그리고 그 비율로 줌을 설정해주고 가운데 정렬을 해주면 된다.
가운데 정렬 역시 또 계산을 해야 하는데.

 const x = this.maxWidth / 2 - (this.originalImgSize.width * scaleFactor) / 2;
 const y = this.maxHeight / 2 - (this.originalImgSize.height * scaleFactor) / 2;

 this.canvas?.absolutePan(new fabric.Point(-x, -y));

css 등에서 가운데 정렬 할 때 요소의 절반 사이즈만큼 당겨 주는 거랑 똑같지만 *scaleFactor를 고려해야 한다. 그리고 -x, -y 여야 함.

scaleFactor를 현재의 줌 기준으로 변경하면

그러면 예쁘게 가운데 정렬이 된다

이렇게 해놓으면 확대 축소는 쉽다! setZoom 메소드를 쓰면 끝!

드래그해서 이동 (panning)

이렇게만 하면 크롭된 영역만 보이기 때문에 panning을 넣겠다.
이것도 fabric.js에서 제공하는 이벤트들을 사용하면 만들 수 있는데

this.canvas?.on('mouse:down', () => {
      const target = this.canvas?.getActiveObject();
      // 뷰포트보다 이미지 영역 클 때만 panning 활성화
      if (!target && this.scaleFactor < this.currentScaleFactor) {
        this.panning = true;
        this.canvas?.setCursor('grabbing');
      }
    });
    this.canvas?.on('mouse:move', (e) => {
      if (this.panning && e.e && this.scaleFactor < this.currentScaleFactor) {
        const delta = new fabric.Point(e.e.movementX, e.e.movementY);
        this.canvas?.relativePan(delta);
      }
    });

    this.canvas?.on('mouse:up', () => {
      this.panning = false;
      this.canvas?.setCursor('grab');
    });

마우스 다운, 마우스 무브, 마우스업으로 이동되게 만든다
이미지 영역이 작을 때는 비활성화 한다

현재 이미지가 몇 퍼센트인지 숫자로 표시하려면
scaleFactor * 100을 한 뒤 소숫점은 취향껏 처리하면 된다.

실행 취소

  private history: FabricJson[] = [];
  private currentIndex = -1;
  private isHistoryLocked = false;
  • history: fabric.js는 json 내보내기를 제공하는데 이것들을 저장할 배열이다.
  • currentIndex: 현재 위치를 나타내는 인덱스
  • isHistoryLocked: 실행 취소를 할 때 등 캔버스에 변화가 있어도 히스토리 쌓이는 걸 방지하는 용도


오브젝트가 삭제, 수정, 추가 될 때 히스토리를 저장하도록 한다.


실행 취소는 이런 식으로 만든다.

이걸 컴포넌트 상에서 컨트롤+z 단축키에 추가하면 된다.

이제 여기다 오브젝트를 추가 하는 건 크게 어렵지가 않다. 그냥 잘 추가 해주면 된다.


이제 이미지 위에 텍스트를 얹을 수 있고
이미지로 저장하면 원본 사이즈로 저장이 잘 된다!

profile
쓰고 싶은 글만 씀
post-custom-banner

0개의 댓글