Fabric.js란?

김하영·2022년 5월 28일
3

Canvas

목록 보기
1/2
post-thumbnail

출처 : https://medium.com/@seohyoda/fabric-js-%EC%9E%85%EB%AC%B8-part-1-78dd390536cf

fabric.js

HTML5의 Canvas는 오늘날 웹을 통해서 실현할 수 있는 그래픽의 수준을 크게 끌어올렸으나, 이를 구현하기 위해 사용하는 API는 복잡하고 어려운 편입니다.

그런 측면에서 Fabric.js 라이브러리는 동일한 그래픽 결과물을 구현함에 있어 방법이 단순하고, 객체 집합이나 사용자 인터랙션을 지원하여, 제대로 파악하고 사용하면 도움이 많이 되는 라이브러리입니다.

간단히 아래 코드를 통해 바로 비교해보겠습니다. 상단의 코드는 Native Canvas API를 통해 구현한 것이고, 하단의 코드는 Fabric.js로 작업한 것입니다.

// == canvas ==
// reference canvas element (with id="c")
var canvasEl = document.getElementById('c');

// get / 2d context / to draw / on / the "bitmap" / mentioned earlier
var ctx = canvasEl.getContext('2d');

// set / fill color / of context
ctx.fillStyle = 'red';

// create / rectangle / at a 100, 100 point, with 20x20 dimensions.
ctx.fillRect(100, 100, 20, 20);
// === fabric.js ===
// create / a wrapper / around native canvas element (with id="c")
var canvas = new fabric.Canvas('c');

// create a rectangle object
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20
});

// "add" rectangle onto canvas
canvas.add(rect);

결과는 아래와 같습니다.

https://miro.medium.com/max/418/0*hU2H8Oz426Qtz3YC.png

코드 양은 별 차이가 없지만, Fabric으로 작성한 코드가 Canvas 방식에 비해 좀더 직관적이며 가독성이 있음을 알 수 있습니다.

네이티브 캔버스를 사용하는 것은 컨텍스트(Context)를 네이티브 메서드로 제어하여 구현하는 방식으로써, 말하자면 하얀 도화지(컨텍스트)를 펼쳐놓고 그것을 옮기고 비튼 후에 도형을 올려놓는다거나 선을 그린다거나 하는 방식으로 생각해볼 수 있습니다.

이에 반해 Fabric은 객체(Object)에 기반하여 먼저 사각형이나 원 등 어떤 도형이나, 이미지 선과 같은 객체를 선정하고 거기에 속성값을 부여한 다음에 캔버스에 추가하는 형식으로 구현합니다.

위에 코드만으로는 딱히 차이점을 못느낄 수도 있을 것 같아서 다음과 같이 코드를 하나 더 준비했습니다. 이번에는 먼저 생성했던 사각형 객체를 45도 각도로 기울여 보겠습니다.

// == CANVAS ==

var canvasEl = document.getElementById('c');
var ctx = canvasEl.getContext('2d');
ctx.fillStyle = 'red';

ctx.translate(100, 100);
ctx.rotate(Math.PI / 180 * 45);
ctx.fillRect(-10, -10, 20, 20);

// == FABRIC.JS ==

var canvas = new fabric.Canvas('c');

// create a rectangle with angle=45
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20,
  angle: 45
});

canvas.add(rect);

https://miro.medium.com/max/416/0*gusK8mPsTrRgkXXJ.png

동일한 결과이지만, 구현하는 방식에서는 차이가 있습니다.

우선 네이티브 Canvas 의 경우는 컨텍스트(ctx)에 변경(translate) 옵션을 지정합니다. 컨텍스트 자체를 가변적인 영역으로 지정하는 것이죠. 말하자면 책상위에 놓인 도화지를 선택한 후 45도 각도로 틀어버린거죠. 그 상태에서 사각형 색종이를 올려 놓는 느낌으로 받아들이면 될 것 같습니다.

Fabric.js에서도 내부적으로는 위와 동일한 과정을 거치겠지만 그것을 구현하는 방식을 보면, 단순히 Fabric 의 Rect 객체를 생성하면서 angle 속성에 45도 각도 값을 지정한 것 뿐입니다.

좀더 살펴볼까요. 최초에 Top, Left를 100으로 지정하여 사각형을 해당 위치에 두었는데요. 이것을 , Top 50, Left 20으로 변경하는 경우를 보겠습니다.

// == native ==

var canvasEl = document.getElementById('c');

// ...
ctx.strokeRect(100, 100, 20, 20);
// ...

// erase entire canvas area
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
ctx.fillRect(20, 50, 20, 20);

