Files
MajoorWaldi-ComfyUI-Majoor-…/ui/features/viewer/probe.ts
T

259 lines
8.2 KiB
TypeScript

import { safeAddListener, safeCall } from "./lifecycle.js";
export function installViewerProbe({
overlay,
content,
state,
VIEWER_MODES,
getPrimaryMedia,
getMediaNaturalSize,
_getViewportRect,
positionOverlayBox,
probeTooltip,
loupeWrap,
onLoupeRedraw,
lifecycle,
}: Record<string, any> = {}): Record<string, any> {
const unsubs = lifecycle?.unsubs || [];
const sampleCanvas = document.createElement("canvas");
sampleCanvas.width = 1;
sampleCanvas.height = 1;
let sampleCtx: any = null;
try {
sampleCtx = sampleCanvas.getContext("2d", { willReadFrequently: true });
} catch (e: any) {
console.debug?.(e);
}
let probeRAF: any = null;
let lastX: any = null;
let lastY: any = null;
const hideProbe = () => {
try {
probeTooltip.style.display = "none";
} catch (e: any) {
console.debug?.(e);
}
try {
loupeWrap.style.display = "none";
} catch (e: any) {
console.debug?.(e);
}
try {
state._probe = null;
} catch (e: any) {
console.debug?.(e);
}
};
const formatProbeText = (p: any) => {
try {
const x = p?.x;
const y = p?.y;
const r = p?.r;
const g = p?.g;
const b = p?.b;
const a = p?.a;
const raw = Array.isArray(p?.raw) ? p.raw : null;
const lin = Array.isArray(p?.lin) ? p.lin : null;
const scale = Number(p?.scale);
const fmt = (v: any) => {
const n = Number(v);
if (!Number.isFinite(n)) return "?";
return n.toFixed(3);
};
const hex =
r != null && g != null && b != null
? `#${[r, g, b]
.map((v: any) => {
const n = Math.max(0, Math.min(255, Number(v) || 0));
return n.toString(16).padStart(2, "0");
})
.join("")}`
: "";
const scaleNote =
Number.isFinite(scale) && scale > 0 && scale < 0.999
? ` (proc ${(scale * 100).toFixed(0)}%)`
: "";
const lines = [];
lines.push(`X: ${x ?? "?"} Y: ${y ?? "?"}${scaleNote}`);
lines.push(`RGBA8: ${r ?? "?"} ${g ?? "?"} ${b ?? "?"} ${a ?? "?"}`);
if (raw && raw.length >= 3) {
lines.push(`RGB: ${fmt(raw[0])} ${fmt(raw[1])} ${fmt(raw[2])}`);
}
if (lin && lin.length >= 3) {
lines.push(`HDR: ${fmt(lin[0])} ${fmt(lin[1])} ${fmt(lin[2])}`);
}
if (hex) lines.push(hex);
return lines.join("\n");
} catch {
return "";
}
};
const samplePixel = (mediaEl: any, px: any, py: any) => {
if (!sampleCtx) return null;
try {
sampleCtx.clearRect(0, 0, 1, 1);
} catch (e: any) {
console.debug?.(e);
}
try {
let sx = Number(px) || 0;
let sy = Number(py) || 0;
if (mediaEl?.tagName === "CANVAS") {
const scale = Number(mediaEl._mjrPixelScale) || 1;
sx = Math.floor(sx * scale);
sy = Math.floor(sy * scale);
}
sampleCtx.drawImage(mediaEl, sx, sy, 1, 1, 0, 0, 1, 1);
const data = sampleCtx.getImageData(0, 0, 1, 1)?.data;
if (!data || data.length < 4) return null;
return { r: data[0], g: data[1], b: data[2], a: data[3] };
} catch {
return null;
}
};
const updateProbeAt = (clientX: any, clientY: any) => {
try {
if (overlay?.style?.display === "none") return hideProbe();
if (state?.mode !== VIEWER_MODES?.SINGLE) return hideProbe();
if (!state?.probeEnabled && !state?.loupeEnabled) return hideProbe();
const mediaEl = getPrimaryMedia?.();
if (!mediaEl) return hideProbe();
const rect = mediaEl.getBoundingClientRect();
if (!rect || !(rect.width > 2 && rect.height > 2)) return hideProbe();
const x = (Number(clientX) || 0) - rect.left;
const y = (Number(clientY) || 0) - rect.top;
if (x < 0 || y < 0 || x > rect.width || y > rect.height) return hideProbe();
const { w: aw, h: ah } = getMediaNaturalSize?.(mediaEl) || { w: 0, h: 0 };
if (!(aw > 0 && ah > 0)) return hideProbe();
const nx = x / rect.width;
const ny = y / rect.height;
const px = Math.max(0, Math.min(aw - 1, Math.floor(nx * aw)));
const py = Math.max(0, Math.min(ah - 1, Math.floor(ny * ah)));
let p: any = null;
if (state?.probeEnabled) {
try {
const proc = mediaEl?._mjrProc;
if (proc?.sampleAtOriginal) {
const s = proc.sampleAtOriginal(px, py);
if (s) p = { x: px, y: py, ...s };
}
} catch (e: any) {
console.debug?.(e);
}
if (!p) {
const rgba = samplePixel(mediaEl, px, py);
if (rgba) p = { x: px, y: py, ...rgba };
}
}
if (!p) p = { x: px, y: py };
try {
state._probe = p;
} catch (e: any) {
console.debug?.(e);
}
if (state?.probeEnabled) {
try {
probeTooltip.textContent = formatProbeText(p);
probeTooltip.style.display = "";
} catch (e: any) {
console.debug?.(e);
}
safeCall(() =>
positionOverlayBox?.(probeTooltip, clientX, clientY, {
offsetX: 18,
offsetY: 18,
}),
);
} else {
try {
probeTooltip.style.display = "none";
} catch (e: any) {
console.debug?.(e);
}
}
if (state?.loupeEnabled) {
safeCall(() => onLoupeRedraw?.(mediaEl, px, py, clientX, clientY));
} else {
try {
loupeWrap.style.display = "none";
} catch (e: any) {
console.debug?.(e);
}
}
} catch (e: any) {
console.debug?.(e);
}
};
const scheduleProbe = (clientX: any, clientY: any) => {
lastX = clientX;
lastY = clientY;
try {
if (probeRAF != null) return;
probeRAF = requestAnimationFrame(() => {
probeRAF = null;
updateProbeAt(lastX, lastY);
});
} catch (e: any) {
console.debug?.(e);
}
};
try {
if (content && !content._mjrProbeBound) {
const onProbeMove = (e: any) => {
try {
scheduleProbe(e.clientX, e.clientY);
} catch (e: any) {
console.debug?.(e);
}
};
const onProbeLeave = () => {
hideProbe();
};
unsubs.push(
safeAddListener(content, "mousemove", onProbeMove, {
passive: true,
capture: true,
}),
);
unsubs.push(
safeAddListener(content, "mouseleave", onProbeLeave, {
passive: true,
capture: true,
}),
);
content._mjrProbeBound = true;
}
} catch (e: any) {
console.debug?.(e);
}
return {
hide: hideProbe,
dispose: () => {
try {
if (probeRAF != null) cancelAnimationFrame(probeRAF);
} catch (e: any) {
console.debug?.(e);
}
probeRAF = null;
hideProbe();
},
};
}