Fix compare node layout rendering
This commit is contained in:
+187
-45
@@ -8,20 +8,26 @@ const PANEL = "#2a2c2e";
|
|||||||
const BORDER = "#444";
|
const BORDER = "#444";
|
||||||
const TEXT = "#d8d8d8";
|
const TEXT = "#d8d8d8";
|
||||||
const MUTED = "#9a9a9a";
|
const MUTED = "#9a9a9a";
|
||||||
const TITLE = "#f3f3f3";
|
|
||||||
|
|
||||||
const MODE_LABELS = ["Show 1", "Show 2", "Left Right", "Up Down", "Overlay", "Difference"];
|
const MODE_LABELS = ["Show 1", "Show 2", "Left Right", "Up Down", "Overlay", "Difference"];
|
||||||
const INIT_W = 420;
|
const MODE_HINTS = ["", "", "Hover to slide left / right", "Hover to slide up / down", "", "Shows pixel differences"];
|
||||||
const INIT_H = 474;
|
const INIT_W = 440;
|
||||||
const MIN_W = 360;
|
const IMAGE_TOP = 54;
|
||||||
const MIN_H = 250;
|
const INIT_H = INIT_W + IMAGE_TOP;
|
||||||
const BUTTON_GAP = 4;
|
const BUTTON_GAP = 3;
|
||||||
const BUTTON_H = 22;
|
const BUTTON_H = 18;
|
||||||
const BUTTON_TOP = 10;
|
const BUTTON_TOP = 10;
|
||||||
const INFO_TOP = 38;
|
const INFO_TOP = 30;
|
||||||
const IMAGE_TOP = 64;
|
const BTN_X = 80;
|
||||||
const OUTER_PAD = 12;
|
const MIN_W = BTN_X + 6 * 56 + BUTTON_GAP * 5 + 6;
|
||||||
|
const MIN_H = IMAGE_TOP + 100;
|
||||||
const TOP_TRIM = 8;
|
const TOP_TRIM = 8;
|
||||||
|
const SIZE_FONT = "12px 'Segoe UI',sans-serif";
|
||||||
|
const BADGE_FONT = "10px 'Segoe UI',sans-serif";
|
||||||
|
const BADGE_R = 8;
|
||||||
|
const SIZE_RESERVE = 86;
|
||||||
|
const SIZE_GAP = 6;
|
||||||
|
const SLIDER_PAD = 72;
|
||||||
|
|
||||||
function clamp(value, min, max) {
|
function clamp(value, min, max) {
|
||||||
return Math.max(min, Math.min(max, value));
|
return Math.max(min, Math.min(max, value));
|
||||||
@@ -42,46 +48,63 @@ function repaintNode(node) {
|
|||||||
node._cmpDomRender?.();
|
node._cmpDomRender?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowLayout(width) {
|
||||||
|
const leftPad = isVueNodes() ? 12 : BTN_X;
|
||||||
|
const rightPad = isVueNodes() ? 12 : 6;
|
||||||
|
const buttonWidth = Math.max(
|
||||||
|
30,
|
||||||
|
Math.floor((width - leftPad - rightPad - BUTTON_GAP * (MODE_LABELS.length - 1)) / MODE_LABELS.length),
|
||||||
|
);
|
||||||
|
return { leftPad, rightPad, buttonWidth };
|
||||||
|
}
|
||||||
|
|
||||||
function getButtons(node, width) {
|
function getButtons(node, width) {
|
||||||
const availableWidth = width - OUTER_PAD * 2 - BUTTON_GAP * (MODE_LABELS.length - 1);
|
const layout = rowLayout(width);
|
||||||
const buttonWidth = Math.max(44, Math.floor(availableWidth / MODE_LABELS.length));
|
|
||||||
return MODE_LABELS.map((label, index) => ({
|
return MODE_LABELS.map((label, index) => ({
|
||||||
label,
|
label,
|
||||||
index,
|
index,
|
||||||
x: OUTER_PAD + index * (buttonWidth + BUTTON_GAP),
|
x: layout.leftPad + index * (layout.buttonWidth + BUTTON_GAP),
|
||||||
y: BUTTON_TOP,
|
y: BUTTON_TOP,
|
||||||
w: buttonWidth,
|
w: layout.buttonWidth,
|
||||||
h: BUTTON_H,
|
h: BUTTON_H,
|
||||||
active: node._cmpMode === index,
|
active: node._cmpMode === index,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInfoRect(width) {
|
function getInfoRect(node, width) {
|
||||||
return {
|
const layout = rowLayout(width);
|
||||||
x: OUTER_PAD,
|
const full = {
|
||||||
|
x: layout.leftPad,
|
||||||
y: INFO_TOP,
|
y: INFO_TOP,
|
||||||
w: width - OUTER_PAD * 2,
|
w: layout.buttonWidth * MODE_LABELS.length + BUTTON_GAP * (MODE_LABELS.length - 1),
|
||||||
h: BUTTON_H,
|
h: BUTTON_H,
|
||||||
};
|
};
|
||||||
|
const reserveLeft = node?._cmpImg1 ? SIZE_RESERVE + SIZE_GAP : 0;
|
||||||
|
const reserveRight = node?._cmpImg2 ? SIZE_RESERVE + SIZE_GAP : 0;
|
||||||
|
return {
|
||||||
|
x: full.x + reserveLeft,
|
||||||
|
y: full.y,
|
||||||
|
w: Math.max(40, full.w - reserveLeft - reserveRight),
|
||||||
|
h: full.h,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSliderRect(width) {
|
function getSliderRect(node, width) {
|
||||||
const info = getInfoRect(width);
|
const info = getInfoRect(node, width);
|
||||||
const trackPad = 88;
|
|
||||||
return {
|
return {
|
||||||
x: info.x + trackPad,
|
x: info.x + SLIDER_PAD,
|
||||||
y: info.y + 8,
|
y: info.y + info.h / 2 - 3,
|
||||||
w: Math.max(40, info.w - trackPad - 8),
|
w: Math.max(20, info.w - SLIDER_PAD - 24),
|
||||||
h: 6,
|
h: 6,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImageRect(width, height) {
|
function getImageRect(width, height) {
|
||||||
return {
|
return {
|
||||||
x: OUTER_PAD,
|
x: 6,
|
||||||
y: IMAGE_TOP,
|
y: IMAGE_TOP,
|
||||||
w: Math.max(40, width - OUTER_PAD * 2),
|
w: Math.max(40, width - 12),
|
||||||
h: Math.max(80, height - IMAGE_TOP - OUTER_PAD),
|
h: Math.max(80, height - IMAGE_TOP - 6),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,15 +151,101 @@ function drawButton(ctx, button, hovered) {
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.fillStyle = button.active ? "#ffffff" : hovered ? "#ebebeb" : MUTED;
|
ctx.fillStyle = button.active ? "#ffffff" : hovered ? "#ebebeb" : MUTED;
|
||||||
ctx.font = "10px sans-serif";
|
let fontSize = 9;
|
||||||
|
ctx.font = `${fontSize}px 'Segoe UI',sans-serif`;
|
||||||
|
while (fontSize > 7 && ctx.measureText(button.label).width > button.w - 8) {
|
||||||
|
fontSize -= 0.5;
|
||||||
|
ctx.font = `${fontSize}px 'Segoe UI',sans-serif`;
|
||||||
|
}
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.fillText(button.label, button.x + button.w / 2, button.y + button.h / 2);
|
ctx.fillText(button.label, button.x + button.w / 2, button.y + button.h / 2);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dimsText(image) {
|
||||||
|
return image ? `${image.naturalWidth}x${image.naturalHeight}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizesDiffer(node) {
|
||||||
|
return !!(
|
||||||
|
node?._cmpImg1
|
||||||
|
&& node?._cmpImg2
|
||||||
|
&& (node._cmpImg1.naturalWidth !== node._cmpImg2.naturalWidth
|
||||||
|
|| node._cmpImg1.naturalHeight !== node._cmpImg2.naturalHeight)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillTextVCenter(ctx, text, x, yMid) {
|
||||||
|
const metrics = ctx.measureText(text);
|
||||||
|
if (metrics.actualBoundingBoxAscent != null && metrics.actualBoundingBoxDescent != null) {
|
||||||
|
ctx.textBaseline = "alphabetic";
|
||||||
|
ctx.fillText(text, x, yMid + (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(text, x, yMid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSizeBadge(ctx, cx, cy, label) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = BRAND;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, BADGE_R, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = "#ffffff";
|
||||||
|
ctx.font = BADGE_FONT;
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(String(label), cx, cy + 0.5);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSizeLabels(ctx, node, width) {
|
||||||
|
if (!node._cmpImg1 && !node._cmpImg2) return;
|
||||||
|
|
||||||
|
const layout = rowLayout(width);
|
||||||
|
const infoFull = {
|
||||||
|
x: layout.leftPad,
|
||||||
|
y: INFO_TOP,
|
||||||
|
w: layout.buttonWidth * MODE_LABELS.length + BUTTON_GAP * (MODE_LABELS.length - 1),
|
||||||
|
h: BUTTON_H,
|
||||||
|
};
|
||||||
|
const rightEdge = infoFull.x + infoFull.w;
|
||||||
|
const yMid = infoFull.y + infoFull.h / 2;
|
||||||
|
const textColor = sizesDiffer(node) ? BRAND : "#cfcfcf";
|
||||||
|
|
||||||
|
if (node._cmpImg1) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(infoFull.x, infoFull.y, SIZE_RESERVE, infoFull.h);
|
||||||
|
ctx.clip();
|
||||||
|
const cx = infoFull.x + BADGE_R;
|
||||||
|
drawSizeBadge(ctx, cx, yMid, 1);
|
||||||
|
ctx.font = SIZE_FONT;
|
||||||
|
ctx.fillStyle = textColor;
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
fillTextVCenter(ctx, dimsText(node._cmpImg1), cx + BADGE_R + 5, yMid);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node._cmpImg2) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(rightEdge - SIZE_RESERVE, infoFull.y, SIZE_RESERVE, infoFull.h);
|
||||||
|
ctx.clip();
|
||||||
|
const cx = rightEdge - BADGE_R;
|
||||||
|
drawSizeBadge(ctx, cx, yMid, 2);
|
||||||
|
ctx.font = SIZE_FONT;
|
||||||
|
ctx.fillStyle = textColor;
|
||||||
|
ctx.textAlign = "right";
|
||||||
|
fillTextVCenter(ctx, dimsText(node._cmpImg2), cx - BADGE_R - 5, yMid);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function drawInfoBar(ctx, node, width) {
|
function drawInfoBar(ctx, node, width) {
|
||||||
const info = getInfoRect(width);
|
const info = getInfoRect(node, width);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = PANEL;
|
ctx.fillStyle = PANEL;
|
||||||
ctx.strokeStyle = BORDER;
|
ctx.strokeStyle = BORDER;
|
||||||
@@ -150,7 +259,7 @@ function drawInfoBar(ctx, node, width) {
|
|||||||
const images = getAvailableImages(node);
|
const images = getAvailableImages(node);
|
||||||
if (!images.length) {
|
if (!images.length) {
|
||||||
ctx.fillStyle = MUTED;
|
ctx.fillStyle = MUTED;
|
||||||
ctx.font = "11px sans-serif";
|
ctx.font = "11px 'Segoe UI',sans-serif";
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.fillText("Connect images and run to compare", info.x + info.w / 2, info.y + info.h / 2);
|
ctx.fillText("Connect images and run to compare", info.x + info.w / 2, info.y + info.h / 2);
|
||||||
@@ -159,9 +268,9 @@ function drawInfoBar(ctx, node, width) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node._cmpMode === 4) {
|
if (node._cmpMode === 4) {
|
||||||
const slider = getSliderRect(width);
|
const slider = getSliderRect(node, width);
|
||||||
ctx.fillStyle = TEXT;
|
ctx.fillStyle = TEXT;
|
||||||
ctx.font = "11px sans-serif";
|
ctx.font = "11px 'Segoe UI',sans-serif";
|
||||||
ctx.textAlign = "left";
|
ctx.textAlign = "left";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.fillText(`Opacity ${Math.round(node._cmpOpacity * 100)}%`, info.x + 10, info.y + info.h / 2);
|
ctx.fillText(`Opacity ${Math.round(node._cmpOpacity * 100)}%`, info.x + 10, info.y + info.h / 2);
|
||||||
@@ -187,13 +296,18 @@ function drawInfoBar(ctx, node, width) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = images.map(({ slot, image }) => `${slot}: ${image.naturalWidth}x${image.naturalHeight}`);
|
if (node._cmpMode >= 2) {
|
||||||
ctx.fillStyle = TEXT;
|
const hint = MODE_HINTS[node._cmpMode];
|
||||||
ctx.font = "11px sans-serif";
|
if (hint) {
|
||||||
ctx.textAlign = "center";
|
ctx.fillStyle = TEXT;
|
||||||
ctx.textBaseline = "middle";
|
ctx.font = "11px 'Segoe UI',sans-serif";
|
||||||
ctx.fillText(parts.join(" "), info.x + info.w / 2, info.y + info.h / 2);
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(hint, info.x + info.w / 2, info.y + info.h / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
drawSizeLabels(ctx, node, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawEmptyState(ctx, rect) {
|
function drawEmptyState(ctx, rect) {
|
||||||
@@ -325,10 +439,10 @@ function paintNode(ctx, node, width, height, mouse) {
|
|||||||
if (node._cmpMode === 2 || node._cmpMode === 3) {
|
if (node._cmpMode === 2 || node._cmpMode === 3) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = MUTED;
|
ctx.fillStyle = MUTED;
|
||||||
ctx.font = "11px sans-serif";
|
ctx.font = "11px 'Segoe UI',sans-serif";
|
||||||
ctx.textAlign = "right";
|
ctx.textAlign = "right";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = "bottom";
|
||||||
ctx.fillText("Move over the image to slide", width - OUTER_PAD, height - 4);
|
ctx.fillText("Move over the image to slide", width - 8, height - 4);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,10 +462,32 @@ function updateImageInteraction(node, x, y, width, height) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateOpacityFromX(node, x, width) {
|
function updateOpacityFromX(node, x, width) {
|
||||||
const slider = getSliderRect(width);
|
const slider = getSliderRect(node, width);
|
||||||
node._cmpOpacity = clamp((x - slider.x) / slider.w, 0, 1);
|
node._cmpOpacity = clamp((x - slider.x) / slider.w, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCursor(node, x, y, width, height) {
|
||||||
|
const buttons = getButtons(node, width);
|
||||||
|
if (buttons.some((button) => pointInRect(x, y, button))) return "pointer";
|
||||||
|
if (node._cmpMode === 4) {
|
||||||
|
const slider = getSliderRect(node, width);
|
||||||
|
if (pointInRect(x, y, {
|
||||||
|
x: slider.x - 10,
|
||||||
|
y: slider.y - 10,
|
||||||
|
w: slider.w + 20,
|
||||||
|
h: slider.h + 20,
|
||||||
|
})) {
|
||||||
|
return "pointer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rect = getImageRect(width, height);
|
||||||
|
if (pointInRect(x, y, rect)) {
|
||||||
|
if (node._cmpMode === 2) return "ew-resize";
|
||||||
|
if (node._cmpMode === 3) return "ns-resize";
|
||||||
|
}
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
function pointerDown(node, x, y, width, height) {
|
function pointerDown(node, x, y, width, height) {
|
||||||
const buttons = getButtons(node, width);
|
const buttons = getButtons(node, width);
|
||||||
for (const button of buttons) {
|
for (const button of buttons) {
|
||||||
@@ -362,11 +498,12 @@ function pointerDown(node, x, y, width, height) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const slider = getSliderRect(node, width);
|
||||||
if (node._cmpMode === 4 && pointInRect(x, y, {
|
if (node._cmpMode === 4 && pointInRect(x, y, {
|
||||||
x: getSliderRect(width).x - 10,
|
x: slider.x - 10,
|
||||||
y: getSliderRect(width).y - 10,
|
y: slider.y - 10,
|
||||||
w: getSliderRect(width).w + 20,
|
w: slider.w + 20,
|
||||||
h: getSliderRect(width).h + 20,
|
h: slider.h + 20,
|
||||||
})) {
|
})) {
|
||||||
node._cmpDraggingOpacity = true;
|
node._cmpDraggingOpacity = true;
|
||||||
updateOpacityFromX(node, x, width);
|
updateOpacityFromX(node, x, width);
|
||||||
@@ -445,6 +582,7 @@ function createDomWidget(node) {
|
|||||||
|
|
||||||
root.addEventListener("pointerdown", (event) => {
|
root.addEventListener("pointerdown", (event) => {
|
||||||
const [x, y] = localPos(event);
|
const [x, y] = localPos(event);
|
||||||
|
root.style.cursor = getCursor(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM);
|
||||||
if (pointerDown(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM)) {
|
if (pointerDown(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM)) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (node._cmpDraggingOpacity) {
|
if (node._cmpDraggingOpacity) {
|
||||||
@@ -461,11 +599,14 @@ function createDomWidget(node) {
|
|||||||
root.addEventListener("pointermove", (event) => {
|
root.addEventListener("pointermove", (event) => {
|
||||||
const [x, y] = localPos(event);
|
const [x, y] = localPos(event);
|
||||||
pointerMove(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM);
|
pointerMove(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM);
|
||||||
|
root.style.cursor = getCursor(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM);
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
root.addEventListener("pointerup", (event) => {
|
root.addEventListener("pointerup", (event) => {
|
||||||
pointerUp(node);
|
pointerUp(node);
|
||||||
|
const [x, y] = localPos(event);
|
||||||
|
root.style.cursor = getCursor(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM);
|
||||||
try {
|
try {
|
||||||
root.releasePointerCapture(event.pointerId);
|
root.releasePointerCapture(event.pointerId);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@@ -477,6 +618,7 @@ function createDomWidget(node) {
|
|||||||
root.addEventListener("pointerleave", () => {
|
root.addEventListener("pointerleave", () => {
|
||||||
pointerUp(node);
|
pointerUp(node);
|
||||||
node._cmpMouse = null;
|
node._cmpMouse = null;
|
||||||
|
root.style.cursor = "default";
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user