Development/Web

[Fabric.js] The transformation matrix in fabric.js (fabric.js의 변환 행렬) 번역 및 예제

대범하게 2024. 2. 29. 13:39
반응형

The transformation matrix in fabric.js

이 글은 faric.js을 공부하면서 마주했던 transmation matrix에 대해 정리한 글에 대한 (가벼운) 번역이자 추가 설명을 위한 글입니다.

 

The transformation matrix in fabric.js

Recently I’m using the excellent fabric.js library for a side project. I came across the transformation matrix. The purpose of this article…

medium.com

 

The purpose of this article is to detail how the transformation matrix works and especially how did we arrive at the transformation matrix, why is it in the format we know? Although this is an explanation in the context of fabric.js, it's extensible to other contexts (perhaps with slight implementation difference).

 

이 글의 목적변환 행렬(transformation matrix)이 어떻게 작동하는지, 특히 어떻게 변환 행렬에 도달하게 되었는지, 왜 우리가 알고 있는 형식으로 되어 있는지에 대해 자세히 설명하기 위함입니다. fabric.js의 맥락에서 설명하는 것이지만, 다른 맥락을 확장할 수 있습니다. (아마도 약간의 구현 차이가 있음.)

 

Since not everyone has a background in mathematics (it's actually high school math, but some people might barely remember), I will start by explaining all the fundamentals so that any reader can follow this article. Feel free to skip some part if you find it too basic.

 

모든 사람이 수학에 대한 배경지식이 있는 것은 아니므로 (사실 고등학교 수학이지만, 기억 안 나는 사람도 있을 수 있으니까..), 모든 독자가 이 글을 따라갈 수 있도록 모든 기본 사항을 설명하는 것으로 시작하겠습니다. 너무 기초적인 내용이라고 생각되면 건너뛰셔도 됩니다.

 

Base vectors of a Cartesian plane (데카르트 평면의 기본 벡터)

We already saw that Cartesian planes are in the previous article (if you are not familiar with this concept and with vectors, I strongly recommend check out this article before continuing reading.)

 

우린 이미 이전 글에서 데카르트 평면이 무엇인지 보앗습니다.(이 개념과 벡터에 익숙하지 않다면 계속 읽기 전에 이 글을 확인해보는 것이 좋습니다.)


 

원문의 글쓴이가 말한 이전 글에 데카르트 평면과 벡터에 대한 자세한 설명이 실려있다. 

 

간단히 설명하자면 데카르트 평면좌표평면이다. (데카르트가 좌표평면을 처음 만들었다고 한다.!)

 

좌표평면은 무엇인가? 바로 x축과 y축, 2개의 축으로 이루어진 평면을 말한다. x과 y축이 만나는 점을이 원점 origin(0, 0)이다.

 

벡터(vecotr)는 벡터에 대한 또렷한 기억 중 하나는 바로 '벡터는 방향을 갖는다' 이다. 


In a Cartesian coordinate system there are standard unit vectors in the direction of the X and Y axis.
The unit vector in the X axis is represented as î and the unit vector in Y axis as ĵ.
The unit vectors î and ĵ are the bases of our coordinate sytem, and we can represent any vector in terms of them. 

 

데카르트 좌표계에는 X축과 Y축 방향의 표준 단위 벡터(standard unit vectors)가 있습니다.

X축의 단위 벡터는 î, Y축의 단위 벡터는 ĵ로 표시됩니다. 

단위 벡터 î와 ĵ는 좌표계의 기저이며, 이를 기준으로 모든 벡터를 나타낼 수 있습니다.

 

Unit vectors: vectors that have a length of 1. By convention every unit vector is represented with a hat. For instance â, î,…  / Unit vectors(단위벡터: 한국말로는 단위 벡터라고 배웠다.) : 길이가 1인 벡터. 

 

Standard unit vectors

Let's look at an example. Here we will use the top left origin, that is, the positive direction of the Y axis is downards, like the canvas in the fabric.js. Representing the point (2, 2) in this plane: 

 

자, 이 예제를 살펴보겠습니다. 여기서는 fabric.js의 캔버스처럼, top left origin(왼쪽 위 원점), 즉 Y축의 양수 방향이 아래 쪽인 원점을 사용하겠습니다. 이 평면에서 점(2, 2)를 나타냅니다. 

 

Plot point (2, 2)

Now let's draw a vector from the origin point (0, 0) of our plane to the point (2, 2): 

 

그럼 평면의 원점 (0, 0)에서 점(2, 2)까지 벡터를 그려 보겠습니다: 

 

We can represent the vector [2, 2] in terms of the base of our plane:
That is, the vector [2, 2] is a linear combination of two î vectors and two ĵ vectors.
Any vector in this plane can be represented as a linear combination of î and ĵ (the basic of our plane).

 

벡터 [2, 2]를 평면의 밑변으로 표현할 수 있습니다. 

즉, 벡터 [2, 2]는 두 개의 î 벡터두 개의 ĵ 벡터의 선형 조합입니다. (why? î 벡터와 ĵ 벡터는 길이가 1인 단위 벡터이기에!_

이 평면의 모든 벡터는 î와 ĵ(평면의 기본)의 선형 조합으로 나타낼 수 있습니다. 

 

Vector [2, 2] in term of  î and  ĵ

 

Linear transformation (선형 변환)

A central concept for understanding the transformation matrix is linear transformation:
Linear transformation alters our plane such that parallel lines stay parallel and equally spaced, and the origin doesn't move.

 

변환 행렬(transformation matrix)을 이해하기 위한 핵심 개념은 선형 변환(linear transformation) 입니다. 

선형 변환(Linear transformation)은 평행선이 평행하며, 동일한 간격을 유지하고, 원점이 움직이지 않도록 평면을 변경합니다. 

 

*기하학적으로 접근한 선형 변환

- 변환 후에도 원점의 위치가 변하지 않는다. 

- 변환 후에도 격자들의 형태가 직선의 형태를 유지한다. 

- 격자 간의 간격이 균등해야한다. 

 

위와 같은 선형 변환을 fabric.js로 확인해보자.

 

Let's see in practice a linear transformation applied to our vector [2, 2]. For this, I created an animation using fabric.js (you can play around here.) When performing a rotation transformation with shear, we can see after transformation our vector remains equal to two step in the direction of the transformed X axis and two steps in the direction of the transformation Y axis.

 

벡터 [2, 2]에 적용된 선형 변환(linear transformation)을 살펴봅시다. 이를 위해 fabric.js를 사용하여 애니메이션을 만들었습니다(여기에서 확인해볼 수 있습니다). 전단으로 회전 변환을 수행할 때 변환 후 벡터가 변환된 X축 방향으로 2단계(2개의 î 벡터), 변환된 Y축 방향으로 2단계(2개의 ĵ 벡터)와 동일하게 유지되는 것을 볼 수 있습니다.

 

( 변환 후에도 원점의 위치가 변하지 X / 변환 후에도 격자들의 형태가 직선의 형태 / 격자 간의 간격이 균등 에 부합하는 것을 볼 수 있다.)

 

Linear transformation in action
"벡터 값은 변환되지 않은 평면(untransformed plane)을 기준으로 변경된다."

 

The blue grids (dark and light) represent our transformed plane, and the light gray grids represent our untransformed plane. The values of  î, ĵ, v vectors are calculated relative to the untransformed plane. 
Notice that the values of these vectors change relative to the untransformed plane.
For example, the vector î before transformation was [1, 0] and after transformation is [0.9397, -0.342].
But even with the transformation, the vector v remained equal to  2î + 2ĵ and the origin remained the same. That is, this is a linear transformation.

 

파란색 격자(어둡고 밝은)는 변환된 평면을 나타내고, 밝은 회색 격자변환되지 않은 평면을 나타냅니다.

î, ĵ, v 벡터의 값은 변환되지 않은 평면을 기준으로 계산됩니다.

이러한 벡터의 값은 변환되지 않은 평면(untransformed plane)을 기준으로 변경된다는 것을 주의하세요.!

 

예를 들어, 변환 전의 벡터 î는 [1, 0]이고, 변환 후의 벡터는 [0.9397, -0.342] 입니다.

그러나 변환 후에도 벡터 v는  2î + 2ĵ 로 동일하게 유지되며 원점도 동일하게 유지됩니다. 즉, 바로 이것이 선형 변환입니다. 

 

Linear transformation with matrics (행렬을 사용한 선형 변환)

As we have seen, to describe a linear transformation we only need to keep track where the vectors î  and 2x2 transformation matrix go and calculate the coordinates with respect to the untransformed plane.

 

앞서 살펴보았듯이 선형 변환을 설명하려면 벡터 î와 2x2 변환 행렬이 어디로 가는지 추적하고, 변환되지 않은 평면에 대한 좌표를 계산하기만 하면 됩니다.

 

The matrix is nothing more than packing these two vectors together to make the calcultation easier. It's called a two-by-two matrix that describes a 2D linear transformation. The first column of this matrix describes the vector î after the transformation and the second column of the matrix describes the vector ĵ after the transformation:

 

행렬은 계산을 더 쉽게 하기 위해 두 벡터를 함께 묶은 것에 불과합니다. 이는 2차원 선형 변환을 설명하는 2x2 행렬이라고 불립니다.

이 행렬의 첫 번째 열은 변환 후 벡터 î ([0.9397, -0.342])를 나타내고 행렬의 두 번째 열은 변환 후의 벡터 ĵ ([0.1763, 1])를 나타냅니다. 

2x2 transformation matrix

 

If we want to find out where a vector is located after the transformation, all we need to do is multiply our original vector by the 2x2 transformation matrix: 

 

변환 후 벡터의 위치 ([2.232, 1.316])를 찾으려면, 원래 벡터에 2x2 변환 행렬을 곱하기만 하면 됩니다:

Applying transformation matrix to original vector to find final position

 

Note that we find the final position of the vector after the transformation without having to work with the Cartesian plane. This makes the calculation process much easier and faster. In an animated way, we can map the calculation from the Cartesian plane to matrices as follow:

 

데카르트 평면을 사용할 필요 없이 변환 후에 벡터의 최종 위치를 찾는다는 것에 주목하세요. (즉, 특정한 좌표 시스템에 의존하지 않고도 벡터의 변환을 계산할 수 있다.) 이렇게 하면 계산 과정이 훨씬 쉽고 빨라집니다. 애니메이션 방식으로 다음과 같이 데카르트 평면에서 행렬로 매핑하는 과정을 보여주고 있습니다:

 

Cartesian plane calculation vs matrix-wise

 

If you need a matrices review, I recommend these videos:

Let us now see some specific matrices and their respective transformations.

Transformation : scaling 

When applying a scale on the X axis and a scale on the Y axis, the unit vectors î and ĵ after the transformation will be: 

 

X축에 scale(배율)을 적용하고 Y축에 scale을 적용하면 변환 후의 단위 벡터 î 와 ĵ는 다음과 같습니다:

Scaling transformation

We can pack this transformation into a matrix that is called a scaling matrix: 

 

이 변환을 스케일링 행렬(scaling matrix)이라고 하는 행렬에 담을 수 있습니다:

Scaling matrix

 

For example, applying scale = 0.8 and scaleY = 0.8:

 

Scaling transformation in fabric.js

 

Transformation : flip

This opreation is similiar to scaling. We just need to invert one of the coordinates for horizontal or vertical inversion (or both) to reflect about the origin.

 

이 연산은 scaling과 유사합니다. 수평 또는 수직 반전(또는 둘 다)을 위해 좌표 중 하나를 원점을 기준으로 반전하기만 하면 됩니다.

 

Flip transformation

 

The flip in the transformation matrix boils down to the inversion of the sign on the respective scale axis:

 

변환 행렬의 반전(The filp)은 각 배율(scale) 축의 부호를 반전하는 것으로 요약됩니다.

Flip matrix

 

For instance, applying flipY

 

Flip transformation in fabric.js

 

Transformation : skewing

First, let's understand how shear occurs. By perfoming a shear along the X axis (skewX) form an alpha angle at the point (x, y), our transformed point will be offset by a delta X value:

 

먼저, shear(전단)이 어떻게 발생하는지 이해 해보겠습니다. 점 (x, y)에서 알파 각도로 X축을 따라 전단(skewX)을 수행하면 변형된 점이 델타 X값으로 오프셋 됩니다:

 

skewX of alpha

To calculate the value of delta X, just a little geometry:

 

Calculation of delta X

 

We know the tangent of alpha (= skewX) and the value of y, we can write delta X in terms of both:

 

 

That is, a shear transform ( skewX) on the X axis applied to a point(x, y) will transform it into:

(x, y) = (x + y * Tan(skewX), y)

The deduction of the shear formula along the Y axis is the same. In order to not get too long, I won’t disassemble it, but the result after a skewY transformation at the point(x, y) will be:

(x, y) = (x, y + x * Tan(skewY))

The more attentive reader may be wondering what happens if we apply skewX and skewY at the same time. We have to determine an order of operation, otherwise we will have a circular reference. In the case of the canvas (and fabric.js), first we apply the skewY and then the skewX:

 

 

So, we can write the shear transformation in the unit vectors as: 

 

따라서 단위 벡터에서 기울임(전단) 변환을 다음과 같이 쓸 수 있습니다:

 

Skew transformation

 

We can pack this transformation into a matrix that is called shear matrix: 

Shear matrix

 

For example, applying skewX = -20 and skewY = -10:

 

Shear transformation in fabric.js

 

Transformation: rotation

As expected, the transformation due to a rotation is a function of the angle of the rotation. The calculation of vectors values is found through trigonometry:

 

예상대로 회전으로 인한 변형회전 각도의 함수입니다. 벡터 값의 계산은 삼각법을 통해 찾을 수 있습니다:

 

Rotation transformation

 

We can pack this transformation into a matrix that is called rotation matrix:

 

이 변환을 회전 행렬(rotation matrix)이라고 불리는 행렬에 담을 수 있습니다:

Rotation matrix

Transformation : translate

Translation moves our coordinate system to another location, i.e. changes the origin. And by definition, by changing the origin, it's not a linear transformation, so we cannot describe this transformation in our linear transformation matrix:

 

이동(Translation)좌표 시스템을 다른 위치로 옮기는 것입니다, 즉 원점(origin)을 변경하는 것입니다. 

정의에 따라 원점을 변경함으로써 선형 변환이 아니므로 이 변환을 선형 변환 행렬로 설명할 수 없습니다. (*선형 변환은 원점이 변경되지 X)

 

To represent this transformation, we create a vector from the origin of the plane to the new origin of the translated plane: 

 

이 변환을 나타내기 위해, 우리는 평면의 원점에서 새로운 원점까지 벡터를 생성합니다:

 

Vector pointing to the translated plane (변환된 평면을 가리키는 벡터)

 

It may seem a little strange that the vector î and ĵ continue with the values [1 0] and [0 1], but here we are talking about different planes.

 

벡터 î ĵ 이 값 [1 0] 및 [0 1]으로 계속되는 것은 약간 이상해 보일 수 있지만, 우리는 여기서는 다른 평면에 대해 이야기하고 있습니다.

 

And how do we represent translation in matrix format? We can combine our two-by-two linear transformation matrix with the translation into a single matrix: 

 

그럼 이동을 행렬 형식으로 어떻게 나타낼까요? 2x2 선형 변환 행렬단일 행렬로 변환할 수 있습니다. 

 

Translation matrix

 

Notice that an extra line appeared at the end of our matrix, we have increased the matrix to make it compatible with translation operations.
Now if we are going to multiply a vector by this transformation matrix, we must also add one more line in the matrix that represents this vector to allow the matrix operations: 

 

행렬 끝에 한 줄이 추가된 것을 확인할 수 있는데, 이동 작업과 호환되도록 행렬을 늘렸습니다. 

이제 벡터에 변환 행렬을 곱하려면 행렬 연산을 허용하기 위해 이 벡터를 나타내는 행렬에 행을 하나 더 추가해야 합니다. 

 


추가적인 설명을 덧붙이자면, [0 0 1]은 이동 벡터이다.

행렬 끝에 한 줄이 추가된 이유는 이동 작업을 수행하기 위해 필요한 것이다.

선형 변환 행렬은 원래 2x2 행렬이지만, 이동을 수행하기 위해서는 이동할 평면의 위치를 나타내는 추가적인 정보가 필요하다.

이를 위해 2차원 공간에서 평면의 이동을 표현하기 위해 3차원 벡터가 사용된다.

 

2차원 평면에서의 이동은 두 개의 좌표 축으로 이루어진 평면을 다른 위치로 옮기는 것이다.

이동 벡터 [0 0 1]원래 평면의 x 및 y 좌표가 변하지 않고, 오직 이동만 있음을 나타낸다

 

이동 작업을 행렬로 표현할 때는 선형 대수의 특성을 유지하기 위해 벡터를 확장해야 합니다.

따라서 이동 작업에 사용되는 벡터는 보통 [x y 1] 형식으로 표현됩니다.

1이 추가되면서 이 벡터는 3차원으로 확장되었기 때문에 이동 작업에 사용되는 행렬도 3x3 행렬이 됩니다.

 

Composing multiple transformations (다중 변환 구성)

We can compose a series of transformation in sequence. For example, a scaling, then a rotation and finally a shear: (note that we are now working with the augumented transformation matrix to a acommodate the translation)

 

일련의 변환을 순서대로 구성할 수 있습니다. 변환의 예를 들어 scaling, 그 다음 rotation, 마지막으로 skew과 같은 변환을 순서대로 적용할 수 있습니다. (주목할 점은 이제 이동을 수용하는 확장된 변환 행렬과 함께 작업한다는 것입니다.)

 

Transformation matrices

 

For this, we multiply each of the transformation matrices to arrive at the final transformation matrix:

 

이를 위해 각 변환 행렬을 곱하여 최종 변환 행렬에 도달합니다: 

 

Final transformation matrix

 

The matrix multipication order makes difference in the final result. That is, matrixA times matrix B is from matrix B times matrix A.
The order of multipication is important, the first transformation start rightmost. For example, if we reversed the order of multiplication, the final result would be different:

 

행렬 곱셈 순서에 따라 최종 결과가 달라집니다. 즉, 행렬 A 곱하기 행렬 B는 행렬 B 곱하기 행렬 A와 다릅니다.

곱셈 순서가 중요하며, 첫 번째 변환은 가장 오른쪽에서 시작됩니다. 예를 들어 곱셈 순서를 반대로 하면 최종 결과가 달라집니다.

 

Different results when we change the order of multiplication

 

But why when we do something like code below in fabric.js the end result is always the same?

 

하지만 아래 코드와 같은 작업을 fabric.sj에서 수행할 때 최종 결과가 항상 동일한 이유는 무엇일까요?

const rect1= new fabric.Rect({ 
  width: 100, 
  height: 100, 
  scaleX: 1,
  scaleY: 1,
  skewX: 1,
  skewY: 1,
  angle: 0
});

const rect2= new fabric.Rect({ 
  width: 100, 
  height: 100, 
  scaleX: 1,
  scaleY: 1,
  skewX: 1,
  skewY: 1,
  angle: 0
});

canvas.add(rect1, rect2);

rect1.set({scaleX: 2});
rect1.set({angle: 30});
rect1.set({skewX: 45});

rect2.set({skewX: 45});
rect2.set({angle: 30});
rect2.set({scaleX: 2});

canvas.renderAll();

 

We are applying the transformation in different order, but the end result is the same. The reason is that there is fixed transformation order for objects, regardless of whether we set one of the transformation before or after. The order used internally by fabric.js is as follows:

 

(위 코드는) 변환을 다른 순서로 적용하지만, 최종 결과는 동일합니다. 그 이유는 변환 중 하나를 미리 설정하든 나중에 설정하든 상관없이 객체에 대해 고정된 변환 순서가 있기 때문입니다. fabric.js에서 내부적으로 사용하는 순서는 다음과 같습니다:

 

Fabric.js transformation order

 

First apply skew, then scaling, rotation and finally translation. We can change this order when working with groups, we'll see later in this article.

 

먼저 skew(기울기)를 적용한 다음, scaling(크기 조정), rotation(회전), 마지막으로 translation(이동)을 적용합니다. 그룹으로 작업할 때 이 순서를 변경할 수 있는데, 이 글의 뒷부분에서 살펴보겠습니다.

 

skewY -> skewX -> scaling -> rotation -> translation (첫 번째 변환은 가장 오른쪽에서 시작됩니다.)

 

Undoing transformations (변환 실행 취소)

We can undo certain transformations directly in the final transformation matrix. For this we use a property of matrices which is: the multiplication of matrix (M) by its inverse (M-¹) produces an identity matrix (I). Note that order of multiplication in this case produces the same result:

 

최종 변환 행렬에서 특정 변환을 직접 실행 취소할 수 있습니다. 

이를 위해 행렬의 속성을 사용합니다: 행렬 (M)을 역행렬 (M-¹)으로 곱하면 항등 행렬 (I)이 생성됩니다.

이 경우 곱셈의 순서는 동일한 결과를 낳는다는 점을 유의하세요:

(행렬과 그 역행렬을 곱하면 순서가 상관없다라는 뜻.)

 

Matrix times its inverse (역행렬 곱하기)

 

The identity matrix does not change the result of a matrix multiplication. That is, matrix A multiplied by identity matrix (I) remains matrix A: (again the order of multiplication is indifferent)

 

항등 행렬은 행렬 곱셈의 결과를 바꾸지 않습니다.

즉, 행렬 A에 항등 행렬 I를 곱한 값은 행렬 A로 유지됩니다: (곱셈의 순서는 중요하지 않음)

 

Matrix times identity

 

So if we have the following transformation:

 

 

And we want to remove the scale transformation, just multiply by the inverse of the scale matrix:

 

scale 변환을 제거하려면, scale 행렬의 역을 곱하기만 하면 됩니다: (역행렬이 필요한 이유 == 특정 변환을 취소하기 위함.)

 

Removing scale

 

One important thing is that we must multiply the inverse of the transformation matrix next to the original matrix. For eaxample, if we wanted to remove skew, the first case is incorrect and the second correct:

 

한 가지 중요한 점은 원래 행렬 옆에 변환 행렬의 역을 곱해야 한다는 것입니다.
예를 들어, skew를 제거하려는 경우, 첫 번째 경우는 올바르지 않고 두 번째 경우가 올바릅니다.

 

Removing skew

 

Fabric.js has functions to assist with these operations:

 

- fabric.util.invertTransform: calculates the inverse of a transformation matrix. (변환 행렬의 역을 계산한다.)

- fabric.util.multiplyTransformMatrices: multiplies two transformation matrices. (두 개의 변환 행렬을 곱한다.)

- fabric.util.transformPoint: apply transform to a point. (point에 transform을 적용한다.)

 

As we saw, for transformation in a single object fabric.js abstracts the entire transformation process. But when we work iwith different planes, knowing these operations is important.

 

앞서 살펴본 것처럼 단일 객체에서의 변환의 경우 fabric.js는 전체 변환 프로세스를 추상화합니다. 하지만, 다른 평면에서 작업할 때는 이러한 연산을 아는 것이 중요합니다. 

 

The transformation matrix in fabric.js 

The transformation matrix in fabric.js is represented in an array of length 6 as follows:

 

fabric.js에서의 변환 행렬은 다음과 같이 길이 6의 배열로 표현됩니다:

 

 

There are a few ways to calculate the transformation matrix of an object in fabric.js. It depends on which plane we are referring to: 

 

fabric.js에서 객체의 변환 행렬을 계산하는 방법에는 몇 가지가 있습니다. 어떤 평면을 참조하는지에 따라 다릅니다:

const rect = new fabric.Rect({ 
  width: 100, 
  height: 100, 
  scaleX: 1,
  scaleY: 1,
  skewX: 1,
  skewY: 1,
  angle: 0
});

// Transformation matrix in the object plane (객체 평면의 변환 행렬)
rect.calcOwnMatrix();

// Transformation matrix from the canvas plane to the object plane (canvas 평면에서 객체 평면으로의 변환 행렬)
rect.calcTransformMatrix();

 

 

- calcOwnMatrix: returns the transformation matrix in the object plane, without considering external transformations.

- calcTransformMatrix: returns the transformation matrix from the canvas plane to the object plane. If the object is inside a group, it will consider these transformations as well

 

- calcOwnMatrix: 외부 변환을 고려하지 않고, 객체 평면의 변환 행렬을 반환합니다. 

- calTransformMatrix: 캔버스 평면에서 객체 평면으로의 변환 행렬을 반환합니다. 객체가 그룹 안에 있는 경우, 이러한 변환도 고려합니다.

 

Later on we will see in more details the multiple planes that exist on the canvas and these concepts will become clearer.

 

나중에 캔버스에 존재하는 여러 평면에 대해 더 자세히 살펴보고 이러한 개념을 더 명확하게 이해할 수 있습니다.

 

The different Cartesian planes in fabric.js

We will work with different Cartesian planes and see how the transformation matrix allows us to locate points on these different planes. 

 

다양한 데카르트 평면으로 작업하고 변환 행렬을 통해 이러한 서로 다른 평면에서 점을 찾는 방법을 살펴봅니다.

 

Let's start with an example. Adding a rectangle to the canvas: 

const rect = new fabric.Rect({
  width: 50,
  height: 50,
  fill: 'red',
  strokeWidth: 0,
  originX: 'left',
  originY: 'top',
  left: 25,
  top: 25
});
canvas.add(rect);

 

Our canvas is a Cartesian plane (origin is top left, so the Y axis grows downwards). When we add a rectangle, behind the scene we are defining a new plane and set of 4 points inside it that form our rectangle: 

 

캔버스는 데카르트 평면입니다.(원점은 top left이므로 Y축은 아래로 증가합니다.) 직사각형을 추가하면, 장면 뒤에 직사각형을 이루는 새로운 평면과 그 안에 있는 4개의 점 집합을 정의합니다:

 

Canvas plane and rect plane

 

Now we have two planes: 

 

  • The canvas plane (has gray arrows as axes) - 캔버스 평면 (회색 화살표가 축으로 표시됨)
  • The rectangle plane (orange arrows as axes) - 사각형 평면 (오렌지색 화살표를 축으로 함)

 

Note that the plane of an object has its center as the origin, so it is located at the point (50, 50). This is a fabric.js design choice. Each of these planes has a transformation matrix that allow us to apply the existing transformations in that plane to a point. If we calculate the transformation matrix of the object and the canvas: 

 

객체의 평면은 객체의 중심을 원점(origin)으로 하고 있으므로 점(50, 50)에 원점이 위치함을 주목하세요. 이건 fabric.js 설계 선택입니다. 이러한 각 평면에는 해당 평면에 있는 기존의 변환을 한 점에 적용할 수 있는 변환 행렬이 있습니다. 객체와 캔버스의 변환 행렬을 계산하면 다음과 같습니다. 

// Canvas plane transformation matrix 
canvas.viewportTransform
// [1, 0, 0, 1, 0, 0]

// Object plane transformation matrix
rect.calcTransformMatrix();
// [1, 0, 0, 1, 50, 50]

 

As we can see, in the plane of the object, translateX and translateY point to the center of the object which is the center of the plane.

 

보시다시피, 객체의 평면에서 translateX와 translateY는 평면의 중심인 객체의 중심을 가리킵니다.

 

In the previous image we are observing the points in relation to the canvas plane. What it we observed the same points but in relation to the plane of the rectangle? The representation would look like this:

 

이전 이미지에서는 캔버스 평면을 기준으로 점들을 관찰하고 있습니다. 우리가 같은 점들을 관찰했지만 직사각형의 평면을 기준으로 관찰한 것은 무엇일까요? 다음과 같이 표현할 수 있습니다.

 

The points relative to the plane of the rectangle

 

Let's apply what we learn. I have two rectangles with two different transformations (one with scale and one with rotation):

 

배운 내용을 적용해 봅시다. 두 가지 변형이 있는 두 개의 직사각형이 있습니다.(하나는 scale이 있고, 하나는 rotation이 있는 직사각형)

 

const rect_1 = new fabric.Rect({
  width: 50, 
  height: 50, 
  fill: 'red', 
  left: 10, 
  top: 10,
  scaleX: 3
});

const rect_2 = new fabric.Rect({
  width: 50, 
  height: 50, 
  fill: 'red', 
  left: 10, 
  top: 100,
  angle: 30
});

canvas.add(rect_1, rect_2);

 

I want to create a line from the center to the top right corner. How could I do this using transformation matrix?

 

중앙에서 오른쪽 상단 모서리까지 선을 만들고 싶습니다. 변환 행렬을 사용하여 이 작업을 수행하려면 어떻게 해야 하나요?

 

Our goal

 

Try to think a little before looking at the solution...

Let's look at one of the ways to achieve this. We first look at the untransformed plane of the object. We want to draw a vector from the origin of this plane, point (0, 0), to the point (25, -25):

 

해결책을 보기 전에 조금만 생각해보세요...

이를 달성하는 방법 중 하나를 살펴 보겠습니다. 먼저 객체의 변형되지 않은 평면을 살펴봅시다. 이 평면의 원점인 점(0, 0)에서 점(25, -25)까지 벡터를 그리고자 합니다:

 

Object's untransformed plane

 

When we are creating the line, we are on the canvas plane. That is, we need to find these points on the canvas plane. to do this, just multiply by the transformation matrix of each of the rectangles:

 

선을 만들 때 우리는 캔버스 평면에 있습니다. 즉, 캔버스 평면에서 이 점을 찾아야 합니다. 이렇게 하려면 각 직사각형의 변환행렬을 곱하기만 하면 됩니다:

 

Points of interest in the untransformed plane of objects

// Points of interest in the untransformed plane of objects
const centerPoint = new fabric.Point(0, 0);
const cornerPoint = new fabric.Point(25, -25);

// The same points of interest now on the canvas plane
// As each rectangle has a different transformation,
// to map to the canvas plane we need to multiply by
// each of the transformation matrices.
const rect_1Center = centerPoint.transform(rect_1.calcTransformMatrix());
const rect_1CornerPoint = cornerPoint.transform(rect_1.calcTransformMatrix());

const rect_2Center = centerPoint.transform(rect_2.calcTransformMatrix());
const rect_2CornerPoint = cornerPoint.transform(rect_2.calcTransformMatrix());  

// Then we create the lines using the points on the canvas plane
const line_1 = new fabric.Line(
  [
    rect_1Center.x, // line origin.x
    rect_1Center.y, // line origin.y
    rect_1CornerPoint.x, // line destination.x
    rect_1CornerPoint.y // line destination.y
  ], 
  { stroke: 'blue', strokeWidth: 1, originX: 'center', originY: 'center'}
);

const line_2 = new fabric.Line(
  [
    rect_2Center.x, 
    rect_2Center.y, 
    rect_2CornerPoint.x, 
    rect_2CornerPoint.y
  ], 
  { stroke: 'blue', strokeWidth: 1, originX: 'center', originY: 'center'}
);

canvas.add(line_1, line_2);

 

We also work with different planes when an object is inside a group. Each group represents a new plane and the transformation a new plane and the transformation existing in the group is applied to all objects (or other groups) contained within it.

 

또한 객체가 그룹 안에 있을 때 다른 평면으로 작업합니다. 각 그룹은 새로운 평면을 나타내며 그룹에 존재하는 변환을 그룹에 포함된 모든 객체 (또는 다른 그룹)에 적용됩니다.

 

For example, let's add two rectangles to a group and apply a scaleY eqaul to 2 to the group.

 

예를 들어, 그룹에 직사각형 두 개를 추가하고 그룹에 2와 같은 scaleY을 적용해보겠습니다.

const rect_1 = new fabric.Rect({
  width: 50, 
  height: 50, 
  fill: 'red', 
  left: 10, 
  top: 10,
  scaleX: 3
});

const rect_2 = new fabric.Rect({
  width: 50, 
  height: 50, 
  fill: 'red', 
  left: 10, 
  top: 100,
  angle: 30
});

const group = new fabric.Group(
  [rect_1, rect_2],
  {
    left: 30,
    top: 100,
    scaleY: 2
  }
)

canvas.add(group);

 

Each of the rectangles and the group will have their respective transformation matrices (calcOwnMatrix), which not consider any external transfomation (from another plane):

 

각 직사각형과 그룹은 외부 변환(다른 평면으로부터의 변환)을 고려하지 않는 각각의 변환 행렬(calcOwnMatrix)을 갖게 됩니다:

console.log(rect_1.calcOwnMatrix()); 
// [3, 0, 0, 1, 12.75, -54.333647796503186]

console.log(rect_2.calcOwnMatrix()); 
// [0.8660254037844387, 0.49999999999999994, -0.49999999999999994, 0.8660254037844387, -54.416352203496814, 45]

console.log(group.calcOwnMatrix()); 
// [1, 0, 0, 2, 119.25, 259.6672955930064]

 

To find the total existing transformation in each of the rectangles, we must multiply all the transformation matrices that affect them. That is, we must multiply the transformation matrix of the group and that of the rectangle itself: 

 

각 직사각형에 존재하는 총 변환을 구하려면 직사각형에 영향을 미치는 모든 변환 행렬을 곱해야 합니다. 즉, 그룹의 변환 행렬과 직사각형 자체의 변환 행렬을 곱해야 합니다:

console.log(fabric.util.multiplyTransformMatrices(
  group.calcOwnMatrix(), 
  rect_1.calcOwnMatrix()
));
// [3, 0, 0, 2, 132, 151]

console.log(fabric.util.multiplyTransformMatrices(
  group.calcOwnMatrix(), 
  rect_2.calcOwnMatrix()
));
// [0.8660254037844387, 0.9999999999999999, -0.49999999999999994, 1.7320508075688774, 64.83364779650319, 349.6672955930064]

 

A point of attention when multiplying the transformation matrices is in the order of multiplication. The first transformation must be rightmost, so the last argument is the rectangle transformation matrix since it is the first transformation. This process of multiplying transformation matrices would get tedious if we had a lot of nested groups. Fabric.js has a method that calculate this for us: 

 

변환 행렬을 곱할 때 주의해야 할 점은 곱하는 순서입니다. 첫 번째 변환이 가장 오른쪽에 있어야 하므로 마지막 인수는 첫 번째 변환이므로 직사각형 변환 행렬이 됩니다. 중첩된 그룹이 많으면 변환 행렬을 곱하는 과정이 지루해질 수 있습니다. Fabric.js에는 이를 계산하는 메서드가 있습니다: -> calcTransformMatrix (객체에 영향을 미치는 모든 변환 행렬을 곱함)

console.log(rect_1.calcTransformMatrix());
// [3, 0, 0, 2, 132, 151]

console.log(rect_2.calcTransformMatrix());
// [0.8660254037844387, 0.9999999999999999, -0.49999999999999994, 1.7320508075688774, 64.83364779650319, 349.6672955930064]

 

Although fabric.js provide us with many out-of-the-box resources, knowing how to work with transformation matrices allow us to develop interesting things.

 

fabric.js는 많은 기본 리소스를 제공하지만 변환 행렬로 작업하는 방법을 알면 흥미로운 것을 개발할 수 있습니다.


결론 및 정리

여기까지가 원문의 번역과 추가적인 설명을 덧붙인 내용이다. 이를 바탕으로 한 적당한 요약을 적어볼까 한다. 

 

*transformation matrix는 fabric.js와 같은 라이브러리에서 그래픽 요소를 효과적으로 변형시키는데 사용된다.

 

1. Base vectors of a Cartesian plane (데카르트 평면의 기본 벡터)

 

데카르트 평면(좌표 평면)에서는 X축과 Y축의 표준 단위 벡터인 î와 ĵ를 기저로 사용하여 벡터를 표현할 수 있다. 

 

2. Linear transformation (선형 변환)

 

선형 변환은 격자들의 형태가 직선의 형태이며, 격자 간의 간격이 균등하며, 원점이 변경되지 않도록 평면을 변경한다. 

î, ĵ, v 벡터의 값은 변환되지 않은 평면을 기준으로 계산됩니다.

변환 후에도 벡터 v는  2î + 2ĵ 로 동일하게 유지되며 원점도 동일하게 유지됩니다. 

 

3. Linear transformation with matrics (행렬을 사용한 선형 변환)

 

행렬은 계산을 더 쉽게 하기 위해 두 벡터를 묶은 것에 불과하다.

선형 변환을 설명하는 데에는 2x2 변환 행렬을 사용하여 간편하게 벡터의 최종 위치를 계산할 수 있다. 

 

4. Transformation : scaling 

 

scaling: 크기 조정 변형에 사용되는 행렬은 다음과 같다. (scaleX, scaleY는 각각 x축과 y축의 크기 조정 비율을 나타낸다.)

Scaling matrix

5. Transformation : flip

 

flip: 반전 조정 변형에 사용되는 행렬은 각 배율(scale) 축의 부호를 반전하는 것이다. (원점을 기준으로 x축, y축을 반전한다.)

Flip matrix

6. Transformation : skewing

 

skew: 요소의 가로 또는 세로 방향을 늘리거나 줄이는 데 사용되며, 요소를 비틀거나 기울일 때 사용된다. 

Shear matrix

 

7. Transformation: rotation

 

rotation: 회전 조정 변형에 사용되는 행렬은 회전 각도(a)에 따른 삼각법 계산으로 이루어진다. 

Rotation matrix

8. Transformation : translate

 

*transformation matrix는 주로 3x3 행렬로 표현된다.

translate: 이동 변형에 사용되는 매트릭스는 다음과 같다. (translateX와 translateY는 이동할 위치를 나타낸다.)

Translation matrix

 

9. Composing multiple transformations (다중 변환 구성)

 

위 각각의 변환을 다중 변환으로 구성할 수 있다.

주목할 점은 행렬 곱셈 순서에 따라 최종 결과가 달라진다는 것이다.

하지만, fabric.js에서는 내부적으로 객체에 대한 고정된 변환 순서가 있기 때문에 순서가 달라도 최종 결과가 동일하다.

skewY -> skewX -> scaling -> rotation -> translation (첫 번째 변환은 가장 오른쪽에서 시작됩니다.)

 

Fabric.js transformation order

 

 

10. Undoing transformations (변환 실행 취소)

 

역행렬을 이용하여 특정 변환을 실행 취소할 수 있다.

중요한 점은 원래 행렬 옆에 변환 행렬의 역을 곱해야한다. 위치가 달라지면 결과 값이 달라진다. 

 

- fabric.util.invertTransform: 변환 행렬의 역을 계산

- fabric.util.multiplyTransformMatrices: 두 개의 변환 행렬을 곱함.

- fabric.util.transformPoint: point에 transform 적용

 

11. The transformation matrix in fabric.js 

 

fabric.js에서 변환 행렬은 길이 6인 배열로 표현할 수 있다.

 

- calcOwnMatrix: 외부 변환을 고려하지 않고, 객체 평면의 변환 행렬을 반환. 

- calTransformMatrix: 캔버스 평면에서 객체 평면으로의 변환 행렬을 반환. 객체가 그룹 안에 있는 경우, 이러한 변환도 고려한다.

 

12. The different Cartesian planes in fabric.js

 

캔버스의 평면은 (*originX, originY가 기본값 left, top인 경우 y축은 아래로 증가함.) 데카르트 평면이다.

객체의 평면은 객체의 중심점을 원점으로 한다. 

 

- canvas.viewportTransform(): Canvas plane transformation matrix 
- rect.calcTransformMatrix(): Object plane transformation matrix

각 객체는 외부 변환(다른 평면으로부터 변환)을 고려하지 않는 각각의 변환 행렬(calcOwnMatrix())를 갖는다.

 

각 객체에 존재하는 총 변환을 구하려면 객체에 영향을 미치는 모든 변환 행렬을 곱해야한다.(fabric.util.multiplyTransformMatrices)

 

fabric.js에서 객체에 영향을 미치는 모든 변환 행렬을 곱하는 메서드는 calcTransformMatrix()이다.

 

예를 들자면, 

 

console.log(fabric.util.multiplyTransformMatrices(
  group.calcOwnMatrix(), -> group의 변환 행렬
  rect_1.calcOwnMatrix() -> rect_1의 변환 행렬
));

// [3, 0, 0, 2, 132, 151]

 

console.log(rect_1.calcTransformMatrix());
// [3, 0, 0, 2, 132, 151]

 

객체에 영향을 미치는 모든 변환 행렬을 곱한 결과 값객체의 calcTransformMatrix()의 결과 값이 같은 것을 확인할 수 있다.

 

이와 같은 fabric.js가 제공하는 메서드를 이용하여 다양한 것을 만들어 볼 수 있다.

끝.