68 lines
2.5 KiB
JavaScript
68 lines
2.5 KiB
JavaScript
/**
|
||
* Draw a luminance histogram of a layer's pixels onto the given
|
||
* canvas. Sampling is capped at ~400×400 so the call stays cheap on
|
||
* very large images.
|
||
*
|
||
* If the layer has a staged Levels adjustment
|
||
* (`layer._stagedAdj.params` with `inBlack` / `inWhite`), the two
|
||
* endpoint markers are drawn over the bars.
|
||
*
|
||
* @param {HTMLCanvasElement} canvas The histogram canvas to render into.
|
||
* @param {{
|
||
* canvas: HTMLCanvasElement,
|
||
* _stagedAdj?: {params?: {inBlack?: number, inWhite?: number}}
|
||
* }} layer Source layer.
|
||
*/
|
||
export function drawHistogram(canvas, layer) {
|
||
if (!canvas) return;
|
||
const w = canvas.width, h = canvas.height;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.clearRect(0, 0, w, h);
|
||
|
||
// Down-sample huge images so the histogram stays interactive on 8k+
|
||
// photos. ~400×400 is enough to characterise the distribution.
|
||
const src = layer.canvas;
|
||
const sw = src.width, sh = src.height;
|
||
const maxSamples = 400;
|
||
const sampleW = Math.min(maxSamples, sw);
|
||
const sampleH = Math.min(maxSamples, sh);
|
||
const tmp = document.createElement('canvas');
|
||
tmp.width = sampleW; tmp.height = sampleH;
|
||
const tctx = tmp.getContext('2d');
|
||
tctx.drawImage(src, 0, 0, sampleW, sampleH);
|
||
const img = tctx.getImageData(0, 0, sampleW, sampleH).data;
|
||
|
||
const hist = new Uint32Array(256);
|
||
for (let i = 0; i < img.length; i += 4) {
|
||
if (img[i + 3] < 8) continue; // skip near-transparent
|
||
// Rec. 709 luminance — common choice for histograms in photo editors.
|
||
const Y = (0.2126 * img[i] + 0.7152 * img[i + 1] + 0.0722 * img[i + 2]) | 0;
|
||
hist[Math.min(255, Y)]++;
|
||
}
|
||
let peak = 1;
|
||
for (let i = 0; i < 256; i++) if (hist[i] > peak) peak = hist[i];
|
||
|
||
// Background.
|
||
ctx.fillStyle = 'rgba(255,255,255,0.05)';
|
||
ctx.fillRect(0, 0, w, h);
|
||
|
||
// Bars. sqrt-scaled so the long tails (specular highlights, deep
|
||
// shadows) stay visible even when the central mass dominates.
|
||
ctx.fillStyle = 'rgba(255,255,255,0.55)';
|
||
for (let i = 0; i < 256; i++) {
|
||
const x = (i / 256) * w;
|
||
const bh = Math.pow(hist[i] / peak, 0.5) * h;
|
||
ctx.fillRect(x, h - bh, w / 256 + 0.5, bh);
|
||
}
|
||
|
||
// Endpoint markers (input black / input white) from a staged Levels
|
||
// adjustment, if one is in flight.
|
||
const p = layer._stagedAdj?.params;
|
||
if (p) {
|
||
ctx.fillStyle = 'rgba(0,0,0,0.9)';
|
||
ctx.fillRect((p.inBlack / 256) * w, 0, 1, h);
|
||
ctx.fillStyle = 'rgba(255,255,255,0.9)';
|
||
ctx.fillRect((p.inWhite / 256) * w, 0, 1, h);
|
||
}
|
||
}
|