Files
odysseus/static/js/editor/fx/histogram.js
pewdiepie-archdaemon e5c99a5eee Odysseus v1.0
2026-05-31 23:58:26 +09:00

68 lines
2.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
}