70 lines
2.9 KiB
JavaScript
70 lines
2.9 KiB
JavaScript
// Pure decision logic for the Image Compare A/B panel. No DOM in here — the
|
||
// wiring lives in js/compare/index.js so this module stays testable
|
||
// under node:test.
|
||
|
||
export const COMPARE_MODES = ["slide", "toggle"];
|
||
|
||
// "hold" was the press-and-hold mode that toggle replaced. Mapping it keeps a
|
||
// workflow saved with it on the button it became, instead of quietly dropping
|
||
// back to slide - the panel would still work, but not the way it was left.
|
||
const LEGACY_MODES = { hold: "toggle" };
|
||
|
||
export function normalizeCompareMode(value) {
|
||
const mapped = LEGACY_MODES[value] ?? value;
|
||
return COMPARE_MODES.includes(mapped) ? mapped : COMPARE_MODES[0];
|
||
}
|
||
|
||
// The caption under the stage: the compared resolution, or "" when nothing
|
||
// has been loaded yet. Both sides are normally the same size, so A speaks for
|
||
// the pair; when they differ, both are named rather than one quietly winning.
|
||
export function compareSizeLabel(refs) {
|
||
const a = refs?.a;
|
||
const b = refs?.b;
|
||
if (!(a?.width > 0) || !(a?.height > 0)) return "";
|
||
if (b?.width > 0 && b?.height > 0 && (b.width !== a.width || b.height !== a.height)) {
|
||
return `A ${a.width}×${a.height} · B ${b.width}×${b.height}`;
|
||
}
|
||
return `${a.width}×${a.height}`;
|
||
}
|
||
|
||
// Fraction of the panel width covered by the pointer, clamped to 0..1.
|
||
// Degenerate rectangles and non-finite input resolve to 0 (all-A).
|
||
export function clipFraction(pointerX, rectLeft, rectWidth) {
|
||
const width = Number(rectWidth);
|
||
if (!Number.isFinite(width) || width <= 0) return 0;
|
||
const fraction = (Number(pointerX) - Number(rectLeft)) / width;
|
||
if (!Number.isFinite(fraction)) return 0;
|
||
return Math.max(0, Math.min(1, fraction));
|
||
}
|
||
|
||
// Give both edges a forgiving 6% landing zone. Leaving through any side
|
||
// settles to the nearest full image instead of stranding a partial split.
|
||
export function slideFraction(pointerX, rectLeft, rectWidth, leaving = false) {
|
||
const fraction = clipFraction(pointerX, rectLeft, rectWidth);
|
||
if (leaving) return fraction < 0.5 ? 0 : 1;
|
||
if (fraction <= 0.06) return 0;
|
||
if (fraction >= 0.94) return 1;
|
||
return fraction;
|
||
}
|
||
|
||
// CSS for a given reveal fraction: image B keeps its left portion up to the
|
||
// seam, the rest is clipped away. The seam only shows while both images are
|
||
// partially visible.
|
||
export function compareClip(fraction) {
|
||
const value = Number(fraction);
|
||
const clamped = Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
||
return {
|
||
clipPath: `inset(0 ${((1 - clamped) * 100).toFixed(2)}% 0 0)`,
|
||
seamLeft: `${(clamped * 100).toFixed(2)}%`,
|
||
seamVisible: clamped > 0 && clamped < 1,
|
||
};
|
||
}
|
||
|
||
// Pull the two preview references out of an onExecuted payload. Both must
|
||
// be present for the panel to have anything meaningful to show.
|
||
export function findCompareImages(message) {
|
||
const a = message?.a_images?.[0] ?? null;
|
||
const b = message?.b_images?.[0] ?? null;
|
||
return a?.filename && b?.filename ? { a, b } : null;
|
||
}
|