커브(톤 커브)

Curves

밝기의 입력-출력 관계를 곡선 하나로 표현해 레벨보다 훨씬 세밀하게 톤을 조절하는 도구.

다른 이름: Tone curve톤커브
···
html
<div class="wrap">
  <div class="stage">
    <div class="pane"><canvas id="pc" width="300" height="180"></canvas></div>
    <div class="pane graph"><canvas id="gc" width="160" height="160"></canvas></div>
  </div>
  <div class="hud"><span>톤 커브 LUT</span><span>입력 → 출력</span></div>
</div>
css
.wrap{position:absolute;inset:0;padding:clamp(8px,3vmin,20px);display:flex;flex-direction:column;gap:6px}
.stage{position:relative;flex:1;min-height:0;display:flex;gap:clamp(6px,2vmin,14px)}
.pane{position:relative;flex:1.3;min-height:0}
.graph{flex:1}
canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;border-radius:10px;border:1px solid var(--line);background:#0a0a0a}
.hud{display:flex;justify-content:space-between;gap:8px;font:700 clamp(9px,2.2vmin,11px)/1 ui-monospace,monospace;color:var(--muted)}
js
var W = 300, H = 180;
var pc = document.getElementById('pc'), px = pc.getContext('2d');
var gc = document.getElementById('gc'), gx = gc.getContext('2d');

function paintPhoto(cx, w, h) {
  var g = cx.createLinearGradient(0, 0, 0, h * 0.62);
  g.addColorStop(0, '#5b86c9'); g.addColorStop(0.55, '#bcd6ea'); g.addColorStop(1, '#f5e0ad');
  cx.fillStyle = g; cx.fillRect(0, 0, w, h);
  var sx = w * 0.74, sy = h * 0.22, sr = h * 0.11;
  var sg = cx.createRadialGradient(sx, sy, 0, sx, sy, sr * 3.2);
  sg.addColorStop(0, 'rgba(255,247,214,0.9)'); sg.addColorStop(1, 'rgba(255,247,214,0)');
  cx.fillStyle = sg; cx.fillRect(0, 0, w, h * 0.62);
  cx.fillStyle = '#fff8df'; cx.beginPath(); cx.arc(sx, sy, sr, 0, 7); cx.fill();
  cx.fillStyle = '#6f9152';
  cx.beginPath(); cx.moveTo(0, h * 0.64);
  for (var x = 0; x <= w; x += 14) cx.lineTo(x, h * 0.64 - Math.sin(x * 0.014) * h * 0.05 - Math.sin(x * 0.032 + 1.2) * h * 0.02);
  cx.lineTo(w, h); cx.lineTo(0, h); cx.closePath(); cx.fill();
  cx.fillStyle = '#405f38';
  cx.beginPath(); cx.moveTo(0, h * 0.79);
  for (var x2 = 0; x2 <= w; x2 += 14) cx.lineTo(x2, h * 0.79 - Math.sin(x2 * 0.022 + 0.6) * h * 0.035);
  cx.lineTo(w, h); cx.lineTo(0, h); cx.closePath(); cx.fill();
  var tx = w * 0.2, ty = h * 0.8;
  cx.strokeStyle = '#3a2a1c'; cx.lineWidth = Math.max(1.5, w * 0.008);
  cx.beginPath(); cx.moveTo(tx, ty); cx.lineTo(tx - w * 0.01, ty - h * 0.13); cx.stroke();
  cx.fillStyle = '#2e4a28';
  for (var i = 0; i < 5; i++) { cx.beginPath(); cx.arc(tx + (i - 2) * w * 0.013, ty - h * 0.15 - (i % 2) * h * 0.018, w * 0.028, 0, 7); cx.fill(); }
  cx.fillStyle = '#1e1e1c';
  cx.beginPath(); cx.arc(w * 0.56, h * 0.745, h * 0.018, 0, 7); cx.fill();
  cx.fillRect(w * 0.555, h * 0.76, w * 0.01, h * 0.06);
  var img = cx.getImageData(0, 0, w, h), d = img.data;
  for (var p = 0; p < d.length; p += 4) { var n = (Math.random() - 0.5) * 10; d[p] += n; d[p + 1] += n; d[p + 2] += n; }
  cx.putImageData(img, 0, 0);
}
paintPhoto(px, W, H);
var orig = px.getImageData(0, 0, W, H);
var t = 0;
function frame() {
  t += 0.012;
  var k = Math.sin(t) * 0.34;
  var lut = new Uint8ClampedArray(256);
  for (var v = 0; v < 256; v++) {
    var x = v / 255;
    var y = x - k * Math.sin(2 * Math.PI * x) / (2 * Math.PI);
    lut[v] = Math.round(Math.min(1, Math.max(0, y)) * 255);
  }
  var out = px.createImageData(W, H);
  var od = out.data, id = orig.data;
  for (var i = 0; i < id.length; i += 4) { od[i] = lut[id[i]]; od[i + 1] = lut[id[i + 1]]; od[i + 2] = lut[id[i + 2]]; od[i + 3] = 255; }
  px.putImageData(out, 0, 0);

  var gw = gc.width, gh = gc.height;
  gx.clearRect(0, 0, gw, gh);
  gx.strokeStyle = 'rgba(150,150,150,.4)'; gx.lineWidth = 1;
  gx.beginPath(); gx.moveTo(0, gh); gx.lineTo(gw, 0); gx.stroke();
  gx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--accent') || '#5b5bf7';
  gx.lineWidth = 2.5;
  gx.beginPath();
  for (var vv = 0; vv < 256; vv += 4) {
    var xx = vv / 255 * gw, yy = gh - (lut[vv] / 255 * gh);
    if (vv === 0) gx.moveTo(xx, yy); else gx.lineTo(xx, yy);
  }
  gx.stroke();
  requestAnimationFrame(frame);
}
frame();

커브는 대각선(입력=출력)에서 시작해, 그래프 위의 점을 움직인 만큼 그 구간의 밝기만 바꿉니다. 이 데모는 완만한 S자 곡선을 사인 함수로 만들어(중간톤은 그대로 두고 그 위아래만 밀어냄) 256칸 LUT로 굽고, 그 LUT를 사진의 모든 픽셀에 그대로 적용합니다 — 그래프의 곡선 모양과 사진의 변화가 동시에 움직이는 걸 보면 LUT가 하는 일이 바로 보입니다.

레벨은 세 점(검정·흰색·감마)만 조절할 수 있지만 커브는 그래프 위 어디든 점을 추가해 그 구간만 따로 조절합니다. 대비를 높이고 싶은 만큼만 S자로 꺾거나, 그림자만 들어 올리거나, 하이라이트만 눌러 담는 식으로 씁니다.

곡선을 너무 급하게 꺾으면 인접한 두 밝기값이 같은 출력값으로 뭉개져 그 구간의 디테일이 사라지고(밴딩·포스터화), 채널별로 따로 커브를 걸면 색이 의도치 않게 틀어집니다. 완만하게 조금씩 움직이는 것이 안전합니다.

언제 쓰나

레벨로는 부족한 정교한 톤 조절이 필요하거나, 특정 밝기 구간만 건드리고 싶을 때 씁니다. 급격한 꺾임은 밴딩을 만듭니다.