Figma에서 CSS로: Linear Gradient 변환의 모든 것

easylogic
31 min readSep 22, 2024

--

Figma Linear Gradient를 CSS Linear Gradient로 변환하기

Figma Linear Gradient 는 CSS linear gradient 로 어떻게 변환할 수 있을까요?

이것을 하기 전에 먼저 Figma 의 Linear Gradient 를 왜 CSS 로 변환하는 글을 굳이 적어야 하는지 알아 봐야할 것 같습니다.

Figma 는 UI 디자인 툴로 널리 사용되고 있는데요.
Figma 에서 만든 Linear Gradient 를 웹 개발에서 사용하기 위해서는 Dev Mode 에서 코드를 보는 방법 말고는 현재 공식적으로 알려진 방법이 없습니다.

비슷하게도 메뉴얼에 속성은 적혀 있지만 어떤 원리로 변환되는지는 설명이 되어 있지 않습니다.

그러다 보니 온갖 플러그인들이 난무 하고 있고, 그게 실제로 정확한 CSS 로 변환되는지는 확인이 필요한 사항이 되어 버렸습니다.

계속 되는 Figma 이슈들

아직 Figma 커뮤니티에서 이슈로 관리되고 있는 이슈들이 있습니다.

https://forum.figma.com/t/how-to-convert-figma-gradient-to-css-gradient/20814

2023 년이 넘어가더라도 figma 에서 공식적인 답변이 없습니다.
하지만 신기하게도 분명 DevMode 에서는 css 로 변환된 코드를 보여주고 있습니다.

그러니깐 어딘가에 있는데 아무도 모르는 형태인것이죠.

figma 데이타를 변환 하다 보면 이런 것들이 생각보다 많습니다.

Figma 의 데이타를 과연 믿을 수 있을까요?

디자인 도구와 웹 개발 사이의 간극을 메우는 것은 생각보다 도전적인 과제입니다.

특히 Figma 처럼 데이타가 다 오픈되어 있는 상태에서도 명확한 가이드가 제공되지 않으면
여러가지 테스트를 너무 많이 해야합니다.

그럼 이러한 테스트 들이 의미가 있을지 고민을 한번 해볼 필요가 있습니다.

만약에 이러한 테스트가 정상적인 값을 내놓을 수 있다면
Figma에도 어느정도 공식이 있다는 것이기 때문에
다른 데이타도 변환하기 훨씬 쉬워질거에요.

Figma 의 Linear Gradient 를 한번 봅시다.

https://www.figma.com/plugin-docs/api/Paint/#gradientpaint

GradientPaint

`type`: 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND' [readonly]

The string literal representing the type of paint this is. Always check the type before reading other properties.

`gradientTransform`: Transform [readonly]

The positioning of the gradient within the layer.

`gradientStops`: ReadonlyArray<ColorStop> [readonly]

Array of colors and their position within the gradient.

figma 는 fill 이나 stroke 에 대해서 gradient 를 정의할 수 있습니다.
기본적인 구조는 gradientTransform 이라는 행렬을 통해서 표현됩니다.

gradientStops 은 각각의 색상과 그 색상이 그라디언트 내에서 위치한 위치를 나타냅니다.

여가까지만 보면 크게 다를건 없어보입니다.

자, 밑에 이미지를 한번 보시죠.

네, 특정 위치 아무 곳에나 그라디언트를 정의할 수 있습니다.
하지만 CSS 에서는 이러한 특정 위치를 정의할 수 있는 방법이 없습니다.

구조가 다른 것이죠.

CSS 의 Linear Gradient 도 봅시다.

W3C 의 문서에는 Linear Gradient 의 정의를 보여줍니다.

https://www.w3.org/TR/css-images-3/#linear-gradients

규칙은 위치를 지정할 수 없고, 특정 영역의 중심을 기준으로 그라디언트를 정의합니다.
Figma 의 기본 규칙과 전혀 다르죠.

Figma Linear Gradient vs CSS Linear Gradient

이제 이 둘의 차이를 좀 더 디테일 하게 살펴보죠.

1. 좌표계

시작과 끝을 정의하는 방식이 다르다고 해야겠네요.

