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

557 lines
22 KiB
TypeScript

export function createViewerGrid({
gridCanvas,
content,
state,
VIEWER_MODES,
getPrimaryMedia,
getViewportRect,
clearCanvas,
}: Record<string, any> = {}): Record<string, any> {
const _getModeRoot = () => {
try {
const mode = state?.mode;
if (mode === VIEWER_MODES?.AB_COMPARE)
return content?.querySelector?.(".mjr-viewer-ab") || content || null;
if (mode === VIEWER_MODES?.SIDE_BY_SIDE)
return content?.querySelector?.(".mjr-viewer-sidebyside") || content || null;
return content?.querySelector?.(".mjr-viewer-single") || content || null;
} catch {
return content || null;
}
};
const _getBoxElForMedia = (mediaEl: any, root: any) => {
try {
if (!mediaEl) return root || null;
const mode = state?.mode;
if (mode === VIEWER_MODES?.SIDE_BY_SIDE || mode === VIEWER_MODES?.AB_COMPARE) {
// Prefer the direct child panel/layer of the mode root (stable, not transformed by pan/zoom).
let el = mediaEl;
while (el && el !== root && el.parentElement) {
if (el.parentElement === root) return el;
el = el.parentElement;
}
return root || null;
}
return root || content || null;
} catch {
return root || content || null;
}
};
const _getAssetForMedia = (mediaEl: any) => {
try {
const id = mediaEl?.dataset?.mjrAssetId;
if (id == null || id === "") return state?.assets?.[state?.currentIndex] || null;
const list = Array.isArray(state?.assets) ? state.assets : [];
for (const a of list) {
try {
if (a?.id != null && String(a.id) === String(id)) return a;
} catch (e: any) {
console.debug?.(e);
}
}
return state?.assets?.[state?.currentIndex] || null;
} catch {
return state?.assets?.[state?.currentIndex] || null;
}
};
const _getNaturalSize = (el: any, assetHint: any = null) => {
try {
if (!el) return { w: 0, h: 0 };
if (el instanceof HTMLCanvasElement) {
const w = Number((el as any)._mjrNaturalW) || Number(el.width) || 0;
const h = Number((el as any)._mjrNaturalH) || Number(el.height) || 0;
if (w > 0 && h > 0) return { w, h };
}
const w = Number(el.videoWidth) || Number(el.naturalWidth) || 0;
const h = Number(el.videoHeight) || Number(el.naturalHeight) || 0;
if (w > 0 && h > 0) return { w, h };
// Prefer per-media asset dimensions (works in side-by-side / A-B while processors are still loading).
try {
const aw = Number(assetHint?.width) || 0;
const ah = Number(assetHint?.height) || 0;
if (aw > 0 && ah > 0) return { w: aw, h: ah };
} catch (e: any) {
console.debug?.(e);
}
// Fallbacks (before processors report natural size).
try {
const sw = Number(state?._mediaW) || 0;
const sh = Number(state?._mediaH) || 0;
if (sw > 0 && sh > 0) return { w: sw, h: sh };
} catch (e: any) {
console.debug?.(e);
}
try {
const cur = state?.assets?.[state?.currentIndex] || null;
const aw = Number(cur?.width) || 0;
const ah = Number(cur?.height) || 0;
if (aw > 0 && ah > 0) return { w: aw, h: ah };
} catch (e: any) {
console.debug?.(e);
}
return { w: 0, h: 0 };
} catch {
return { w: 0, h: 0 };
}
};
const _fitHeightRect = (boxW: any, boxH: any, naturalW: any, naturalH: any) => {
try {
const bw = Number(boxW) || 0;
const bh = Number(boxH) || 0;
const nw = Number(naturalW) || 0;
const nh = Number(naturalH) || 0;
if (!(bw > 0 && bh > 0 && nw > 0 && nh > 0)) return { x: 0, y: 0, w: bw, h: bh };
const aspect = nw / nh;
if (!Number.isFinite(aspect) || aspect <= 0) return { x: 0, y: 0, w: bw, h: bh };
const h = bh;
const w = bh * aspect;
const x = (bw - w) / 2;
const y = 0;
return { x, y, w, h };
} catch {
return { x: 0, y: 0, w: Number(boxW) || 0, h: Number(boxH) || 0 };
}
};
const _transformRectAround = (rectCss: any, centerCss: any, zoom: any, panX: any, panY: any) => {
try {
const z = Math.max(0.1, Math.min(16, Number(zoom) || 1));
const cx = Number(centerCss?.x) || 0;
const cy = Number(centerCss?.y) || 0;
const x = Number(rectCss?.x) || 0;
const y = Number(rectCss?.y) || 0;
const w = Number(rectCss?.w) || 0;
const h = Number(rectCss?.h) || 0;
const px = Number(panX) || 0;
const py = Number(panY) || 0;
return {
x: cx + (x - cx) * z + px,
y: cy + (y - cy) * z + py,
w: w * z,
h: h * z,
};
} catch {
return { x: 0, y: 0, w: 0, h: 0 };
}
};
const _drawHudSizeLabel = (ctx: any, rect: any, label: any, dpr: any) => {
try {
const r = rect || {};
const x = Number(r.x) || 0;
const y = Number(r.y) || 0;
const w = Number(r.w) || 0;
const h = Number(r.h) || 0;
if (!(w > 8 && h > 8)) return;
const text = String(label || "");
if (!text) return;
const padX = Math.max(6, Math.round(6 * dpr));
const padY = Math.max(3, Math.round(3 * dpr));
const fontPx = Math.max(11, Math.round(11 * dpr));
ctx.save();
ctx.font = `${fontPx}px var(--comfy-font, ui-sans-serif, system-ui)`;
ctx.textAlign = "left";
ctx.textBaseline = "top";
const tw = Math.ceil(ctx.measureText(text).width);
const bw = tw + padX * 2;
const bh = fontPx + padY * 2;
const bx = x + Math.max(2, Math.round(8 * dpr));
const by = y + Math.max(2, Math.round(8 * dpr));
ctx.fillStyle = "rgba(0,0,0,0.55)";
ctx.strokeStyle = "rgba(255,255,255,0.18)";
ctx.lineWidth = Math.max(1, Math.round(1 * dpr));
ctx.beginPath();
const rr = Math.max(6, Math.round(8 * dpr));
ctx.moveTo(bx + rr, by);
ctx.arcTo(bx + bw, by, bx + bw, by + bh, rr);
ctx.arcTo(bx + bw, by + bh, bx, by + bh, rr);
ctx.arcTo(bx, by + bh, bx, by, rr);
ctx.arcTo(bx, by, bx + bw, by, rr);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.fillStyle = "rgba(255,255,255,0.92)";
ctx.fillText(text, bx + padX, by + padY);
ctx.restore();
} catch {
try {
ctx.restore();
} catch (e: any) {
console.debug?.(e);
}
}
};
const ensureCanvasSize = () => {
try {
const rect = getViewportRect?.();
if (!rect) return { w: 0, h: 0, dpr: 1 };
const dpr = Math.max(1, Math.min(3, Number(window.devicePixelRatio) || 1));
const w = Math.max(1, Math.floor(rect.width * dpr));
const h = Math.max(1, Math.floor(rect.height * dpr));
try {
if (gridCanvas?.width !== w) gridCanvas.width = w;
if (gridCanvas?.height !== h) gridCanvas.height = h;
} catch (e: any) {
console.debug?.(e);
}
return { w, h, dpr };
} catch {
return { w: 0, h: 0, dpr: 1 };
}
};
const _aspectFromFormat = (format: any, mediaEl: any, mediaRect: any) => {
try {
const f = String(format || "image");
if (f === "image") {
const nat = _getNaturalSize(mediaEl);
const a = (Number(nat.w) || 0) / (Number(nat.h) || 1);
if (Number.isFinite(a) && a > 0) return a;
const b = (Number(mediaRect?.width) || 0) / (Number(mediaRect?.height) || 1);
return Number.isFinite(b) && b > 0 ? b : 1;
}
if (f === "16:9") return 16 / 9;
if (f === "9:16") return 9 / 16;
if (f === "1:1") return 1;
if (f === "4:3") return 4 / 3;
if (f === "2.39") return 2.39;
return 1;
} catch {
return 1;
}
};
const _fitAspectInBox = (boxW: any, boxH: any, aspect: any) => {
try {
const bw = Number(boxW) || 0;
const bh = Number(boxH) || 0;
const a = Number(aspect) || 1;
if (!(bw > 0 && bh > 0 && a > 0)) return { x: 0, y: 0, w: bw, h: bh };
const boxA = bw / bh;
let w = bw;
let h = bh;
if (a >= boxA) {
w = bw;
h = bw / a;
} else {
h = bh;
w = bh * a;
}
const x = (bw - w) / 2;
const y = (bh - h) / 2;
return { x, y, w, h };
} catch {
return { x: 0, y: 0, w: Number(boxW) || 0, h: Number(boxH) || 0 };
}
};
const _drawMaskOutside = (ctx: any, rect: any, alpha: any) => {
try {
const a = Math.max(0, Math.min(0.92, Number(alpha)));
if (!(a > 0)) return;
ctx.save();
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = `rgba(0,0,0,${a})`;
ctx.fillRect(0, 0, gridCanvas.width, gridCanvas.height);
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "rgba(0,0,0,1)";
const rects = Array.isArray(rect) ? rect : [rect];
for (const r of rects) {
if (!r) continue;
const x = Number(r.x) || 0;
const y = Number(r.y) || 0;
const w = Number(r.w) || 0;
const h = Number(r.h) || 0;
if (!(w > 1 && h > 1)) continue;
ctx.fillRect(x, y, w, h);
}
ctx.restore();
} catch {
try {
ctx.restore();
} catch (e: any) {
console.debug?.(e);
}
}
};
const _drawFormatOutline = (ctx: any, rect: any, dpr: any) => {
try {
ctx.save();
try {
ctx.setLineDash?.([Math.max(2, 4 * dpr), Math.max(2, 3 * dpr)]);
} catch (e: any) {
console.debug?.(e);
}
ctx.strokeStyle = "rgba(255,255,255,0.22)";
ctx.lineWidth = Math.max(1, Math.floor(1 * dpr));
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w - 1, rect.h - 1);
ctx.restore();
} catch {
try {
ctx.restore();
} catch (e: any) {
console.debug?.(e);
}
}
};
const redrawGrid = ({ w, h, dpr }: Record<string, any> = {}) => {
try {
const ctx = gridCanvas?.getContext?.("2d");
if (!ctx) return;
try {
clearCanvas?.(ctx, w, h);
} catch (e: any) {
console.debug?.(e);
}
const vp = getViewportRect?.();
if (!vp) return;
const vpW = Number(vp.width) || 0;
const vpH = Number(vp.height) || 0;
if (!(vpW > 1 && vpH > 1)) return;
const root = _getModeRoot();
const mode = state?.mode;
const mediaEls = (() => {
try {
if (mode === VIEWER_MODES?.SINGLE) return [getPrimaryMedia?.()].filter(Boolean);
// Multi-media modes: collect all .mjr-viewer-media elements in the active view root.
const els = root?.querySelectorAll?.(".mjr-viewer-media") || [];
return Array.from(els || []).filter(Boolean);
} catch {
return [getPrimaryMedia?.()].filter(Boolean);
}
})();
if (!mediaEls.length) return;
// IMPORTANT: overlays must not move with pan/zoom.
// Compute the format rect purely from the viewport and the image aspect, NOT from the transformed media element rect.
const rects = [];
const baseRects = [];
for (const mediaEl of mediaEls) {
try {
const boxEl = _getBoxElForMedia(mediaEl, root);
const boxRect = boxEl?.getBoundingClientRect?.() || null;
if (!boxRect) continue;
const boxW = Number(boxRect.width) || 0;
const boxH = Number(boxRect.height) || 0;
if (!(boxW > 1 && boxH > 1)) continue;
const assetHint = _getAssetForMedia(mediaEl);
const nat = _getNaturalSize(mediaEl, assetHint);
const base = _fitHeightRect(boxW, boxH, nat.w, nat.h);
const aspect = _aspectFromFormat(state?.overlayFormat, mediaEl, {
width: base.w,
height: base.h,
});
const fmt = _fitAspectInBox(base.w, base.h, aspect);
const ox = (Number(boxRect.left) || 0) - (Number(vp.left) || 0);
const oy = (Number(boxRect.top) || 0) - (Number(vp.top) || 0);
const baseRectCss0 = {
x: ox + (base.x || 0),
y: oy + (base.y || 0),
w: base.w || boxW,
h: base.h || boxH,
};
const fmtRectCss0 = {
x: baseRectCss0.x + (fmt.x || 0),
y: baseRectCss0.y + (fmt.y || 0),
w: fmt.w || baseRectCss0.w,
h: fmt.h || baseRectCss0.h,
};
const centerCss = { x: ox + boxW / 2, y: oy + boxH / 2 };
const z = Number(state?.zoom) || 1;
// Pan state is stored as "screen_pixels * zoom" (inflated) in panzoom.js to maintain stability.
// We must deflate it here to get back to screen pixels for the grid transform.
const panX = (Number(state?.panX) || 0) / z;
const panY = (Number(state?.panY) || 0) / z;
const baseRectCss = _transformRectAround(
baseRectCss0,
centerCss,
z,
panX,
panY,
);
const fmtRectCss = _transformRectAround(fmtRectCss0, centerCss, z, panX, panY);
const rb = {
x: baseRectCss.x * dpr,
y: baseRectCss.y * dpr,
w: baseRectCss.w * dpr,
h: baseRectCss.h * dpr,
_sizeLabel: (() => {
try {
const hw = Number(assetHint?.width) || Number(nat.w) || 0;
const hh = Number(assetHint?.height) || Number(nat.h) || 0;
if (hw > 0 && hh > 0) return `${hw}x${hh}`;
} catch (e: any) {
console.debug?.(e);
}
return "";
})(),
};
const rf = {
x: fmtRectCss.x * dpr,
y: fmtRectCss.y * dpr,
w: fmtRectCss.w * dpr,
h: fmtRectCss.h * dpr,
};
if (rb.w > 1 && rb.h > 1) baseRects.push(rb);
if (rf.w > 1 && rf.h > 1) rects.push(rf);
} catch (e: any) {
console.debug?.(e);
}
}
if (!rects.length && !baseRects.length) return;
const formatRects = rects.length ? rects : baseRects;
const rect = formatRects[0] || null;
// Optional format mask (dim outside chosen rect).
try {
if (state?.overlayMaskEnabled) {
_drawMaskOutside(ctx, formatRects, state?.overlayMaskOpacity ?? 0.65);
// The viewer background is often already dark; draw outlines so the format is visible.
for (const r of formatRects) {
try {
_drawFormatOutline(ctx, r, dpr);
} catch (e: any) {
console.debug?.(e);
}
}
}
} catch (e: any) {
console.debug?.(e);
}
// Nuke-like format box per image (single mode only).
try {
if (state?.hudEnabled && state?.mode === VIEWER_MODES?.SINGLE) {
for (const r of baseRects) {
try {
ctx.save();
ctx.strokeStyle = "rgba(255,255,255,0.22)";
ctx.lineWidth = Math.max(1, Math.floor(1 * dpr));
ctx.strokeRect(r.x + 0.5, r.y + 0.5, r.w - 1, r.h - 1);
ctx.restore();
} catch {
try {
ctx.restore();
} catch (e: any) {
console.debug?.(e);
}
}
try {
_drawHudSizeLabel(ctx, r, r._sizeLabel || "", dpr);
} catch (e: any) {
console.debug?.(e);
}
}
}
} catch (e: any) {
console.debug?.(e);
}
// Only draw guide lines in single mode (mask can apply in any mode).
if (state?.mode !== VIEWER_MODES?.SINGLE) return;
if ((state?.gridMode || 0) === 0) return;
if (!rect) return;
try {
ctx.save();
ctx.translate(rect.x, rect.y);
ctx.strokeStyle = "rgba(255, 255, 255, 0.22)";
// Slightly thicker grid lines for readability.
ctx.lineWidth = Math.max(2, Math.round(1.25 * dpr));
const drawLine = (x1: any, y1: any, x2: any, y2: any) => {
try {
ctx.beginPath();
ctx.moveTo(Math.round(x1) + 0.5, Math.round(y1) + 0.5);
ctx.lineTo(Math.round(x2) + 0.5, Math.round(y2) + 0.5);
ctx.stroke();
} catch (e: any) {
console.debug?.(e);
}
};
if (state.gridMode === 1) {
// Thirds
drawLine(rect.w / 3, 0, rect.w / 3, rect.h);
drawLine((2 * rect.w) / 3, 0, (2 * rect.w) / 3, rect.h);
drawLine(0, rect.h / 3, rect.w, rect.h / 3);
drawLine(0, (2 * rect.h) / 3, rect.w, (2 * rect.h) / 3);
} else if (state.gridMode === 2) {
// Center crosshair
drawLine(rect.w / 2, 0, rect.w / 2, rect.h);
drawLine(0, rect.h / 2, rect.w, rect.h / 2);
} else if (state.gridMode === 3) {
// Safe zones (title/action safe)
const drawRect = (inset: any, alpha: any) => {
try {
ctx.save();
ctx.strokeStyle = `rgba(255,255,255,${alpha})`;
const ix = Math.round(rect.w * inset);
const iy = Math.round(rect.h * inset);
const rw = Math.round(rect.w * (1 - inset * 2));
const rh = Math.round(rect.h * (1 - inset * 2));
ctx.strokeRect(ix + 0.5, iy + 0.5, rw, rh);
} catch (e: any) {
console.debug?.(e);
} finally {
try {
ctx.restore();
} catch (e: any) {
console.debug?.(e);
}
}
};
// Approx Nuke-like: action safe 90% (5%), title safe 80% (10%)
drawRect(0.05, 0.24);
drawRect(0.1, 0.18);
} else if (state.gridMode === 4) {
// Golden ratio lines (0.382 / 0.618)
const g0 = 0.382;
const g1 = 1 - g0;
drawLine(rect.w * g0, 0, rect.w * g0, rect.h);
drawLine(rect.w * g1, 0, rect.w * g1, rect.h);
drawLine(0, rect.h * g0, rect.w, rect.h * g0);
drawLine(0, rect.h * g1, rect.w, rect.h * g1);
}
} catch (e: any) {
console.debug?.(e);
} finally {
try {
ctx.restore();
} catch (e: any) {
console.debug?.(e);
}
}
} catch (e: any) {
console.debug?.(e);
}
};
return { ensureCanvasSize, redrawGrid };
}