바이브런스 vs 채도

Vibrance vs. saturation

채도는 모든 색의 순도를 똑같이 끌어올리고, 바이브런스는 이미 진한 색은 그대로 둔 채 흐린 색만 끌어올립니다.

다른 이름: Vibrance vs Saturation자연채도와 채도
···
html
<div class="wrap">
  <div class="stage">
    <div class="pane"><canvas id="lc" width="300" height="180"></canvas><span class="lbl">채도</span></div>
    <div class="pane"><canvas id="rc" width="300" height="180"></canvas><span class="lbl">바이브런스</span></div>
  </div>
  <div class="hud"><span>같은 세기를 동시에 적용</span><b id="amt">0%</b></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,12px)}
.pane{position:relative;flex:1;min-height:0}
.lbl{position:absolute;left:8px;bottom:8px;z-index:2;font:800 clamp(9px,2.2vmin,11px)/1 ui-monospace,monospace;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.6)}
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)}
.hud b{color:var(--fg)}
js
var W = 300, H = 180;
var lc = document.getElementById('lc'), lx = lc.getContext('2d');
var rc = document.getElementById('rc'), rx = rc.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(lx, W, H);
var orig = lx.getImageData(0, 0, W, H);
function rgbToHsl(r, g, b) {
  r /= 255; g /= 255; b /= 255;
  var max = Math.max(r, g, b), min = Math.min(r, g, b);
  var h = 0, s = 0, l = (max + min) / 2;
  var d2 = max - min;
  if (d2 !== 0) {
    s = l > 0.5 ? d2 / (2 - max - min) : d2 / (max + min);
    if (max === r) h = ((g - b) / d2 + (g < b ? 6 : 0));
    else if (max === g) h = (b - r) / d2 + 2;
    else h = (r - g) / d2 + 4;
    h /= 6;
  }
  return [h, s, l];
}
function hue2rgb(p, q, t) {
  if (t < 0) t += 1; if (t > 1) t -= 1;
  if (t < 1 / 6) return p + (q - p) * 6 * t;
  if (t < 1 / 2) return q;
  if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
  return p;
}
function hslToRgb(h, s, l) {
  if (s === 0) { var v = l * 255; return [v, v, v]; }
  var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  var p = 2 * l - q;
  return [hue2rgb(p, q, h + 1 / 3) * 255, hue2rgb(p, q, h) * 255, hue2rgb(p, q, h - 1 / 3) * 255];
}
var t = 0;
function frame() {
  t += 0.02;
  var amt = (Math.sin(t) + 1) / 2;
  var id = orig.data;
  var satOut = lx.createImageData(W, H), vibOut = rx.createImageData(W, H);
  var sd = satOut.data, vd = vibOut.data;
  var satFactor = 1 + amt * 1.6;
  for (var i = 0; i < id.length; i += 4) {
    var hsl = rgbToHsl(id[i], id[i + 1], id[i + 2]);
    var s1 = Math.min(1, hsl[1] * satFactor);
    var c1 = hslToRgb(hsl[0], s1, hsl[2]);
    sd[i] = c1[0]; sd[i + 1] = c1[1]; sd[i + 2] = c1[2]; sd[i + 3] = 255;
    var vibFactor = 1 + amt * 1.6 * (1 - hsl[1]);
    var s2 = Math.min(1, hsl[1] * vibFactor);
    var c2 = hslToRgb(hsl[0], s2, hsl[2]);
    vd[i] = c2[0]; vd[i + 1] = c2[1]; vd[i + 2] = c2[2]; vd[i + 3] = 255;
  }
  lx.putImageData(satOut, 0, 0);
  rx.putImageData(vibOut, 0, 0);
  document.getElementById('amt').textContent = Math.round(amt * 100) + '%';
  requestAnimationFrame(frame);
}
frame();

채도(Saturation)는 모든 픽셀의 HSL 채도값에 같은 비율을 곱하는 단순한 연산입니다. 바이브런스(Vibrance)는 픽셀마다 다른 비율을 씁니다 — 원래 채도가 낮았던 픽셀일수록 더 많이, 이미 채도가 높았던 픽셀일수록 더 적게 끌어올리는 가중치를 곱합니다. 이 데모는 같은 세기 값을 두 공식에 동시에 넣어 나란히 보여줍니다.

바이브런스는 인물 사진에서 특히 요긴합니다 — 피부색은 원래 채도가 낮은 편이라 바이브런스를 올려도 거의 안 변하는데, 옷이나 배경의 흐린 색은 살아납니다. 채도는 반대로 팝아트풍 색감처럼 전체를 균일하게 쨍하게 만들고 싶을 때 씁니다.

채도를 과하게 올리면 이미 진했던 색이 인쇄·디스플레이가 표현 가능한 색역을 벗어나 뭉개지고(특히 빨강·주황), 피부가 오렌지빛으로 번들거립니다. 바이브런스는 이런 과포화를 어느 정도 자동으로 억제하지만, 그래도 과하면 밋밋했던 부분만 부자연스럽게 튀어 보입니다.

언제 쓰나

인물 사진처럼 피부색은 지키고 배경·소품만 살리고 싶을 때 바이브런스, 전체를 균일하게 쨍하게 만들고 싶을 때 채도를 씁니다.