Figma는 width, height 를 기준으로 0, 1 사이의 값을 가집니다.

이미지 : https://gist.github.com/yagudaev/0c2b89674c6aee8b38cd379752ef58d0?permalink_comment_id=4270301#gistcomment-4270301

대략 이정도라고 보시면 되요.

CSS 는 중심을 기준으로 width, height 를 기준으로 그라디언트를 정의합니다.

2. 각도 기준

두번 째는 각도를 보는 기준이 다릅니다.

Figma 는 좌측 중간(0, 0.5) 에서 시작하여 우측 중간(1, 0.5)으로 기본 각도를 0도로 정의합니다.

CSS 는 하단 중간(0.5, 1) 에서 시작하여 상단 중간(0.5, 0)으로 기본 각도를 0도로 정의합니다.

이렇게 보면 90도 차이가 나는 것 같지만 실제로는 180도 차이가 납니다.

180 도 차이가 나는 것은 방향에 대한 정의와 Figma Linear Gradient 가 실제로 0도를 가리키는 방향이 다르기 때문입니다.
이건 공식적인 문서에서도 언급되어 있지 않습니다.

3. 그라디언트 범위

Figma 의 그라디언트는 요소의 경계를 아주 자유롭게 벗어날 수 있습니다.
width, height 랑 상관 없이 어디든 갈 수 있습니다.

하지만 CSS는 중심점을 기준으로만 그라디언트를 정의할 수 있습니다.

결국은 이런 차이점때문에 일반적인 방법으로는 Figma -> CSS 로 바로 변환하는 것이 불가능합니다.

예를 들어 중심점이 아닌 요소에 Figma의 그라디언트를 적용하면 어떻게 될까요?
CSS 에서는 중심점을 기준으로 그라디언트를 정의하기 때문에 요소의 경계를 벗어나는 부분은 보이지 않습니다.

Figma 의 그라디언트를 CSS 로 변환하는 방법

위에서 정의한 몇가지 차이점들을 가지고 계속해서 맞춰나가 보도록 하겠습니다.

이 글을 적을 때 사용했던 참고 코드 및 링크 들입니다.

https://forum.figma.com/t/how-to-convert-figma-gradient-to-css-gradient/20814

https://forum.figma.com/t/need-help-with-gradienttranform-matrix/26792/3

https://9elements.com/blog/gradient-angles-in-css-figma-and-sketch/

https://www.w3.org/TR/css-images-3/#example-b6f5099c

https://github.com/yagudaev/css-gradient-to-figma/blob/main/src/shared/color.ts

https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/master/src/helpers/extractLinearGradientStartEnd.ts

0. 준비

Figma 는 여러가지 그라디언트를 제공합니다.

  • Linear Gradient
  • Radial Gradient
  • Angular Gradient
  • Diamond Gradient

그 중에서도 가장 많이 사용되는 Linear Gradient 를 변환하는 방법을 알아보도록 하겠습니다.

Linear Gradient 의 기본 구조

type GradientPaint = {
gradientTransform: Transform;
gradientStops: ColorStop[];
};

Linear Gradient 는 gradientTransform 과 gradientStops 으로 구성되어 있습니다.

gradientTransform 은 그라디언트의 위치와 방향을 정의하는 행렬이고, gradientStops 은 그라디언트의 색상과 위치를 정의하는 배열입니다.

gradientTransform 의 구조

Figma 의 Linear Gradient 를 CSS Linear Gradient 로 변환할 때 행렬 연산을 이용해야합니다.

gradientTransform 은 Figma 의 Gradient 의 위치를 표준 좌표 공간으로 변환하는 행렬입니다.

그래서 gradientTransform 의 역행렬을 이용해서 실제 시작점과 끝점을 계산해야 합니다.

식으로 나타내볼까요?

Figma의 gradientTransform 행렬 A를 사용하여 그라디언트 좌표 공간에서 표준 공간으로의 변환을 나타내는 수식은 다음과 같습니다:

여기서 (xg​,yg​)는 그라디언트 좌표 공간의 점이고, (xs​,ys​)는 표준 공간의 점입니다.

우리가 원하는 것은 표준 공간에서 그라디언트 좌표 공간으로의 변환이므로, gradientTransform 의 역행렬을 사용해야 합니다:

이 수식을 사용하여 표준 공간의 점을 그라디언트 좌표 공간으로 변환할 수 있습니다.

1. 시작점과 끝점 계산

FFigma 의 linear gradient 의 경우 기본적으로 시작점과 끝점이 정의되어 있습니다.
FFigma 의 경우 기본적으로 시작점은 [0, 0.5] 끝점은 [1, 0.5] 로 정의되어 있습니다.
하지만 이러한 부분이 공식 문서에 나와 있지는 않습니다.

코드로 나타내면 아래와 같습니다.

function applyMatrixToPoint(matrix: number[][], point: number[]) {
const x = matrix[0][0] * point[0] + matrix[0][1] * point[1] + matrix[0][2];
const y = matrix[1][0] * point[0] + matrix[1][1] * point[1] + matrix[1][2];
return [x, y];
}

function inverseMatrix(matrix: number[][]) {
const det = matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
const invDet = 1 / det;
return [
[
matrix[1][1] * invDet,
-matrix[0][1] * invDet,
(matrix[0][1] * matrix[1][2] - matrix[0][2] * matrix[1][1]) * invDet,
],
[
-matrix[1][0] * invDet,
matrix[0][0] * invDet,
(matrix[0][2] * matrix[1][0] - matrix[0][0] * matrix[1][2]) * invDet,
],
];
}
// 기본 시작점과 끝점
const startPoint = [0, 0.5];
const endPoint = [1, 0.5];
// gradient 의 시작점과 끝점
const oldStartPoint = applyMatrixToPoint(inverseMatrix(gradientTransform), startPoint);
const oldEndPoint = applyMatrixToPoint(inverseMatrix(gradientTransform), endPoint);

하나의 함수로 표현을 해보면 아래와 같습니다.

function calculateActualPositions(gradientTransform: number[][], width: number, height: number) {
const matrixInverse = inverseMatrix(gradientTransform);
const figmaStart = applyMatrixToPoint(matrixInverse, [0, 0.5]);
const figmaEnd = applyMatrixToPoint(matrixInverse, [1, 0.5]);

return {
start: { x: figmaStart.x * width, y: figmaStart.y * height },
end: { x: figmaEnd.x * width, y: figmaEnd.y * height },
};
}

생각보다 어렵지 않죠? 하지만 공식 문서에 없기 때문에 이게 진짜인지 아닌지 계속 확인을 해야합니다.

대부분의 figma 플러그인이 이러한 방식을 쓰지 않고 있기 때문에
실제로 figma 가 만들어주는 css 와 다릅니다.

일단 figma 의 특정 노드에서 width, height 기준으로 start, end 가 어디에 존재하는지 알았습니다.

이제 첫발을 내딛었습니다. 그 다음으로 넘어가 보시죠.

2. 그라디언트 각도 계산 및 보정

Figma와 CSS의 각도 차이를 확인해봅시다.

  1. Figma의 각도 기준:
  • 0도: 위쪽에서 아래쪽으로 (6시 방향)
  • 각도 증가 방향: 시계 방향
  1. CSS의 각도 기준:
  • 0도: 아래에서 위로 (12시 방향)
  • 각도 증가 방향: 시계 방향

이 관점으로 보면 정확히 180도가 차이가 나게 됩니다.

function calculateAngle(start: Point, end: Point): number {
const dx = end.x - start.x;
const dy = end.y - start.y;

// atan2 함수를 사용하여 각도 계산 (라디안)
// atan2는 -π에서 π 사이의 값을 반환
let angle = Math.atan2(dy, dx);
// 라디안을 도로 변환
angle = angle * (180 / Math.PI);
// Figma의 각도 체계로 변환
angle = (angle - 90) % 360;
// 음수 각도를 양수로 변환 (예: -90도 -> 270도)
return angle < 0 ? angle + 360 : angle;
}

각도는 보통 atan2 함수를 이용해서 계산합니다. 다만 여기는 함정이 하나 있는데요.
atan2 함수는 x 축 기준으로 각도를 계산하기 때문에 0도가 오른쪽이라는 점을 이용해서 90도를 빼줍니다.