// == fabric ==
var canvas = new fabric.Canvas('c');
// ...
canvas.add(rect);
// ...

rect.set({ left: 20, top: 50 });
canvas.renderAll();

https://miro.medium.com/max/416/0*dFENmdEJiFCSMLmw.png

네이티브의 경우 먼저 Top, Left 100 에 위치했던 도형을 지우고 다시 도형을 만드는 방식을 택했습니다.

반면 Fabric의 경우는 캔버스에 추가했던 rect 객체에 set 메서드를 통해서 속성값만 변경하고, canvas의 renderAll() 메서드를 사용하여 도형의 위치를 변경했습니다.


객체(Objects)

우리는 앞 서 fabric.Rect 생성자를 통해서 사각형 개체를 생성해보았습니다. 예상하셨겠지만, 이런식으로 제공하는 기본 도형 객체가 몇가지 있습니다.

Circle(원형), Triangle(삼각형), ellipses(타원형) 등등. 이들 모두는 fabric이라는 네임스페이스 이하 각각의 명칭으로 식별되고 사용됩니다.

fabric.Circle, fabric.Triangle, fabric.Ellipse, etc

  • fabric.Circle
  • fabric.Ellipse
  • fabric.Line
  • fabric.Polygon
  • fabric.Polyline
  • fabric.Rect
  • fabric.Triangle
var circle = new fabric.Circle({
  radius: 20, fill: 'green', left: 100, top: 100
});
var triangle = new fabric.Triangle({
  width: 20, height: 30, fill: 'blue', left: 50, top: 50
});

canvas.add(circle, triangle);

https://miro.medium.com/max/418/0*jywvPsO5tGpI0we8.png

객체조작하기

그래픽 객체를 생성하는 것은 단지 시작에 불과합니다. 특정 시점에 이르러, 우리는 만든 객체를 변경하고자 하는 니즈가 생길테죠. 이를테면 특정 액션을 통해 상태 변화를 꾀한다거나, 일련의 애니메이션을 재생한다거나, 마우스 조작을 통해서 객체의 속성(color, opacity, size, position)들을 변경한다던지 하는 것이지요.

Fabric은 캔버스의 랜더링과 상태 관리를 담당합니다. 우리는 그저 그래픽 객체만 수정하면 되는 것이죠.

먼저번 예시를 통해 set 메소드와 이전 위치에서 이동 시킬 때 그것을 어떻게 호출 set({ left: 20, top: 50}) 하는지를 살펴보았습니다. 이와 같은 방식으로 객체가 지닌 다른 속성들도 변경할 수 있습니다. 그러한 속성들은 무엇이 있을까요?

예상했다시피, 위치 지정과 관련된 것으로서 left, top 이 있습니다.

면적과 관련하여 width, height. 그리고 렌더링과 관련하여, fill, opacity, stroke, strikeWidth 가 있습니다.

그 외 확대축소에 해당하는 scaleX, scaleY가 있으며, 회전과 관련하여 angle 속성도 있습니다. 상하좌우 반전을 시키거나 뒤집는 용도로서 flipX, flipY가 있으며, 비대칭 적용을 위한 skewX, skewY 속성도 제공합니다.

플립된 객체를 만든다면, 단순히 flip* 속성을 true로 지정하는 것으로 끝납니다.

var canvas = new fabric.Canvas('c');

...

canvas.add(rect);

rect.set('fill', 'red');
rect.set({ strokeWidth: 5, stroke: 'rgba(100, 200, 200, 0.5)' });
rect.set('angle', 15).set('flipY', true);

set문장 각각은 조금씩 다름을 확인할 수 있습니다.

첫번째 set의 경우 하나의 속성와 해당하는 값을 지정하는 형식이고 두번째는 Key, value 의 Object형식으로 set을 지정했습니다. 세번째의 경우, 체이닝이라는 형식을 통해 연속적으로 속성을 할당하는 방식을 보여줍니다.

워낙 많이 쓰이는 메서드이므로, 필요나 형식에 따라 유연하게 set을 지정할 수 있도록 의도한 형태로 볼 수 있습니다.

이제 getter를 살펴보겠습니다. 일반적인 형식의 get 메서드를 사용할 수도 있고, 특정한 형태의 get* 메서드를 사용할 수도 있습니다. get(‘width’) 또는 getWidth()와 같이 쓸 수 있는 것이죠. 이 또한 편의를 고려한 것으로 보면 될 것 같습니다.

도형 객체를 생성할 때 쓰는 생성자에서 사용된 Object나 set메서드에 적용된 Object나 같습니다. 생성 시에 설정 객체를 지정하거나, 도형 객체를 만든 이후 set 메서드를 통해 설정 객체를 지정하는 것은 같다고 볼 수 있습니다.

