블렌드 이프

Blend if

두 레이어를 밝기 구간별로 골라 섞는 규칙 — 이 범위는 아래 레이어가, 저 범위는 위 레이어가 보이게 지정합니다.

다른 이름: Blend If밝기 기반 블렌딩
···
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="rng">40–130</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:center;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);
var tint = [255, 170, 90];
function smooth(e0, e1, x) { var tt = Math.min(1, Math.max(0, (x - e0) / (e1 - e0))); return tt * tt * (3 - 2 * tt); }
var t = 0;
function frame() {
  t += 0.012;
  var lo = 40 + Math.sin(t) * 12;
  var hi = 130 + Math.sin(t) * 12;
  var maskImg = lx.createImageData(W, H);
  var resImg = rx.createImageData(W, H);
  var id = orig.data;
  for (var i = 0; i < id.length; i += 4) {
    var lum = 0.2126 * id[i] + 0.7152 * id[i + 1] + 0.0722 * id[i + 2];
    var wgt = 1 - smooth(lo, hi, lum);
    var mv = Math.round(wgt * 255);
    maskImg.data[i] = mv; maskImg.data[i + 1] = mv; maskImg.data[i + 2] = mv; maskImg.data[i + 3] = 255;
    resImg.data[i] = id[i] * (1 - wgt * 0.55) + tint[0] * wgt * 0.55;
    resImg.data[i + 1] = id[i + 1] * (1 - wgt * 0.55) + tint[1] * wgt * 0.55;
    resImg.data[i + 2] = id[i + 2] * (1 - wgt * 0.55) + tint[2] * wgt * 0.55;
    resImg.data[i + 3] = 255;
  }
  lx.putImageData(maskImg, 0, 0);
  rx.putImageData(resImg, 0, 0);
  document.getElementById('rng').textContent = Math.round(lo) + '–' + Math.round(hi);
  requestAnimationFrame(frame);
}
frame();

위 레이어(예: 단색 틴트)와 아래 레이어(원본 사진)를 밝기 구간별로 선택적으로 노출시키는 규칙입니다. "이 레이어 밝기가 X 이하일 때는 아래 레이어가 뚫고 나온다" 같은 슬라이더로 지정합니다. 이 데모는 위 레이어(색 틴트)가 어두운 영역에서만 섞이고 밝은 하이라이트에서는 자동으로 빠지도록 범위를 애니메이션으로 움직입니다.

마스크를 손으로 그리지 않고도 "그림자에만 색을 얹고 싶다", "하이라이트에만 텍스처를 더하고 싶다" 같은 작업을 즉석에서 할 수 있어 합성·질감 오버레이에 많이 씁니다. 루미노시티 마스크와 개념은 비슷하지만, 별도 마스크 레이어를 만들지 않고 레이어 패널의 슬라이더로 실시간 조절한다는 점이 다릅니다.

전환 구간을 0으로(칼같이 자르면) 경계에 계단현상(눈에 띄는 띠)이 생깁니다. 그래서 슬라이더를 둘로 쪼개(범위 시작·끝) 그 사이를 부드럽게 보간하는 것이 표준입니다. "이 레이어" 기준과 "아래 레이어" 기준을 혼동해서 반대로 설정하면 의도와 정반대 영역에 효과가 걸리는 실수도 흔합니다.

언제 쓰나

그림자에만 틴트를 얹거나 하이라이트에만 텍스처를 더하고 싶을 때 씁니다. 전환 구간을 0으로 두면 경계가 계단처럼 보입니다.