그래야 figma 의 0 도의 위치값으로 맞출 수 있어요. (0도가 위쪽이라는 점을 이용)
그래서 먼저 계산은 figma 기준으로 하고 최종적으로 css 각도를 표현할 때 180도를 빼주면 됩니다.

const angle = calculateAngle(start, end);
const cssAngle = Math.round(angle - 180);

일단은 계산을 위해서 angle 을 사용하고 표현을 위해서 cssAngle 을 나중에 사용할 예정입니다.

여기까지 왔으면 이제 기본은 했습니다.
CSS Linear Gradient 로 들어가기 위한 몇가지를 좀 더 알아보도록 하겠습니다.

3. CSS 그라디언트 길이 계산

2가지 관점에서 확인이 필요한데요.

  1. Figma 의 시작점과 끝점은 길이가 고정이 아닙니다.
  2. CSS 의 시작점과 끝점은 중심점 기준으로 고정입니다. (공식이 있다는 것을 확인)

CSS Linear Gradient Length 를 구하는 방식은 W3C 표준 문서를 봅시다.
https://www.w3.org/TR/css-images-3/#example-b6f5099c

figma 와 css 가 일치하지 않는 가장 큰 이유중에 이 부분이 있습니다.
대부분의 플러그인들이 단순히 gradientStops의 stop.position x 100 을 하기 때문에 실제 위치와 다른 결과를 보여줍니다.

왜냐하면 figma 의 linear gradient 의 시작점과 끝점이 고정이 아니고, 길이도 고정이 아니기 때문입니다.

계속해서 CSS Linear Gradient 의 구조를 봅시다.

linear-gradient(to right, red 0%, blue 100%);

이렇게 표현이 되는데요.
0%, 100% 는 그라디언트의 시작점과 끝점을 의미합니다.
CSS Linear Gradient 상에서 시작점과 끝점을 구해봅시다.

gradientLength=∣width⋅sin(θ)∣+∣height⋅cos(θ)∣

GradientLength 를 계산을 해봅니다. 그런 다음 이를 이용해서 시작점과 끝점을 계산합니다.

function calculateGradientLength(width: number, height: number, angleDeg: number): number {
const angleRad = (angleDeg * Math.PI) / 180;
return Math.abs(width * Math.sin(angleRad)) + Math.abs(height * Math.cos(angleRad));
}

const gradientLengthHalf = calculateGradientLength(width, height, angle) / 2;
const { cssStart, cssEnd } = calculateCSSStartEnd(center, angle, gradientLengthHalf);

자, css 기준으로 start, end 가 어디에 위치하는지 알았습니다.

그럼 여기서 질문하나 드립니다.

figma 의 그라디언트 stop 의 위치는 css start, end 의 위치에 완전히 맞추는게 맞을가요?

아니면 중간에 빈 공간이 생기는 경우가 있을까요?

4. GradientStop Mapping

위의 질문에 답을 할 차례입니다.

figma 의 그라디언트 stop 의 위치는 css 의 start, end 와 처음부터 맞질 않습니다.
그래서 우리가 해야할 것은 css 의 start, end 를 잇는 직선 가운데 그라디언트 stop 의 위치를 맞추는 것입니다.

이건 코드를 바로 보면서 이해를 해보도록 하겠습니다.

매핑 함수를 한번 보시죠.

function mapGradientStops(stops: ColorStop[], figmaStart: Point, figmaEnd: Point, cssStart: Point, cssEnd: Point) {
const figmaVector = {
x: figmaEnd.x - figmaStart.x,
y: figmaEnd.y - figmaStart.y,
};
const cssVector = {
x: cssEnd.x - cssStart.x,
y: cssEnd.y - cssStart.y,
};
const cssLength = Math.sqrt(cssVector.x ** 2 + cssVector.y ** 2);
return stops.map((stop) => {
const offsetX = figmaStart.x + figmaVector.x * stop.position;
const offsetY = figmaStart.y + figmaVector.y * stop.position;
const projectedPosition = projectPointOnLine({ x: offsetX, y: offsetY }, cssStart, cssEnd);
const relativePosition =
Math.sqrt((projectedPosition.x - cssStart.x) ** 2 + (projectedPosition.y - cssStart.y) ** 2) / cssLength;
return {
position: relativePosition,
color: stop.color,
};
});
}

