Luminosity mask

루미노시티 마스크

Uses a pixel's own brightness as a selection mask, so an edit applies only to the highlights, only to the shadows, or anywhere in between — automatically.

Also known as: Luminosity masking광도 마스크
···
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="th">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 t = 0;
function frame() {
  t += 0.015;
  var thresh = 130 + Math.sin(t) * 55;
  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 = Math.max(0, Math.min(1, (lum - thresh) / 60 + 0.5));
    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;
    var factor = 1 - wgt * 0.45;
    resImg.data[i] = id[i] * factor; resImg.data[i + 1] = id[i + 1] * factor; resImg.data[i + 2] = id[i + 2] * factor; resImg.data[i + 3] = 255;
  }
  lx.putImageData(maskImg, 0, 0);
  rx.putImageData(resImg, 0, 0);
  document.getElementById('th').textContent = Math.round(thresh);
  requestAnimationFrame(frame);
}
frame();

A luminosity mask uses the photo's own brightness values directly as a black-and-white mask — a bright pixel is bright in the mask too (gets more of the edit), a dark pixel is dark in the mask (gets less). There's no hand-drawn lasso; a precise "highlights only" or "shadows only" selection falls straight out of the image content. This demo sweeps a threshold to show how the highlight mask changes, and what happens when an edit — here, gently darkening highlights — is routed through it.

Use it to tone down a sky while leaving the ground untouched, or to gently pull back bright highlights on a forehead or nose in a portrait while leaving the shadows alone. Unlike a hard-edged manual selection, the mask itself is a brightness gradient, so the edit fades in naturally too.

Which channel builds the mask (overall luminance vs. one specific color channel) changes the result a lot, and grabbing whichever channel is convenient without checking can pull unwanted areas into the mask. Making the mask too high-contrast also makes the edit's edge visible as a band, which is why the mask usually gets a light blur to soften the transition.

When to use

Use it to edit areas distinguished by brightness — a sky, a highlight — without a hand-drawn selection. Blur the mask slightly or its edit edge shows as a band.