선형보간과 linear-gradient
이번에는 CSS linear-gradient를 보겠습니다. 이름은 “그라디언트”라서 디자인 기능처럼 보이지만, 안쪽을 열어보면 꽤 정직한 수학 함수입니다.
브라우저는 박스 안의 각 픽셀을 보고 이렇게 묻습니다.
“너는 gradient 축 위에서 어디쯤 있니?”
그리고 그 위치에 맞춰 색을 섞습니다. 그러니까 linear-gradient는 색상 목록이 아니라, 픽셀을 축에 투영하고 color stop 사이를 보간하는 함수입니다. 색칠 공부 같지만 속은 좌표 계산입니다. 은근히 야무집니다.
CSS 각도는 조금 특이하다
CSS에서 linear-gradient(0deg, ...)는 오른쪽이 아니라 위쪽을 향합니다. 일반적인 수학 좌표계에 익숙하면 처음에 살짝 배신감이 듭니다.
0deg = to top
90deg = to right
그래서 각도 theta에서 gradient axis는 이렇게 잡을 수 있습니다.
axis = (sin(theta), -cos(theta))
theta = 0deg이면 (0, -1)이 됩니다. 화면에서 y가 아래로 증가하니까 위쪽은 음수 y입니다. 브라우저 좌표계가 여기서 또 슬쩍 등장합니다. 지난 시간 내용이 몰래 따라왔죠.
gradient line은 보이는 대각선이 아니다
여기서 중요한 함정이 하나 있습니다.
gradient line은 박스 중심을 지나지만, “박스 안에서 실제로 보이는 선분”과 같은 길이가 아닙니다. 브라우저는 박스의 네 꼭짓점을 gradient 축에 투영했을 때 전체 범위를 덮도록 선의 길이를 잡습니다.
그래야 박스의 한쪽 끝은 첫 색에, 반대쪽 끝은 마지막 색에 안정적으로 닿습니다.
L = width * |axis.x| + height * |axis.y|
L = width * |sin(theta)| + height * |cos(theta)|
예를 들어 200 x 100 박스에서 90deg라면 axis는 오른쪽을 봅니다. 길이는 200입니다. 그런데 45deg라면 가로와 세로가 둘 다 영향을 줍니다.
L = width * |sin(45deg)| + height * |cos(45deg)|
이 값은 단순한 대각선 길이와 다릅니다. “대각선이면 피타고라스 아닌가요?” 하고 묻고 싶어질 수 있습니다. 좋은 질문입니다. 하지만 여기서는 박스 내부의 한 선분 길이가 아니라, 모든 픽셀을 축에 투영했을 때 필요한 전체 투영 범위를 구하는 중입니다.
각 픽셀은 축 위로 투영된다
박스 중심을 center라고 하고, 픽셀 위치를 pixel이라고 하겠습니다. 픽셀이 gradient axis 위에서 얼마나 떨어져 있는지는 dot product로 계산합니다.
t = dot(pixel - center, axis)
이 값을 0..1 범위로 바꾸면 color stop을 샘플링할 수 있습니다.
u = (t + L / 2) / L
start = center - axis * L / 2
end = center + axis * L / 2
u = 0이면 시작 색, u = 1이면 끝 색입니다. 중간이면 두 색을 섞습니다.
color = color0 * (1 - u) + color1 * u
물론 실제 CSS 색상 보간은 색 공간에 따라 더 복잡해질 수 있습니다. sRGB에서 섞느냐, OKLCH 같은 공간에서 섞느냐에 따라 중간색이 달라집니다. 하지만 기본 뼈대는 “축 위의 위치를 구하고, 그 위치로 색을 샘플링한다”입니다.
에디터에서는 무엇을 저장할까
디자인 에디터에서 gradient를 만들 때는 CSS 문자열을 그대로 모델로 삼기보다, 보통 이런 값을 저장하는 편이 다루기 쉽습니다.
- 중심점
- 축 방향
- stop 목록
- 각 stop의 위치
0..1 - 각 stop의 색
사용자가 gradient handle을 드래그하면 시작점과 끝점으로 axis를 만들고, stop marker는 그 축 위의 위치값으로 배치합니다. CSS로 내보낼 때만 linear-gradient(...) 문자열로 직렬화하면 됩니다.
데모의 초록 선은 CSS가 색상 보간에 사용하는 gradient line 길이 L을 보여줍니다. 박스 안에서 중심을 통과하며 실제로 보이는 교차선 길이는 아래처럼 따로 계산할 수 있습니다.
visibleChord = min(width / |axis.x|, height / |axis.y|)
이 둘을 구분해야 gradient editor의 핸들이 “왜 저 위치에 있지?” 하는 얄미운 버그를 피할 수 있습니다. 수학이 조금 정확하면 UI가 훨씬 덜 삐걱댑니다.
데모에서 볼 것
angle과 stop position을 바꿔보면서 픽셀 색이 축 투영값으로 결정된다는 점을 확인합니다.
오늘의 핵심은 이것입니다. linear-gradient는 “예쁜 배경 문법”이 아니라, 박스 위에 놓인 1차원 좌표계를 색으로 읽는 함수입니다.