기본 원리는 figma 의 그라디언트 벡터를 이용해서 css 의 그라디언트 벡터로 투영을 하는 것입니다.

각 Figma 의 GradientStop 에 대해 다음 과정을 수행합니다:

a. Figma 공간에서의 오프셋 계산:

const offsetX = figmaStart.x + figmaVector.x * stop.position;
const offsetY = figmaStart.y + figmaVector.y * stop.position;

b. CSS 그라디언트 선으로 투영:

const projectedPosition = projectPointOnLine({ x: offsetX, y: offsetY }, cssStart, cssEnd);

Figma 공간의 점을 CSS 그라디언트 선 상으로 수직 투영합니다.

c. 상대적 위치 계산:

const relativePosition =
Math.sqrt((projectedPosition.x - cssStart.x) ** 2 + (projectedPosition.y - cssStart.y) ** 2) / cssLength;

각도 개념으로 보면 CSS Linear Gradient 의 선과 Figma 의 선이 평행하게 되는 것입니다. (180도로 방향만 다를 뿐)

그래서 CSS 의 start, end 를 있는 선이 있다고 쳤을 때 평행하게 내려줄 수 있으면 CSS 의 영역으로 들어오게 되는 것입니다.

투영하는 방식은 vector dot product 를 사용합니다.

참고: https://yamyamcoding.oopy.io/1395eec8-125c-42a1-ae71-c3628af3177c

결론

Figma 가 공식 문서에 어떤 내용을 적어둔 게 아니기 때문에 여러가지 플러그인들과 참고문서들을 맞춰가면서 계산을 해야했습니다.

Figma 의 Linear Gradient 가 제대로 변환이 될 수 있도록 가이드가 있었으면 좋겠네요.

이런 gradient 가 있다고 했을 때 피그마는 아래와 같이 보여줍니다. 100%를 넘어가는 영역이 나오죠.

실제 변환된 결과물은 아래와 같습니다. 대략 비슷하게 되었습니다.

마지막에 소수점이 안 맞는 부분은 아마도 중간에 round 를 많이 하다 보니 바뀌었을 가능성이 있습니다.

이것으로 Figma Linear Gradient 를 CSS Linear Gradient 로 유사(?)하게 바뀌는 것을 확인 할 수 있었습니다.

아래는 사용된 소스코드 입니다.

interface Point {
x: number;
y: number;
}


