Blend if

블렌드 이프

A rule for blending two layers by brightness range — show the layer below in this range, the layer above in that one.

Also known as: 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();

It's a rule for selectively exposing an upper layer (say, a solid color tint) and a lower layer (the original photo) by brightness range. A slider says "when this layer's brightness is below X, let the layer beneath show through instead." This demo animates that range so a color tint blends in only over the dark areas and automatically drops out over bright highlights.

It lets you do things like "tint only the shadows" or "add texture only to the highlights" on the fly, without hand-painting a mask — which makes it a go-to for compositing and texture overlays. The idea is close to a luminosity mask, but instead of building a separate mask layer, it's tuned live with sliders right in the layer panel.

Collapse the transition range to zero (a hard cutoff) and the boundary shows up as a visible step. The standard fix is splitting each slider into two handles (range start and end) so the transition interpolates smoothly. Mixing up "this layer" versus "the layer below" and setting them backwards is a common mistake — it applies the effect to the opposite range from what was intended.

When to use

Use it to tint only the shadows or texture only the highlights. A zero-width transition shows up as a visible step at the boundary.