var rect = new fabric.Rect({ width: 10, height: 20, fill: '#f55' opacity: 0.7 });

// or functionally identical

var rect = new fabric.Rect();
rect.set({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

기본 옵션

도형 객체를 생성할 때 설정 객체 없이 생성하는 경우, Fabric 의 객체는 기본 속성을 가지고 있어. 그 값을 할당하게 됩니다. 그 기본 값은 아래와 같습니다.

var rect = new fabric.Rect(); // notice no options passed in

rect.get('width'); // 0
rect.get('height'); // 0

rect.get('left'); // 0
rect.get('top'); // 0

rect.get('fill'); // rgb(0, 0, 0)
rect.get('stroke'); // null

rect.get('opacity');  // 1

사각형 도형 객체를 생성할 경우, 불투명한 객체로 폭과 높이 포지션이 0이므로 화면상에 보이지 않습니다. 여기에 면적값인 width, height에 값을 지정할 경우 해당 도형이 표시될 것입니다.

https://miro.medium.com/max/418/0*uw8FLTMEKDntZkWH.png

계층구조 및 상속

Fabric 객체는 각각이 독자적으로 존재하지 않습니다. 이들은 매우 정밀한 계층 구조를 형성하고 있습니다.

대다수 객체는 fabric.object 를 상속받습니다. fabric.object는 2차원의 캔버스 평면에 2차원 형태로 표시됩니다. 이 객체는 left/top, width/height 속성 외에도, 여러가지 다른 그래픽 특성들을 가집니다.

우리가 봐왔던 fill, stroke, angle, opacity, flip*, etc 등 자주 등장했던 속성들은 fabric.object로부터 상속받은 것들입니다.

이러한 상속으로 하여금 fabric.object에 대한 메서드를 정의하는 것이 가능하며, 이렇게 정의한 메서드는 모든 자식 클래스에서 공유할 수 있습니다. 예를 들어 getAngleInRadians 메서드를 사용하고 싶다면, fabric.object.prototype 에 해당 메서드를 생성하기만 하면됩니다.

fabric.Object.prototype.getAngleInRadians = function() {
  return this.get('angle') / 180 * Math.PI;
};

var rect = new fabric.Rect({ angle: 45 });
rect.getAngleInRadians(); // 0.785...

var circle = new fabric.Circle({ angle: 30, radius: 10 });
circle.getAngleInRadians(); // 0.523...

circle instanceof fabric.Circle; // true
circle.instanceof fabric.Object; // true

보다시피, 프로토타입을 통해서 구현한 메서드는 즉시 모든 인스턴스에서 사용 가능해졌습니다.

fabric.Object에서 기인한 클래스들은 종종 자체적으로 가진 속성과 메서드 정의를 필요로 할 때가 있습니다.

위의 예시와 같이 fabric.Circle의 경우처럼 라디안 값을 구해야할 때와 같이 말이죠. 부채꼴 호의 길이를 구하고자 할 때, 라디안 값과 반지름(radius)을 알아야 합니다. 이럴 때 요긴하게 사용될 수 있겠지요.

마찬가지로, fabric.Image의 경우는 Html 요소에 접근 하기위해 getElement/setElement 메서드가 필요할 수 있는데, 이런 것들을 구현할 수 있겠지요.

이와 같은 사용자 정의 렌더링과 동작을 얻기 위해서 프로토타입을 사용하는 것은 고급 프로젝트에서는 흔한 일입니다.

Canvas

오브젝트에 대해서는 이정도로 다루기로 하고, 이제 캔버스로 다시 돌아가보겠습니다.

캔버스 객체를 생성할 때, 모든 예제에서 가장 먼저 보게되는 것은 new fabric.Canvas(‘…’) 입니다. fabric.Canvas는 요소에 대한 래퍼(wrapper)를 제공합니다.

아울러, 특정 캔버스에 존재하는 fabric 객체들의 모든 관리책임을 가지고 있습니다. 이것은 요소의 아이디를 가져오고, fabric.Canvas의 인스턴스를 반환합니다.

우리는 이 기반 위에 도형 객체들을 추가(add)하거나, 참조 혹은 제거할 수 있습니다.

var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect();

canvas.add(rect);   // add object

canvas.item(0);   // reference fabric.Rect added earlier (first object)
canvas.getObjects();   // get all objects on canvas (rect will be first and only)

canvas.remove(rect);   // remove previously-added fabric.Rect

fabric.Canvas의 주요 목적은 도형 객체들을 관리하는 것과 설정 호스트로서 역할합니다. 캔버스 전체에 대한 백그라운드 색상 지정이나 이미지를 설정해야 한다거나, 특정 영역에 모든 컨텐츠를 위치시킨(Clip)다거나, 폭과 높이를 다르게 지정 한다거나 또는, 캔버스를 상호 작용토록 할지 안 할지를 지정 한다거나 하는 등의 모든 옵션 지정은 바로 fabric.Canvas에서 할 수 있습니다. 이러한 속성 지정은 캔버스 생성 시 또는 생성 이후로도 가능합니다.

var canvas = new fabric.Canvas('c', {
  backgroundColor: 'rgb(100,100,200)',
  selectionColor: 'blue',
  selectionLineWidth: 2
  // ...
});

// or

var canvas = new fabric.Canvas('c');
canvas.setBackgroundImage('htttp://...');
canvas.onFpsUpdate = function() { /* ... */ };
// ...

상호작용(Interactivity)

이제까진 캔버스 엘리먼트를 주제로 이야기 했고, 이제 상호작용에 대해서 이야기 해보려고 합니다. Fabric의 독특한 기능 중 하나로 우리가 지금까지살펴봤던 편리한 객체 모델 위에 존재하는 상호작용 레이어가 있습니다.

객체모델로 하여금 프로그램적으로 캔버스의 객체에 접근하고 조작하는 것이 가능한 한편, 외부 또는 사용자 차원에서 마우스(또는 터치, 터치장비)를 통해서도 이것이 가능합니다.

new fabric.Canvas(‘…’)를 통해 캔버스를 초기화하자마자, 객체들을 선택하고, 이리저리 끌고다니며, 크기를 조정하거나 회전시키고, 심지어 한덩어리로 뭉쳐 조정하는 것도 가능합니다.

https://miro.medium.com/max/418/0*77FhhfSnXtPAQFB6.png

https://miro.medium.com/max/416/0*SBgGIEJAXuvws8jF.png

캔버스에 이미지를 끌어다 놓고자 한다면, 캔버스를 초기화하고 그 위에 도형객체(또는 이미지)를 추가하기만 하면 됩니다. 기본값이 그렇기 때문에 추가 구성 또는 설정이 필요하지 않습니다.

만약 이러한 상호작용을 제어하고자 한다면, 캔버스에 대해서는 “selection” 속성을 false로 지정하면 됩니다. 더불어 개별 객체에 대해서 선택 가능 여하를 지정하고 싶다면 “selectable” 속성에 값(false)을 부여하면 됩니다.

var canvas = new fabric.Canvas('c');
...
canvas.selection = false;  // disable group selection
rect.set('selectable', false); // make object unselectable

만약 상호작용 계층을 아예 사용하지 않겠다고 하다면, fabric.Canvas 대신에 fabric.StaticCanvas를 사용하면 됩니다. 네임스페이스만 다를뿐 초기화를 위한 구문은 완전히 동일합니다.

var staticCanvas = new fabric.StaticCanvas('c');

staticCanvas.add(
  new fabric.Rect({
    width: 10, height: 20,
    left: 100, top: 100,
    fill: 'yellow',
    angle: 30
  }));

이것은 다르게 보자면 캔버스의 경량 버전이라고 보면 되며, 이벤트 핸들 로직이 배제된 것이라고 보면 됩니다. 다시말해, 도형객체(이미지)의 추가, 삭제, 수정과 캔버스 구성 변경은 여전히 가능하며 단지 이벤트 핸들링 기능만 빠진 것으로 볼 수 있습니다.

추후, 사용자 정의 빌드 옵션을 따로 다룰테지만, 그저 StaticCanvas 만으로 더 가벼운 버전의 Fabric버전을 쓸 수 있음을 의미합니다. 가령, 상호작용이 불필요한 조회 전용 차트라던가, 필터 기능만 있는 이미지가 필요하다면 이것만 쓰면 됩니다.

Images

웹 그래픽 작업이 비단 기본적인 도형만 캔버스에 올려놓고 제어하는 것에 국한 된다면 굳이 소개하지 않았을지도 모르겠습니다. Fabric에서는 이미지에 대해서도 막강한 기능을 제공하며, Fabric.Image 객체를 통해 이미지와 관련한 작업을 쉽게 할 수 있도록 해줍니다.

<canvas id="c"></canvas>
<img src="my_image.png" id="my-image">
var canvas = new fabric.Canvas('c');
var imgElement = document.getElementById('my-image');
var imgInstance = new fabric.Image(imgElement, {
  left: 100,
  top: 100,
  angle: 30,
  opacity: 0.85
});

canvas.add(imgInstance);

https://miro.medium.com/max/514/0*rq22RzUCRJjJqBys.png

위의 경우는 HTML문서에 특정 이미지가 있는 것을 전제로 작업을 했지만, 문서 상에 이미지가 없더라도 상관없습니다. image.fromURL을 통해서도 이미지를 확보하여 사용할 수 있습니다.

fabric.Image.fromURL('my_image.png', function(oImg) {
  canvas.add(oImg);
});

단지 fabric.Image.fromURL 을 이용해 꽤나 간단하게 이미지를 불러와 캔버스에 추가할 수 있는 것을 확인할 수 있습니다. 다만 여기에서는 콜백 함수가 쓰여졌는데, 자바스크립트의 비동기적 특성을 고려한 형태로 URL을 통해 이미지가 확보된 시점 이후로 동작해야할 로직을 해당 콜백함수에 적용해두었습니다. 콜백 함수의 첫번째 인자인 ‘oImg’가 이미지 객체라는 것을 알 수 있네요.

fabric.Image.fromURL('my_image.png', function(oImg) {
  // scale image down, and flip it, before adding it onto canvas
  oImg.scale(0.5).set('flipX', true);
  canvas.add(oImg);
});

문자(Text)

캔버스에 이미지와 벡터 도형뿐만 아니라, 텍스트도 표시하려면 어떻게 해야할까요? 패브릭에서 지원하는 텍스트 객체라는 것을 통해서 가능합니다. 패브릭에서는 2가지 이유로 텍스트 추상화를 제공하고 있습니다.

첫번째는 객체지향 스타일로 텍스트 작업을 가능토록 하기 위함입니다. 네이티브 캔버스 메서드는 저수준 레벨에서 채우는것과 외곽선을 그리는 것이 가능합니다.

패브릭에서는 fabric.Text 인스턴스를 통해 여타 패브릭 객체와 비슷한 방식(이동, 크기조정, 특성 변경 등)으로 작업이 가능합니다.

두번째로는 캔버스에서 제공하는 기본 기능 이상의 것을 제공하기 위함입니다.

  • Multiline support: 네이티브 텍스트의 경우는 새로운 라인을 지원하지 않았습니다.
  • Text alignment: Left, Center. right 등 멀티라인 텍스트에 대한 정렬을 제공합니다.
  • Text background: 백그라운드 역시 텍스트 정렬을 지원합니다.
  • Text decoration: 텍스트 장식을 지원합니다.
  • Line height: 멀티라인에 대한 높이 간격 설정에 유용합니다.
  • Char spacing: 문자 간격을 조정할 수 있습니다.
  • Subranges: 텍스트 객체의 하위 범주에 대한 색상 및 속성 지정이 가능합니다.
  • Multibyte: 이모티콘도 지원합니다.
  • On canvas editing: 대화형 클래스를 통해 캔버스에서 직접 편집이 가능합니다.
// Hello world

var text = new fabric.Text('hello world', { left: 100, top: 100});
canvas.add(text);

// fontfamily

var comicSansText = new fabric.Text("I'm in Comic Sans", {
	fontFamily: 'Comic Sans'
});

// fontSize

var text40 = new fabric.Text("I'm at fontSize 40", {
	fontSize: 40
});
var text20 = new fabric.Text("I'm at fontSize 20", {
	fontSize: 20
});

// fontWeight

var normalText = new fabric.Text("I'm a normal text", {
	fontWeight: 'normal'
});
var boldText = new fabric.Text("I'm at bold text", {
	fontWeight: 'bold'
});

// text Decoration

var underlineText = new fabric.Text("I'm an decorated Text", {
	underline: true,
	linethrough: true,
	overline: true
});

// shadow

var shadowText1 = new fabric.Text("I'm a text with shadow", {
  shadow: 'rgba(0,0,0,0.3) 5px 5px 5px'
});
var shadowText2 = new fabric.Text("Lorem ipsum dolor sit", {
  shadow: 'green -5px -5px 3px'
});

// fontStyle

var italicText = new fabric.Text("A very fancy italic text", {
  fontStyle: 'italic',
  fontFamily: 'Delicious'
});

// stroke and strokeWidth

var textWithStroke = new fabric.Text("Text with a stroke", {
  stroke: '#ff1318',
  strokeWith: 1
});

var loremIpsumDolor = new fabric.Text("Lorem ipsum dolor", {
  fontFamily: 'Impact',
  stroke: '#c3bfbf',
  strokeWidth: 3
});

// textAlign

var text = 'this is\na multiline\ntext\naligned right!';
var alignedRightText = new fabric.Text(text, {
  textAlign: 'right'
});

// lineHeight

var lineHeight3 = new fabric.Text('Lorem ipsum ...', {
  lineHeight: 3
});

var lineHeight1 = new fabric.Text('Lorem ipsum ...', {
  lineHeight: 1
});

// textBackgroundColor

var text = 'this is\na multiline\ntext\nwith\ncustom lineheight\n&background';
var textWithBackground = new fabric.Text(text, {
  textBackgroundColor: 'rgb(0, 200, 0)'
});

Events

이벤트 주도 아키텍처는 프레임워크 내에서 막강한 기능과 유연성의 기초가 되어줍니다. 패브릭 또한 이에 속하며 마우스 이벤트와 같은 저수준 레벨 부터 고수준 오브젝트까지 광범위한 이벤트 시스템을 제공합니다.

이들 이벤트들은 캔버스에서 다양한 행동이 발생하는 순간에 일어나 활용할 수 있도록 해줍니다.

마우스를 눌렀을 때는 “mouse:down” 이벤트를 확인해보면 됩니다.캔버스에 객체가 추가 되었을 때는 “object:added” 이벤트가 있습니다. 캔버스 전체가 다시 렌더링 되면요? “after:render”를 사용하면 됩니다.

var canvas = new fabric.Canvas('...');
canvas.on('mouse:down', function(options) {
  console.log(options.e.clientX, options.e.clientY);
});

캔버스에 “mouse:down”라고 하는 이벤트 리스너를 추가하고, 이벤트 발생 지점의 좌표를 기록하는 이벤트 핸들러를 작성하였습니다. 캔버스 위에서 마우스 버튼이 눌리면 해당 지점을 정확히 기록할 것입니다.

이벤트 핸들러는 두가지 속성(e:이벤트 원본, target: 대상)을 가진 옵션(options)을 수신하게 됩니다.

이벤트는 항상 존재하지만, 대상(target)은 실제로 캔버스에서 어떤 물체를 클릭했을 때만 존재합니다. 대상은 또한 해당 이벤트에 해당하는 핸들러에 전달됩니다.

예를 들어 “mouse:down”이 그런 예이고, “after:render”의 경우는 해당사항이 없지요. (전체 캔버스가 다시 그려진 경우임)

canvas.on('mouse:down', function(options) {
  if (options.target) {
    console.log('an object was clicked! ', options.target.type);
  }
});

위 예제에서는 사용자가 객체를 클릭하면 “객체가 클릭되었습니다” 뒤에 클릭된 객체의 타입도 표시하는 로그를 보여줄 것입니다.

그외 다른 이벤트는 어떤게 있을까요?마우스 레벨에서는 “mouse:down”, “mouse:move”, “mouse:up” 등이 있습니다.

일반적인 것으로는 “after:render”도 있구요.

선택과 연관된 이벤트로는 “before:selection:cleared”, “slection:created”, “selecton:cleared”가 있습니다.

마지막으로 객체에 해당하는 이벤트로는 “object:modified”, “object:selected”, “object:moving”, “object:scaling”, “object:rotating”, “object:added”, “object:removed” 등이 있습니다.

“object:moving”(또는 “object:scaling”)과 같은 이벤트는 픽셀 단위로 매번 발생하는 이벤트입니다. 한편, “object:modified” 또는 “selection:created”의 경우는 해당 액션이 끝난 시점에 한번 발생한다는 것을 알아두면 좋을 것 같습니다.

캔버스 상에 어떻게 이벤트를 붙여야 정상인 걸까요? 다수의 캔버스가 있다면 캔버스마다 별개로 이벤트가 적용됩니다. 즉 같은 이벤트 일지라도 캔버스에 종속되어 독립적인 이벤트로 간주된다고 보면 될 것 같습니다.

더 나아가, 패브릭에서는 편의를 위해 캔버스의 객체에 직접 이벤트 시스템을 붙일수도 있도록 했습니다.

var rect = new fabric.Rect({ width: 100, height: 50, fill: 'green' });
rect.on('selected', function() {
  console.log('selected a rectangle');
});

var circle = new fabric.Circle({ radius: 75, fill: 'blue' });
circle.on('selected', function() {
  console.log('selected a circle');
});

위 예제에서는 “object:selected” 대신에 사각형과 원형 도형에 이벤트 리스너를 직접 붙였습니다. 마찬가지로 “modified(“object:modified”)”나 rotating(object:rotating) 이벤트도 사용할 수 있습니다.

Groups

https://miro.medium.com/max/208/0*EqZSp8Pj21mbaw-l.png

먼저 그룹에 대해서 이야기 해보겠습니다. 그룹은 패브릭의 주요 기능 중 하나입니다. 그룹이 무엇이냐라고 하면 자명합니다. 패브릭 객체들을 단일한 개체로 묶어 놓은 것입니다. 그룹으로 묶는 이유는 여러 객체들을 단일한 유닛으로 다루기 위해서인거죠.

캔버스에 있는 몇 개의 패브릭 객체를 마우스로 묶을 수 있었죠. 일단 그룹을 형성하게 되면 객체들은 함께 이동이 가능하고 수정도 가능합니다. 우리는 이 그룹을 키우고, 회전시키고, 속성을 변경할 수도 있습니다. — color, transparency, border, etc.

이것이 그룹이 하는 일이고, 캔버스에서 그루핑된 셀렉션을 자주 확인하게 될것입니다. fabric.Group이 이 역할을 하며, 객체들을 묵시적으로 묶고, 프로그램적으로 접근할 수 있는 권한을 제공합니다.

원형과 텍스트로 그룹을 생성해보겠습니다.

var circle = new fabric.Circle({
  radius: 100,
  fill: '#eef',
  scaleY: 0.5,
  originX: 'center',
  originY: 'center'
});

var text = new fabric.Text({
  fontSize: 30,
  originX: 'center',
  originY: 'center'
});

var group = new fabric.Group([ circle, text ], {
  left: 150,
  top: 100,
  angle: -10
});

canvas.add(group);

먼저 텍스트의 경우, originX, orginY를 center로 설정하면 그룹에 중심에 위치하게 됩니다. 참고로 기본적으로는 그룹의 멤버 객체는 그룹의 왼쪽 상단 모서리에 위치하게 됩니다.

원형의 경우는 반경 100px을 지정하고 색상을 채운 후 스케일을 수직으로 .5로 눌러주었죠.

그룹에 만든 객체를 배열형태로 전달하고, 150, 100 위치에 -10 각도로 위치를 부여하여 캔버스에 추가하였습니다.

이제 타원처럼 생긴 객체를 볼 수 있게 되었습니다. 이 개체를 수정하기 위해서는 그룹의 속성을 변경하면 되는 것이지요.

https://miro.medium.com/max/296/0*PsIrRqASto-ehJvh.png

group.item(0).setFill('red');
group.item(1).set({
  text: 'trololo',
  fill: 'white'
});

그룹에 item() 이라는 메서드를 통해서 개별 객체에 접근할 수 있는 것을 확인할 수 있습니다. 그리고 이 객체의 속성을 수정할 수도 있구요. 먼저번에는 원형을 압착하여 타원형을 만들었다면 이번에는 텍스트를 바꾸었네요.

https://miro.medium.com/max/299/0*Piuhnjw2BL1hXMpS.png

한가지 중요한 사실은 그룹의 객체들은 그룹의 중심에 위치해있다는 것입니다. 따라서 텍스트가 변경되어 텍스트 길이가 변경되더라도 그 중심이 유지된 것이죠. 이런 상태를 원하지 않는다면, 객체의 왼쪽/위쪽 좌표를 지정해야 합니다.

이번에는 3개의 원형으로 이뤄진 그룹을 하나를 기준으로 그 뒤로 수평으로 위치하도록 만들어 보겠습니다.

var circle1 = new fabric.Circle({
  radius: 50,
  fill: 'red',
  left: 0
});
var circle2 = new fabric.Circle({
  radius: 50,
  fill: 'green',
  left: 100
});
vir circle3 = new fabric.Circle({
  radius: 50,
  fill: 'blue',
  left: 200
});

var group = new fabric.Group([ circle1, circle2, circle3 ], {
  left: 200,
  top: 100
});

canvas.add(group);

https://miro.medium.com/max/400/0*ilqFt944ESWjD__w.png

그룹으로 작업을 할 때 염두에 둬야 할 것은 객체들의 상태입니다. 예를들어 이미지로 그룹을 구성할 때는 해당 이미지가 완전히 로드되었는지 확인이 필요합니다. 패브릭은 이미지를 로드한 여하를 판단하는데 필요한 방법을 제공하므로 어렵지 않습니다.

fabric.Image.fromURL('/assets/pug.jpg', function(img) {
  var img1 = img.scale(0.1).set({ left: 100, top: 100});
  
  fabric.Image.fromURL('/assets/pug.jpg', function(img) {
    var img2 = img.scale(0.1).set({ left: 175, top: 175 });
    
    fabric.Image.fromURL('/assets/pug.jpg', function(img) {
      var img3 = img.scale(0.1).set({ left: 250, top: 250 });
      
      canvas.add(new fabric.Group([ img1, img2, img3], { left: 200, top: 200}))
    });
  });
});

그룹을 가지고 작업할 때 가능한 또다른 메서드론 무엇이 있을까요? getObjects()라고 하는 메서드입니다. 이는 fabric.Canvas#getObjects() 같은 형식의 구문으로 동작하며, 그룹 내 모든 객체의 배열을 반환합니다.

contain() 메서드는 그룹내 특정 객체가 있는지 유무를 판단하는데 사용하며, 앞서 본 item()은 그룹내 특정 객체를 조회 및 반환(참조)할 때 쓰입니다. forEachObject()는 그룹객체들과 관련이 있는 경우 그룹객체를 미러링 합니다. 마지막으로 add()와 remove() 메서드가 있습니다.

객체 추가/삭제는 두가지 방법이 있습니다. — 면적/위치 갱신하거나 그렇지 않을 경우.

// 그룹 센터에 사각형 추가

group.add(new fabric.Rect({
  ...
  originX: 'center',
  originY: 'center'
}));

// 그룹 센터에 100px 위치에 추가

group.add(new fabric.Rect({
  ...
  left: 100,
  top: 100,
  originX: 'center',
  originY: 'center'
}));

// 그룹에 사각형 추가하고 그룹의 면적을 부여함

group.addWithUpdate(new fabric.Rect({
  ...
  left: group.get('left'),
  top: group.get('top'),
  originX: 'center',
  originY: 'center'
}));

// 그룹 센터 위치에서 100px 위치에 사각형 추가하고, 면적 갱신

group.addWithUpdate(new fabric.Rect({
  ...
  left: group.get('left') + 100,
  top: group.get('top') + 100,
  originX: 'center',
  originY: 'center'
}));

// 이미 존재하는 오브젝트에 대해서.  복사하는 케이스

var group = new fabric.Group([
  canvas.item(0).clone(),
  canvas.item(1).clone()
]);

canvas.clear().renderAll();

canvas.add(group);

프로젝트 구현 예시

최근 진행했던 처음처럼 프로젝트같은 경우 ‘이미지 객체 삭제/편집 기능’과 ‘사용자가 직접 작성한 텍스트를 이미지와 함께 그룹화해 구현’하는 이슈가 있었다.

  • 객체 삭제 버튼
let $delete = document.createElement('img');
$delete.src = '/assets/images/delete-img.svg';

function renderIcon(ctx, left, top) {
	let size = this.cornerSize;

	ctx.translate(left, top);
	ctx.drawImage($delete, -size / 2, -size / 2, size, size);
}

function deleteObject(eventData, transform) {
	let target = transform.target,
			canvas = target.canvas;
	
	canvas.remove(target);
	canvas.requestRenderAll();
}

fabric.Object.prototype.controls.deleteControl = new fabric.Control({
	cursorStyle = 'pointer',
	mouseUpHandler: deleteObject,
	render: renderIcon,
	x: -.5,
	y: -.5,
	cornerSize: 44,
})

// 다중 객체 드래그 방지
canvas.selection = false;
  • 이미지 + 사용자에게 직접 입력받은 텍스트를 그룹화하여 캔버스에 구현
$('.element > i').on('click', function(e) {
		let $theme = new fabric.Group([
			// 이미지 
			new fabric.Image(e.target, {
				scaleX: 1.2,
			}),
			// 텍스트
			new fabric.Textbox($inputValue, {
				width: 180,
				fontFamily: 'example',
				fontSize: 54,
				color: #000,
				textAlign: 'center',
				originY: .25
			}),
		], { // 크기, 각도 조정
				borderScaleFactor: 1,
				borderColor: '#000',
				cornerSize: 22,
				cornerColor: '#000',
				transparentCorners: true
		});

		canvas.add($theme);
		canvas.centerObject($theme); // 캔버스 중앙 정렬
})
  • 다시하기 버튼 클릭시 모든 객체 삭제 구현 이슈
const canvas = new fabric.Canvas('canvas');

$('.refresh-btn').on('click', function() {
	let alert = confirm('지금까지 작업한 내역이 모두 사라집니다.\n새로고침 하시겠습니까?');

	if(alert) {
		// 원래는 아예 페이지를 새로고침하는 location.reload(); 메서드를 사용했었는데,
		
		// 캔버스 내 객체들을 직접 지우는 방법도 있다.
		let obj = canvas.getObjects();
		for(let i = 0; i < obj.length; i++) {
			canvas.remove(obj[i]);
		}
	}
})
  • 추가 이슈로 스티커 리스트에 Swiper 플러그인을 사용했었는데, loop를 사용해 두번째 슬라이드 구현 시, 클릭 이벤트가 작동하지 않는 이슈가 있었다.

→ click 이벤트를 Swiper 스크립트 하단에 작성해 해결

                                  
profile
호기심 많은 프론트엔드 주니어 💡

0개의 댓글