← Photoshot · 사용법과 개발기↗편집기 열기
Photoshot / Development04

무료 포토샵 개발기 4편: GitHub의 glfx.js에서 출발한 곡선 코드

무료 포토샵의 곡선은 GitHub의 glfx.js를 읽으며 구조를 잡았다. 점 사이 값을 미리 256칸 표로 만든다는 아이디어를 참고하고, 보간과 편집기 연결 코드는 AI와 별도로 작성했다. 이후 비교 결과에 따라 계산을 바꾸고 예전 작업의 모양도 지킨 과정이다.

출처 대조 · 원고 갱신 2026.10.16

원고 읽기 ↓
보정 전 · AI로 제작한 예시 인물 이미지
보정 전 · AI로 제작한 예시 인물 이미지
실제 포토샷 편집 화면 · 2026.10.13
실제 포토샷 편집 화면 · 2026.10.13
BUILD LOG / 01

glfx.js에서 읽은 것은 처리 구조였다

evanw/glfx.js의 src/filters/adjust/curves.js는 제어점으로 보간기를 만들고 256개 값을 준비한 뒤 채널별 조회에 사용한다. 원 개발 기록에도 이 LUT 아이디어를 검토했다고 남아 있다. 픽셀마다 점 사이를 새로 계산할 필요가 없다는 것이 핵심이었다.

glfx.js의 WebGL 필터 전체를 포토샷에 붙인 것은 아니다. 포토샷은 별도의 보간 함수를 작성하고 기존 compileAdjustment에 넣었다. 외부 코드에서 얻은 구조와 우리 코드에서 선택한 계산법을 구분해야 한다.

BUILD LOG / 02

첫 구현은 형태 보존 Hermite

처음 작성한 curveTable은 형태 보존 Hermite 보간이다. 아래 코드는 각 점의 기울기를 정하는 부분이다. 앞뒤 기울기의 부호가 달라지면 0으로 두어 급격한 편집 주변에서 곡선이 불필요하게 튀는 것을 줄이려 했다.

이 코드는 glfx.js에서 복사한 보간 함수가 아니다. LUT로 계산을 미리 준비하는 구조를 참고하되, 포토샷에 넣을 수학과 TypeScript 구현은 별도로 작성했다.

코드 발췌 · src/adjustments.ts · v0.16.0 · L380–388 ↗
const m = points.map((_, i) =>
  i === 0
    ? d[0]
    : i === n - 1
      ? d[n - 2]
      : d[i - 1] * d[i] <= 0
        ? 0
        : 2 / (1 / d[i - 1] + 1 / d[i]),
);
BUILD LOG / 03

비교 후 자연 3차 스플라인을 추가했다

점 네 개가 같아도 점 사이를 잇는 방식이 다르면 사진의 중간 밝기가 달라진다. 비교 작업 후 업그레이드 경로에는 naturalCurveTable을 추가했다. 아래 코드는 자연 3차 스플라인의 곡률을 계산하기 위한 전진 계산 부분이다.

“AI가 곡선 기능을 만들었다”라는 말만으로는 이 변화가 보이지 않는다. GitHub의 초기 아이디어를 우리 함수로 만들고, 결과를 비교해 보간법을 다시 선택한 것이 실제 개발 과정이었다.

코드 발췌 · src/adjustments.ts · v0.16.0 · L414–424 ↗
for (let i = 1; i < n - 1; i++) {
  const span = points[i + 1].x - points[i - 1].x,
    sig = (points[i].x - points[i - 1].x) / span,
    p = sig * y2[i - 1] + 2;
  y2[i] = (sig - 1) / p;
  const slope =
    (points[i + 1].y - points[i].y) / (points[i + 1].x - points[i].x) -
    (points[i].y - points[i - 1].y) / (points[i].x - points[i - 1].x);
  u[i] = ((6 * slope) / span - sig * u[i - 1]) / p;
}
for (let i = n - 2; i >= 0; i--) y2[i] = y2[i] * y2[i + 1] + u[i];
BUILD LOG / 04

화면과 픽셀에 같은 버전의 곡선을 적용하기

새 계산을 모든 저장 파일에 강제로 적용하면 예전에 만든 작업의 색이 달라질 수 있다. 그래서 revision 2는 자연 스플라인, 기존 작업은 Hermite 경로를 유지한다. 곡선 편집기의 표도 같은 조건으로 선택한다.

픽셀 처리에서는 개별 채널 표를 먼저 읽고 공통 RGB 표를 이어서 적용한다. 소수 위치는 인접 두 칸 사이를 보간한다. 외부에서 찾은 LUT 개념이 포토샷의 채널 순서와 버전 규칙에 맞게 조합되는 지점이다.

코드 발췌 · src/curves-editor.tsx · v0.16.0 · L43–43 ↗
const table = (value.revision === 2 ? naturalCurveTable : curveTable)(points),
코드 발췌 · src/adjustments.ts · v0.16.0 · L529–535 ↗
if (t === 'curves') {
  x = ct![channel as Channel][i];
  const f = clamp(x) * 255,
    lo = Math.floor(f),
    hi = Math.min(255, lo + 1);
  x = ct!.rgb[lo] + (ct!.rgb[hi] - ct!.rgb[lo]) * (f - lo);
}
BUILD LOG / 05

같은 점, 다른 곡선이라는 문제를 검증하기

사진 예시의 RGB 점은 (0,0), (64,80), (192,176), (255,255)다. 인물 원본은 AI 생성 예시 이미지이고, 아래 화면과 결과는 포토샷에서 얻었다.

과거 비교 차트는 (0,0), (64,48), (192,208), (255,255)를 사용했다. 기록된 해당 차트에서는 수정 후 평균·최대 차이가 모두 0이었다. 특정 입력의 일치 결과이며, 모든 점 배치와 사진에서 같은 결과를 보장한다는 뜻은 아니다.

보정 전 · AI 생성 예시 인물 이미지
보정 전 · AI 생성 예시 인물 이미지
보정 후 · 실제 포토샷 출력
보정 후 · 실제 포토샷 출력
BUILD LOG / 06

가져온 아이디어를 제품의 규칙에 맞추기

무료 포토샵의 곡선 개발에서 glfx.js는 읽을 수 있는 실제 출발점이었다. LUT라는 구조는 참고했고, Hermite·자연 스플라인 함수와 편집기 연결, 예전 작업 호환 처리는 AI와 별도로 작성·수정했다.

공개 코드의 가치는 완성된 기능을 그대로 가져오는 데만 있지 않았다. 구현을 읽어 설계를 이해하고, 우리 레이어 구조 안에서 다른 기능과 함께 움직이게 만드는 데 있었다. 다음 편에서는 노출 계산과 버전 호환 코드를 이어서 본다.

구현과 참고 자료

모든 사용법·개발기 보기 ↗↗직접 편집해 보기