259 lines
8.2 KiB
TypeScript
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();
|
|
},
|
|
};
|
|
}
|