/**
* Figma의 선형 그라디언트를 CSS 선형 그라디언트로 변환하는 메인 함수
* @param gradientData Figma의 그라디언트 데이터
* @param width 요소의 너비
* @param height 요소의 높이
* @returns CSS 선형 그라디언트 문자열
*/
export function figmaLinearGradientToCSS(gradientData: GradientPaint, width: number, height: number): string {
// 1. Figma 그라디언트의 실제 시작점과 끝점 계산
const { start, end } = calculateActualPositions(gradientData.gradientTransform, width, height);
// 2. 요소의 중심점 계산
const center = { x: width / 2, y: height / 2 };
// 3. 그라디언트 각도 계산
// Figma: 오른쪽이 0도, 시계 방향으로 증가
const figmaAngle = calculateAngle(start, end);
// 3.1 Figma 각도를 CSS 각도로 변환
// CSS: 위쪽이 0도, 시계 방향으로 증가
// 변환 공식: cssAngle = (figmaAngle + 90) % 360
const cssAngle = Math.round((figmaAngle - 180) % 360);
// 4. CSS 그라디언트 길이 계산
const gradientLength = calculateGradientLength(width, height, cssAngle);
const gradientLengthHalf = gradientLength / 2;
// 5. CSS 그라디언트의 시작점과 끝점 계산 (중심점 기준)
const { cssStart, cssEnd } = calculateCSSStartEnd(center, cssAngle, gradientLengthHalf);
// 6. Figma의 그라디언트 정지점을 CSS 공간으로 매핑
const stops = mapGradientStops(gradientData.gradientStops as ColorStop[], start, end, cssStart, cssEnd);
// 7. CSS 선형 그라디언트 문자열 생성
return `linear-gradient(${cssAngle}deg, ${stops
.map((stop) => `${rgbaToHex(stop.color)} ${(stop.position * 100).toFixed(2)}%`)
.join(', ')})`;
}
/**
* Figma의 그라디언트 변환 행렬을 사용하여 실제 시작점과 끝점 계산
* @param gradientTransform Figma의 그라디언트 변환 행렬
* @param width 요소의 너비
* @param height 요소의 높이
* @returns 실제 픽셀 단위의 시작점과 끝점
*/
function calculateActualPositions(gradientTransform: number[][], width: number, height: number) {
const matrixInverse = inverseMatrix(gradientTransform);
// gradient 공간에서 시작점과 끝점을 표준 공간으로 변환
// gradient 공간에서도 시작점과 끝점은 [0, 0.5] [1, 0.5] 이지만
// 표준 공간에서는 시작점과 끝점이 0도 기준으로 변환되어야 함
const normalizedStart = applyMatrixToPoint(matrixInverse, [0, 0.5]);
const normalizedEnd = applyMatrixToPoint(matrixInverse, [1, 0.5]);
console.log({
start: { x: normalizedStart.x * width, y: normalizedStart.y * height },
end: { x: normalizedEnd.x * width, y: normalizedEnd.y * height },
});
// 표준 공간에서 시작점과 끝점을 픽셀 단위로 변환
// figma 공간에서 0도 기준으로 변환되어야 함
return {
start: { x: normalizedStart.x * width, y: normalizedStart.y * height },
end: { x: normalizedEnd.x * width, y: normalizedEnd.y * height },
};
}
/**
* 시작점과 끝점을 사용하여 그라디언트 각도 계산
* @param start 시작점
* @param end 끝점
* @returns Figma 기준 그라디언트 각도 (도 단위)
*
* Figma 각도 체계:
* - 오른쪽: 0도
* - 위쪽: 90도
* - 왼쪽: 180도
* - 아래쪽: 270도
* - 시계 방향으로 각도가 증가
*/
function calculateAngle(start: Point, end: Point): number {
const dx = end.x - start.x;
const dy = end.y - start.y;
// atan2 함수를 사용하여 각도 계산 (라디안)
// atan2는 -π에서 π 사이의 값을 반환
let angle = Math.atan2(dy, dx);
// 라디안을 도로 변환
angle = angle * (180 / Math.PI);
// Figma의 각도 체계로 변환
angle = (angle - 90) % 360;
// 음수 각도를 양수로 변환 (예: -90도 -> 270도)
return angle < 0 ? angle + 360 : angle;
}
/**
* 요소의 크기와 각도를 고려하여 CSS 그라디언트 길이 계산
* @param width 요소의 너비
* @param height 요소의 높이
* @param angleDeg 그라디언트 각도 (도 단위, CSS 기준)
* @returns 그라디언트 길이
*
* CSS 각도 체계:
* - 위쪽: 0도
* - 오른쪽: 90도
* - 아래쪽: 180도
* - 왼쪽: 270도
* - 시계 방향으로 각도가 증가
*/
function calculateGradientLength(width: number, height: number, angleDeg: number): number {
const angleRad = (angleDeg * Math.PI) / 180;
// 대각선 길이 계산 (그라디언트가 요소를 완전히 커버하도록)
return Math.abs(width * Math.sin(angleRad)) + Math.abs(height * Math.cos(angleRad));
}
/**
* CSS 그라디언트의 시작점과 끝점 계산 (중심점 기준)
* @param center 요소의 중심점
* @param cssAngle CSS 그라디언트 각도 (도 단위)
* @param gradientLengthHalf 그라디언트 길이의 절반
* @returns CSS 그라디언트의 시작점과 끝점
*/
function calculateCSSStartEnd(center: Point, cssAngle: number, gradientLengthHalf: number) {
// CSS 각도를 라디안으로 변환 (위쪽이 0도, 시계 방향으로 증가)
const cssAngleRad = (cssAngle - 90) * (Math.PI / 180);
return {
cssStart: {
x: center.x - gradientLengthHalf * Math.cos(cssAngleRad),
y: center.y - gradientLengthHalf * Math.sin(cssAngleRad),
},
cssEnd: {
x: center.x + gradientLengthHalf * Math.cos(cssAngleRad),
y: center.y + gradientLengthHalf * Math.sin(cssAngleRad),
},
};
}
/**
* Figma의 그라디언트 정지점을 CSS 공간으로 매핑
* @param stops Figma의 그라디언트 정지점들
* @param figmaStart Figma 그라디언트 시작점
* @param figmaEnd Figma 그라디언트 끝점
* @param cssStart CSS 그라디언트 시작점
* @param cssEnd CSS 그라디언트 끝점
* @returns CSS 그라디언트에 맞게 변환된 정지점들
*/
function mapGradientStops(stops: ColorStop[], figmaStart: Point, figmaEnd: Point, cssStart: Point, cssEnd: Point) {
const figmaVector = {
x: figmaEnd.x - figmaStart.x,
y: figmaEnd.y - figmaStart.y,
};
const cssVector = {
x: cssEnd.x - cssStart.x,
y: cssEnd.y - cssStart.y,
};
const cssLength = Math.sqrt(cssVector.x ** 2 + cssVector.y ** 2);
return stops.map((stop) => {
// Figma 공간에서의 정지점 실제 픽셀 위치 계산 (offset)
const offsetX = figmaStart.x + figmaVector.x * stop.position;
const offsetY = figmaStart.y + figmaVector.y * stop.position;
// offset을 CSS 그라디언트 선 상의 위치로 투영
const projectedPosition = projectPointOnLine({ x: offsetX, y: offsetY }, cssStart, cssEnd);
// 투영된 위치를 CSS 그라디언트 상의 상대적 위치로 변환
// 0~1 범위를 벗어날 수 있음 (100% 초과 또는 음수 퍼센트 허용)
const relativePosition =
Math.sqrt((projectedPosition.x - cssStart.x) ** 2 + (projectedPosition.y - cssStart.y) ** 2) / cssLength;
return {
position: relativePosition,
color: stop.color,
};
});
}
/**
* 점을 선분 위로 투영하는 함수
* @param point 투영할 점
* @param lineStart 선분의 시작점
* @param lineEnd 선분의 끝점
* @returns 선분 위로 투영된 점의 좌표
*/
function projectPointOnLine(point: Point, lineStart: Point, lineEnd: Point): Point {
const lineVector = {
x: lineEnd.x - lineStart.x,
y: lineEnd.y - lineStart.y,
};
const pointVector = {
x: point.x - lineStart.x,
y: point.y - lineStart.y,
};
const lineLength = Math.sqrt(lineVector.x ** 2 + lineVector.y ** 2);
const dotProduct = pointVector.x * lineVector.x + pointVector.y * lineVector.y;
const projectionLength = dotProduct / lineLength;
// 투영된 점의 좌표 계산. 선분 밖의 점도 허용
return {
x: lineStart.x + (lineVector.x / lineLength) * projectionLength,
y: lineStart.y + (lineVector.y / lineLength) * projectionLength,
};
}
function inverseMatrix(matrix: number[][]): number[][] {
const [a, b, c] = matrix[0];
const [d, e, f] = matrix[1];
const det = a * e - b * d;
return [
[e / det, -b / det, (b * f - c * e) / det],
[-d / det, a / det, (c * d - a * f) / det],
];
}
function applyMatrixToPoint(matrix: number[][], point: number[]): Point {
return {
x: matrix[0][0] * point[0] + matrix[0][1] * point[1] + matrix[0][2],
y: matrix[1][0] * point[0] + matrix[1][1] * point[1] + matrix[1][2],
};
}
function toHex(n: number): string {
return ('0' + n.toString(16)).slice(-2);
}
function rgbaToHex(color: RGBA): string {
const r = Math.round(color.r * 255);
const g = Math.round(color.g * 255);
const b = Math.round(color.b * 255);
const a = Math.round(color.a * 255);
if (color.a === 1) {
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
}
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(a)}`.toUpperCase();
}

--

--

easylogic
easylogic

Written by easylogic

걸작을 만드는 사람. 에디터를 만드는 사람.

No responses yet