거리 함수와 radial/conic-gradient
이번에는 원형으로 퍼지는 색, radial-gradient입니다. 그리고 옆에 살짝 붙여서 conic-gradient도 같이 보겠습니다.
지난 시간의 linear-gradient가 “축 위에서 어디쯤이냐”를 물었다면, radial-gradient는 이렇게 묻습니다.
“중심에서 얼마나 멀리 있니?”
그리고 conic-gradient는 이렇게 묻습니다.
“중심을 기준으로 몇 도 방향이니?”
질문이 참 단순하죠. 그런데 이 질문 두 개가 원형 그라디언트, 각도형 컬러 피커, 브러시 크기, 원형 hit area, 핸들 근접 판정까지 줄줄이 데리고 옵니다. 중심과 거리, 중심과 각도. 이 조합은 그래픽 에디터에서 생각보다 자주 나타납니다.
중심에서 거리 재기
한 픽셀의 위치를 pixel, 중심을 center라고 하면 둘의 차이는 벡터입니다.
v = pixel - center
이 벡터의 길이를 구하면 중심에서 픽셀까지의 거리가 됩니다.
d = length(pixel - center)
반지름이 radius라면, 이 거리를 0..1 값으로 정규화할 수 있습니다.
u = clamp(d / radius, 0, 1)
u = 0이면 중심, u = 1이면 반지름 지점입니다. 색상 stop은 이 u 값을 기준으로 샘플링됩니다.
여기서 “거리장”이라는 말을 쓸 수 있습니다. 각 픽셀마다 중심으로부터의 거리값을 하나씩 가지고 있는 장입니다. 이름이 좀 학술적으로 들리지만, 사실은 모든 픽셀에 “너 중심에서 몇 점?” 하고 점수를 매기는 겁니다.
color stop은 위치표다
gradient의 color stop은 그냥 색 목록이 아닙니다. “이 위치에서는 이 색”이라는 표입니다.
radial-gradient(
circle farthest-corner at 50% 48%,
#f6c85f 0%,
#0d9488 36%,
#9ac8eb 68%,
#101820 100%
)
위 코드는 이렇게 읽으면 됩니다.
center = (50%, 48%)
radius = distance(center, farthestCorner)
u = distance(pixel, center) / radius
color = interpolate stops by u
예를 들어 u = 0.52라면 36%와 68% 사이에 있으니, 브라우저는 teal과 blue 사이를 보간합니다. 즉 color stop은 “색을 몇 번째로 칠할지”가 아니라, 거리 함수의 결과값을 색으로 바꾸는 lookup table에 가깝습니다. 말이 좀 그럴싸하죠. 하지만 하는 일은 착합니다.
CSS에서 circle farthest-corner를 쓰면 반지름은 중심에서 가장 먼 모서리까지의 거리입니다.
r = max(
distance(center, topLeft),
distance(center, topRight),
distance(center, bottomRight),
distance(center, bottomLeft)
)
그래서 중심을 드래그하면 색이 단순히 “위치만” 바뀌는 게 아닙니다. 중심에서 가장 먼 모서리도 바뀌고, 그 결과 100% stop이 놓이는 실제 픽셀 거리도 바뀝니다. 이 부분을 안 보면 gradient editor를 만들 때 stop marker가 묘하게 밀립니다. 어? 왜 맞는데 안 맞지? 하는 그 맛없는 버그가 여기서 나옵니다.
원과 타원은 살짝 다르다
circle은 x와 y를 같은 비율로 봅니다. 그런데 ellipse는 가로 반지름과 세로 반지름이 다릅니다. 그래서 먼저 좌표를 타원 기준으로 정규화한 다음 길이를 재면 됩니다.
q = ((x - cx) / rx, (y - cy) / ry)
u = length(q)
이렇게 하면 타원도 “정규화된 공간에서는 원”처럼 다룰 수 있습니다. 그래픽스에서 자주 쓰는 수법입니다. 어려운 도형을 바로 상대하지 말고, 쉬운 좌표계로 끌고 와서 계산하는 겁니다. 수학도 가끔 협상을 합니다.
CSS 파라미터로 보면
CSS의 radial-gradient에서 shape, size, position은 결국 중심과 반지름을 정하는 말입니다.
background: radial-gradient(circle at 50% 50%, red, blue);
이 문장은 대략 이렇게 읽을 수 있습니다.
center = box center
shape = circle
color = sample by distance from center
디자인 에디터에서는 중심점 핸들과 반지름 핸들을 따로 노출하면 모델이 깔끔해집니다. 사용자는 핸들을 움직이고, 에디터는 그 값을 CSS 문자열로 바꿉니다.
conic-gradient는 거리 대신 각도다
conic-gradient는 중심점까지는 radial-gradient와 같습니다. 하지만 색을 고르는 기준이 거리가 아니라 각도입니다.
background: conic-gradient(
from 0deg at 50% 48%,
#f6c85f 0deg,
#0d9488 120deg,
#9ac8eb 240deg,
#f6c85f 360deg
);
CSS gradient 각도는 화면 위쪽을 0deg로 보고, 시계 방향으로 증가한다고 생각하면 편합니다. 그래서 화면 좌표의 벡터 v = pixel - center에서 conic angle은 이렇게 잡을 수 있습니다.
dx = pixel.x - center.x
dy = pixel.y - center.y
angle = atan2(dx, -dy)
보통 수학 시간의 atan2(dy, dx)와 인자 순서가 달라 보이죠. 괜찮습니다. 우리가 원하는 기준이 “오른쪽 0도”가 아니라 “위쪽 0도”라서 그렇습니다. CSS가 살짝 장난을 치는 게 아니라, 화면 좌표계에 맞춘 겁니다. 그래도 처음 보면 조금 얄밉긴 합니다.
여기서 color stop은 0%..100% 대신 0deg..360deg 위치표가 됩니다.
angle = 156deg
active stop interval = 120deg -> 240deg
color = interpolate stops by angle
그래서 radial과 conic은 같은 “중심점 기반 gradient”처럼 보이지만, 샘플링 함수가 다릅니다.
radial: color = f(distance(pixel, center))
conic: color = f(angle(pixel - center))
이 차이를 분리해두면 UI도 좋아집니다. radial editor는 중심점과 반지름/타원 핸들이 중요하고, conic editor는 중심점과 각도 stop marker가 중요합니다.
편집기에서 쓰이는 곳
원형 브러시 미리보기, spotlight 효과, radial color picker는 모두 거리장을 시각화한 것입니다. 사용자가 핸들 근처를 클릭했는지 판단할 때도 같은 거리 계산을 씁니다.
회전된 타원형 gradient처럼 복잡해 보이는 경우도 있습니다. 이때는 포인터를 먼저 오브젝트의 local 좌표로 되돌린 다음 거리 함수를 적용하면 훨씬 단순해집니다. “좌표계를 바꾸면 문제가 쉬워진다”는 말, 앞으로도 계속 나옵니다. 교수님이 질리게 말할 예정입니다.
데모에서 볼 것
데모에서는 가운데 흰 핸들을 직접 드래그해보세요. radial 모드에서는 원형 stop marker가 0%, 36%, 68%, 100% 거리 위치를 보여줍니다. conic 모드에서는 각도 방향선과 0deg, 120deg, 240deg stop marker를 보여줍니다.
radial-gradient는 결국 중심에서의 거리로 색을 고르는 함수입니다. conic-gradient는 중심 기준 각도로 색을 고르는 함수입니다. 그러니 원형 도구를 만들 때마다 속으로 이렇게 물어보면 됩니다. “이 픽셀은 중심에서 얼마나 멀고, 어느 방향이지?”