Files
MajoorWaldi-ComfyUI-Majoor-…/js/preview/shared/preview-widget.js
T
Ewald 068792c764 feat(color-correct): add zone-based color adjustment and soft brush drawing
- Implement global, shadows, midtones, and highlights zones for color correction
- Add brightness, contrast, saturation, vibrance, and gamma controls per zone
- Include temperature and tint adjustments with improved hue adjustment range
- Introduce soft brush drawing feature with adjustable softness
- Enhance draw tool with improved cursor feedback and eraser functionality
- Optimize performance with requestAnimationFrame for certain interactions
- Refactor color correction widget synchronization and reset logic
2026-04-24 23:52:41 +02:00

762 lines
32 KiB
JavaScript

import { ensureState } from "./state.js";
import { IMAGEOPS_CLASSES } from "./classes.js";
import { isNode as isPreviewNode } from "../nodes/preview.js";
import { isNode as isColorCorrectNode } from "../nodes/color-correct.js";
import { isNode as isCropNode } from "../nodes/crop.js";
import { isNode as isDrawNode } from "../nodes/draw.js";
import { isNode as isCompNode } from "../nodes/comp.js";
import { styleSoftButton, styleSoftField, styleSoftRange, styleInlineAction, createColorSwatch } from "./dom-styles.js";
import { COMP_BLEND_MODES } from "../comp.js";
function getNodePreviewMinHeight(node) {
if (isDrawNode(node)) return 220;
if (isColorCorrectNode(node)) return 490;
if (isCompNode(node)) return 400;
if (isPreviewNode(node)) return 360;
return 320;
}
function getNodePreviewTargetSize(node, root, fallbackWidth = 360) {
const minWidth = 360;
const minHeight = getNodePreviewMinHeight(node);
const measuredWidth = root ? Math.max(root.offsetWidth, root.scrollWidth) + 12 : 0;
const measuredHeight = root ? Math.max(root.offsetHeight, root.scrollHeight) + 12 : 0;
return [
Math.max(minWidth, fallbackWidth, measuredWidth),
Math.max(minHeight, measuredHeight)
];
}
function ensurePreviewWidget(node, progress, canvasSize) {
if (!IMAGEOPS_CLASSES.has(node.comfyClass)) return null;
const st = ensureState(node);
if (st.canvas) return st;
const previewNode = isPreviewNode(node);
const colorCorrectNode = isColorCorrectNode(node);
const cropNode = isCropNode(node);
const drawNode = isDrawNode(node);
const compNode = isCompNode(node);
const root = document.createElement("div");
root.style.width = "100%";
root.style.boxSizing = "border-box";
root.style.padding = "6px";
const canvas = document.createElement("canvas");
canvas.width = canvasSize;
canvas.height = canvasSize;
canvas.style.width = "100%";
canvas.style.height = "auto";
canvas.style.display = "block";
canvas.style.borderRadius = "8px";
canvas.style.background = "rgba(0,0,0,0.35)";
canvas.style.border = "1px solid rgba(255,255,255,0.08)";
canvas.style.touchAction = "none";
canvas.tabIndex = 0;
const mediaWrap = document.createElement("div");
mediaWrap.style.width = "100%";
mediaWrap.style.display = "none";
mediaWrap.style.borderRadius = "8px";
mediaWrap.style.overflow = "hidden";
mediaWrap.style.background = "rgba(0,0,0,0.35)";
mediaWrap.style.border = "1px solid rgba(255,255,255,0.08)";
const mediaVideo = document.createElement("video");
mediaVideo.controls = false;
mediaVideo.loop = true;
mediaVideo.muted = true;
mediaVideo.playsInline = true;
mediaVideo.autoplay = true;
mediaVideo.preload = "metadata";
mediaVideo.style.width = "100%";
mediaVideo.style.height = "auto";
mediaVideo.style.display = "block";
mediaVideo.style.background = "transparent";
mediaVideo.hidden = true;
const mediaImage = document.createElement("img");
mediaImage.style.width = "100%";
mediaImage.style.height = "auto";
mediaImage.style.display = "block";
mediaImage.style.background = "transparent";
mediaImage.hidden = true;
mediaWrap.appendChild(mediaVideo);
mediaWrap.appendChild(mediaImage);
const metaRow = document.createElement("div");
metaRow.style.marginTop = "6px";
metaRow.style.display = "flex";
metaRow.style.alignItems = "center";
metaRow.style.justifyContent = "space-between";
metaRow.style.gap = "8px";
const info = document.createElement("div");
info.style.fontSize = "11px";
info.style.opacity = "0.8";
info.style.flex = "1 1 auto";
info.textContent = "Live preview (no queue)";
let cropResetButton = null;
if (cropNode) {
cropResetButton = document.createElement("button");
cropResetButton.type = "button";
cropResetButton.textContent = "Reset";
styleInlineAction(cropResetButton);
cropResetButton.style.opacity = "0.85";
}
let previewControls = null;
if (previewNode) {
previewControls = document.createElement("div");
previewControls.style.marginTop = "8px";
previewControls.style.display = "grid";
previewControls.style.gap = "6px";
const targetRow = document.createElement("div");
targetRow.style.display = "grid";
targetRow.style.gridTemplateColumns = "auto auto auto minmax(0,1fr)";
targetRow.style.gap = "6px";
targetRow.style.alignItems = "center";
for (const [value, label] of [["auto", "Auto"], ["image", "Image"], ["mask", "Mask"]]) {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.dataset.previewTarget = value;
styleSoftButton(button, value === "auto");
targetRow.appendChild(button);
}
const modeSelect = document.createElement("select");
modeSelect.dataset.previewMode = "1";
modeSelect.title = "Preview export mode";
styleSoftField(modeSelect);
modeSelect.style.width = "100%";
for (const mode of ["images", "strip", "animated_webp", "animated_gif"]) {
const option = document.createElement("option");
option.value = mode;
option.textContent = mode.replace(/_/g, " ");
modeSelect.appendChild(option);
}
targetRow.appendChild(modeSelect);
previewControls.appendChild(targetRow);
}
let colorWheelCanvas = null;
let colorHueLabel = null;
let colorSatLabel = null;
let colorSwatch = null;
let colorResetButton = null;
let colorTemperatureInput = null;
let colorTemperatureLabel = null;
let colorTintInput = null;
let colorTintLabel = null;
let colorContrastInput = null;
let colorContrastLabel = null;
let colorSaturationInput = null;
let colorSaturationValueLabel = null;
let colorVibranceInput = null;
let colorVibranceLabel = null;
let colorGammaInput = null;
let colorGammaLabel = null;
let colorShadowWheelCanvas = null;
let colorShadowLabel = null;
let colorMidtoneWheelCanvas = null;
let colorMidtoneLabel = null;
let colorHighlightWheelCanvas = null;
let colorHighlightLabel = null;
let colorBrightnessInput = null;
let colorBrightnessLabel = null;
let colorZoneTabsRow = null;
let colorZoneTabGlobal = null;
let colorZoneTabShadows = null;
let colorZoneTabMidtones = null;
let colorZoneTabHighlights = null;
let colorControls = null;
if (colorCorrectNode) {
colorControls = document.createElement("div");
colorControls.style.marginTop = "8px";
colorControls.style.display = "grid";
colorControls.style.gridTemplateColumns = "minmax(0,1fr)";
colorControls.style.gap = "10px";
colorControls.style.alignItems = "stretch";
colorWheelCanvas = document.createElement("canvas");
colorWheelCanvas.width = 152;
colorWheelCanvas.height = 152;
colorWheelCanvas.style.width = "100%";
colorWheelCanvas.style.aspectRatio = "1";
colorWheelCanvas.style.borderRadius = "999px";
colorWheelCanvas.style.cursor = "crosshair";
colorWheelCanvas.style.background = "radial-gradient(circle at center, rgba(255,255,255,0.08), rgba(255,255,255,0.02) 55%, rgba(0,0,0,0.18) 100%)";
colorWheelCanvas.style.border = "1px solid rgba(255,255,255,0.1)";
colorWheelCanvas.style.boxSizing = "border-box";
const colorMeta = document.createElement("div");
colorMeta.style.display = "grid";
colorMeta.style.gap = "6px";
colorMeta.style.alignContent = "start";
const colorTitle = document.createElement("div");
colorTitle.textContent = "Color wheel";
colorTitle.style.fontSize = "12px";
colorTitle.style.fontWeight = "600";
colorTitle.style.letterSpacing = "0.02em";
const colorHint = document.createElement("div");
colorHint.textContent = "Drag for hue and chroma. Use the saturation slider below for desat or negative fine tuning.";
colorHint.style.fontSize = "11px";
colorHint.style.opacity = "0.72";
colorHint.style.lineHeight = "1.35";
colorSwatch = document.createElement("div");
colorSwatch.style.height = "34px";
colorSwatch.style.borderRadius = "10px";
colorSwatch.style.border = "1px solid rgba(255,255,255,0.12)";
colorSwatch.style.background = "linear-gradient(135deg, rgba(255,255,255,0.18), rgba(255,255,255,0.02))";
const readoutRow = document.createElement("div");
readoutRow.style.display = "grid";
readoutRow.style.gridTemplateColumns = "minmax(0,1fr) minmax(0,1fr)";
readoutRow.style.gap = "6px";
colorHueLabel = document.createElement("div");
colorHueLabel.style.fontSize = "11px";
colorHueLabel.style.opacity = "0.86";
colorHueLabel.textContent = "Hue 0 deg";
colorSatLabel = document.createElement("div");
colorSatLabel.style.fontSize = "11px";
colorSatLabel.style.opacity = "0.86";
colorSatLabel.style.textAlign = "right";
colorSatLabel.textContent = "Sat 0%";
readoutRow.appendChild(colorHueLabel);
readoutRow.appendChild(colorSatLabel);
colorResetButton = document.createElement("button");
colorResetButton.type = "button";
colorResetButton.textContent = "Reset wheel";
styleSoftButton(colorResetButton, false);
colorResetButton.style.justifySelf = "start";
colorMeta.appendChild(colorTitle);
colorMeta.appendChild(colorHint);
colorMeta.appendChild(colorSwatch);
colorMeta.appendChild(readoutRow);
colorMeta.appendChild(colorResetButton);
const globalWheelRow = document.createElement("div");
globalWheelRow.style.display = "grid";
globalWheelRow.style.gridTemplateColumns = "minmax(132px, 152px) minmax(0,1fr)";
globalWheelRow.style.gap = "10px";
globalWheelRow.style.alignItems = "stretch";
globalWheelRow.appendChild(colorWheelCanvas);
globalWheelRow.appendChild(colorMeta);
colorControls.appendChild(globalWheelRow);
const makeRangeRow = (labelText, input, valueLabel, min, max, step, value, accent) => {
const row = document.createElement("div");
row.style.display = "grid";
row.style.gridTemplateColumns = "68px minmax(0,1fr) 40px";
row.style.gap = "8px";
row.style.alignItems = "center";
const label = document.createElement("div");
label.textContent = labelText;
label.style.fontSize = "11px";
label.style.opacity = "0.8";
input.type = "range";
input.min = String(min);
input.max = String(max);
input.step = String(step);
input.value = String(value);
styleSoftRange(input);
if (accent) input.style.accentColor = accent;
valueLabel.style.fontSize = "11px";
valueLabel.style.opacity = "0.84";
valueLabel.style.textAlign = "right";
valueLabel.textContent = String(value);
row.appendChild(label);
row.appendChild(input);
row.appendChild(valueLabel);
return row;
};
const primariesCard = document.createElement("div");
primariesCard.style.display = "grid";
primariesCard.style.gap = "8px";
const primariesTop = document.createElement("div");
primariesTop.style.display = "flex";
primariesTop.style.alignItems = "center";
primariesTop.style.justifyContent = "space-between";
primariesTop.style.gap = "8px";
const primariesTitle = document.createElement("div");
primariesTitle.textContent = "Primaries";
primariesTitle.style.fontSize = "12px";
primariesTitle.style.fontWeight = "600";
primariesTitle.style.letterSpacing = "0.02em";
primariesTop.appendChild(primariesTitle);
primariesTop.appendChild(colorResetButton);
primariesCard.appendChild(primariesTop);
colorZoneTabsRow = document.createElement("div");
colorZoneTabsRow.style.display = "grid";
colorZoneTabsRow.style.gridTemplateColumns = "repeat(4, 1fr)";
colorZoneTabsRow.style.gap = "4px";
const makeZoneTab = (label) => {
const btn = document.createElement("button");
btn.type = "button";
btn.textContent = label;
btn.style.fontSize = "11px";
btn.style.padding = "4px 6px";
styleSoftButton(btn, false);
return btn;
};
colorZoneTabGlobal = makeZoneTab("Global");
colorZoneTabShadows = makeZoneTab("Shadows");
colorZoneTabMidtones = makeZoneTab("Midtones");
colorZoneTabHighlights = makeZoneTab("Highlights");
colorZoneTabsRow.appendChild(colorZoneTabGlobal);
colorZoneTabsRow.appendChild(colorZoneTabShadows);
colorZoneTabsRow.appendChild(colorZoneTabMidtones);
colorZoneTabsRow.appendChild(colorZoneTabHighlights);
primariesCard.appendChild(colorZoneTabsRow);
colorBrightnessInput = document.createElement("input");
colorBrightnessLabel = document.createElement("div");
primariesCard.appendChild(makeRangeRow("Bright", colorBrightnessInput, colorBrightnessLabel, -100, 100, 1, 0, ""));
colorTemperatureInput = document.createElement("input");
colorTemperatureLabel = document.createElement("div");
primariesCard.appendChild(makeRangeRow("Temp", colorTemperatureInput, colorTemperatureLabel, -100, 100, 1, 0, "#ffb347"));
colorTintInput = document.createElement("input");
colorTintLabel = document.createElement("div");
primariesCard.appendChild(makeRangeRow("Hue", colorTintInput, colorTintLabel, -180, 180, 1, 0, "#d77dff"));
colorContrastInput = document.createElement("input");
colorContrastLabel = document.createElement("div");
primariesCard.appendChild(makeRangeRow("Contrast", colorContrastInput, colorContrastLabel, -100, 100, 1, 0, ""));
colorSaturationInput = document.createElement("input");
colorSaturationValueLabel = document.createElement("div");
primariesCard.appendChild(makeRangeRow("Sat", colorSaturationInput, colorSaturationValueLabel, -100, 100, 1, 0, ""));
colorVibranceInput = document.createElement("input");
colorVibranceLabel = document.createElement("div");
primariesCard.appendChild(makeRangeRow("Vibrance", colorVibranceInput, colorVibranceLabel, -100, 100, 1, 0, ""));
colorGammaInput = document.createElement("input");
colorGammaLabel = document.createElement("div");
primariesCard.appendChild(makeRangeRow("Gamma", colorGammaInput, colorGammaLabel, 0.2, 2.2, 0.01, 1, ""));
colorControls.appendChild(primariesCard);
}
let compAddButton = null;
let compResetButton = null;
let compResizeButton = null;
let compCornerPinButton = null;
let compModeSelect = null;
let compOpacityInput = null;
let compOpacityLabel = null;
let compLayerLabel = null;
let compControls = null;
if (compNode) {
compControls = document.createElement("div");
compControls.style.marginTop = "8px";
compControls.style.display = "grid";
compControls.style.gridTemplateColumns = "auto auto auto auto 1fr";
compControls.style.gap = "6px";
compControls.style.alignItems = "center";
compAddButton = document.createElement("button");
compAddButton.type = "button";
compAddButton.textContent = "+ Layer";
styleSoftButton(compAddButton, false);
compResetButton = document.createElement("button");
compResetButton.type = "button";
compResetButton.textContent = "Reset";
styleSoftButton(compResetButton, false);
compResizeButton = document.createElement("button");
compResizeButton.type = "button";
compResizeButton.textContent = "Scale";
styleSoftButton(compResizeButton, true);
compCornerPinButton = document.createElement("button");
compCornerPinButton.type = "button";
compCornerPinButton.textContent = "Pin";
styleSoftButton(compCornerPinButton, false);
compLayerLabel = document.createElement("div");
compLayerLabel.style.fontSize = "11px";
compLayerLabel.style.opacity = "0.85";
compLayerLabel.style.justifySelf = "end";
compLayerLabel.textContent = "Layer";
const compBottomRow = document.createElement("div");
compBottomRow.style.gridColumn = "1 / -1";
compBottomRow.style.display = "grid";
compBottomRow.style.gridTemplateColumns = "minmax(0,1fr) auto minmax(110px,0.9fr)";
compBottomRow.style.gap = "6px";
compBottomRow.style.alignItems = "center";
compModeSelect = document.createElement("select");
compModeSelect.style.width = "100%";
styleSoftField(compModeSelect);
for (const mode of COMP_BLEND_MODES) {
const option = document.createElement("option");
option.value = mode;
option.textContent = mode.replace("_", " ");
compModeSelect.appendChild(option);
}
compOpacityLabel = document.createElement("div");
compOpacityLabel.textContent = "100%";
compOpacityLabel.style.fontSize = "11px";
compOpacityLabel.style.opacity = "0.82";
compOpacityLabel.style.justifySelf = "end";
compOpacityInput = document.createElement("input");
compOpacityInput.type = "range";
compOpacityInput.min = "0";
compOpacityInput.max = "100";
compOpacityInput.step = "1";
compOpacityInput.value = "100";
compOpacityInput.title = "Layer opacity";
styleSoftRange(compOpacityInput);
compControls.appendChild(compAddButton);
compControls.appendChild(compResetButton);
compControls.appendChild(compResizeButton);
compControls.appendChild(compCornerPinButton);
compControls.appendChild(compLayerLabel);
compBottomRow.appendChild(compModeSelect);
compBottomRow.appendChild(compOpacityLabel);
compBottomRow.appendChild(compOpacityInput);
compControls.appendChild(compBottomRow);
}
let drawBrushButton = null;
let drawEraserButton = null;
let drawClearButton = null;
let drawColorInput = null;
let drawEdgeSelect = null;
let drawSoftnessInput = null;
let drawSoftnessLabel = null;
let drawOpacityInput = null;
let drawOpacityLabel = null;
let drawSizeInput = null;
let drawSizeLabel = null;
let drawWidthInput = null;
let drawHeightInput = null;
let drawLinkButton = null;
let drawBgColorInput = null;
let drawOverlayFormatSelect = null;
let drawPressureSizeInput = null;
let drawPressureOpacityInput = null;
let drawTiltSizeInput = null;
let drawControls = null;
if (drawNode) {
drawControls = document.createElement("div");
drawControls.style.marginTop = "8px";
drawControls.style.display = "grid";
drawControls.style.gap = "8px";
const topRow = document.createElement("div");
topRow.style.display = "flex";
topRow.style.alignItems = "center";
topRow.style.justifyContent = "space-between";
topRow.style.gap = "8px";
const toolRow = document.createElement("div");
toolRow.style.display = "flex";
toolRow.style.alignItems = "center";
toolRow.style.gap = "6px";
drawBrushButton = document.createElement("button");
drawBrushButton.type = "button";
drawBrushButton.textContent = "Brush";
styleSoftButton(drawBrushButton, true);
drawEraserButton = document.createElement("button");
drawEraserButton.type = "button";
drawEraserButton.textContent = "Eraser";
styleSoftButton(drawEraserButton, false);
drawClearButton = document.createElement("button");
drawClearButton.type = "button";
drawClearButton.textContent = "Clear";
styleInlineAction(drawClearButton);
drawOverlayFormatSelect = document.createElement("select");
drawOverlayFormatSelect.title = "Overlay format";
drawOverlayFormatSelect.style.width = "72px";
styleSoftField(drawOverlayFormatSelect);
for (const format of ["png", "webp"]) {
const option = document.createElement("option");
option.value = format;
option.textContent = format.toUpperCase();
drawOverlayFormatSelect.appendChild(option);
}
const rightTools = document.createElement("div");
rightTools.style.display = "flex";
rightTools.style.alignItems = "center";
rightTools.style.gap = "6px";
rightTools.appendChild(drawOverlayFormatSelect);
rightTools.appendChild(drawClearButton);
toolRow.appendChild(drawBrushButton);
toolRow.appendChild(drawEraserButton);
topRow.appendChild(toolRow);
topRow.appendChild(rightTools);
const strokeRow = document.createElement("div");
strokeRow.style.display = "grid";
strokeRow.style.gridTemplateColumns = "auto minmax(0,1fr) auto auto minmax(0,1fr)";
strokeRow.style.alignItems = "center";
strokeRow.style.gap = "6px";
const colorLabel = document.createElement("div");
colorLabel.textContent = "Color";
colorLabel.style.fontSize = "11px";
colorLabel.style.opacity = "0.78";
const colorSwatchResult = createColorSwatch("#FFFFFF");
drawColorInput = colorSwatchResult.input;
drawEdgeSelect = document.createElement("select");
styleSoftField(drawEdgeSelect);
drawEdgeSelect.title = "Brush edge";
for (const edge of ["hard", "soft"]) {
const option = document.createElement("option");
option.value = edge;
option.textContent = edge === "hard" ? "Hard edge" : "Soft edge";
drawEdgeSelect.appendChild(option);
}
drawOpacityLabel = document.createElement("div");
drawOpacityLabel.textContent = "100%";
drawOpacityLabel.style.fontSize = "11px";
drawOpacityLabel.style.opacity = "0.82";
drawOpacityLabel.style.justifySelf = "end";
drawOpacityInput = document.createElement("input");
drawOpacityInput.type = "range";
drawOpacityInput.min = "0";
drawOpacityInput.max = "100";
drawOpacityInput.step = "1";
drawOpacityInput.value = "100";
drawOpacityInput.title = "Brush opacity";
styleSoftRange(drawOpacityInput);
strokeRow.appendChild(colorLabel);
strokeRow.appendChild(colorSwatchResult.host);
strokeRow.appendChild(drawEdgeSelect);
strokeRow.appendChild(drawOpacityLabel);
strokeRow.appendChild(drawOpacityInput);
const softnessRow = document.createElement("div");
softnessRow.style.display = "grid";
softnessRow.style.gridTemplateColumns = "auto minmax(0,1fr) auto";
softnessRow.style.alignItems = "center";
softnessRow.style.gap = "6px";
const softnessLabel = document.createElement("div");
softnessLabel.textContent = "Softness";
softnessLabel.style.fontSize = "11px";
softnessLabel.style.opacity = "0.78";
drawSoftnessInput = document.createElement("input");
drawSoftnessInput.type = "range";
drawSoftnessInput.min = "0";
drawSoftnessInput.max = "100";
drawSoftnessInput.step = "1";
drawSoftnessInput.value = "50";
drawSoftnessInput.title = "Soft brush feather (only when edge=Soft)";
styleSoftRange(drawSoftnessInput);
drawSoftnessLabel = document.createElement("div");
drawSoftnessLabel.textContent = "50%";
drawSoftnessLabel.style.fontSize = "11px";
drawSoftnessLabel.style.opacity = "0.82";
drawSoftnessLabel.style.justifySelf = "end";
softnessRow.appendChild(softnessLabel);
softnessRow.appendChild(drawSoftnessInput);
softnessRow.appendChild(drawSoftnessLabel);
const sizeRow = document.createElement("div");
sizeRow.style.display = "grid";
sizeRow.style.gridTemplateColumns = "auto minmax(0,1fr) auto auto auto auto auto";
sizeRow.style.alignItems = "center";
sizeRow.style.gap = "6px";
const sizeLabel = document.createElement("div");
sizeLabel.textContent = "Size";
sizeLabel.style.fontSize = "11px";
sizeLabel.style.opacity = "0.78";
drawSizeInput = document.createElement("input");
drawSizeInput.type = "range";
drawSizeInput.min = "1";
drawSizeInput.max = "256";
drawSizeInput.step = "1";
drawSizeInput.value = "10";
drawSizeInput.title = "Brush size";
styleSoftRange(drawSizeInput);
drawSizeLabel = document.createElement("div");
drawSizeLabel.textContent = "10";
drawSizeLabel.style.fontSize = "11px";
drawSizeLabel.style.opacity = "0.82";
drawSizeLabel.style.justifySelf = "end";
drawWidthInput = document.createElement("input");
drawWidthInput.type = "number";
drawWidthInput.min = "64";
drawWidthInput.max = "4096";
drawWidthInput.step = "64";
drawWidthInput.value = "1024";
drawWidthInput.placeholder = "W";
drawWidthInput.title = "Width";
drawWidthInput.style.width = "62px";
styleSoftField(drawWidthInput);
drawHeightInput = document.createElement("input");
drawHeightInput.type = "number";
drawHeightInput.min = "64";
drawHeightInput.max = "4096";
drawHeightInput.step = "64";
drawHeightInput.value = "1024";
drawHeightInput.placeholder = "H";
drawHeightInput.title = "Height";
drawHeightInput.style.width = "62px";
styleSoftField(drawHeightInput);
drawLinkButton = document.createElement("button");
drawLinkButton.type = "button";
drawLinkButton.textContent = "Linked";
styleSoftButton(drawLinkButton, true);
const bgColorSwatchResult = createColorSwatch("#000000", { compact: true, title: "Background color" });
drawBgColorInput = bgColorSwatchResult.input;
sizeRow.appendChild(sizeLabel);
sizeRow.appendChild(drawSizeInput);
sizeRow.appendChild(drawSizeLabel);
sizeRow.appendChild(drawWidthInput);
sizeRow.appendChild(drawHeightInput);
sizeRow.appendChild(drawLinkButton);
sizeRow.appendChild(bgColorSwatchResult.host);
const dynamicsRow = document.createElement("div");
dynamicsRow.style.display = "flex";
dynamicsRow.style.alignItems = "center";
dynamicsRow.style.gap = "10px";
dynamicsRow.style.flexWrap = "wrap";
dynamicsRow.style.fontSize = "11px";
dynamicsRow.style.opacity = "0.86";
const dynamicsLabel = document.createElement("span");
dynamicsLabel.textContent = "Dynamics";
dynamicsLabel.style.opacity = "0.78";
const makeDynamicsToggle = (text, title) => {
const label = document.createElement("label");
label.style.display = "inline-flex";
label.style.alignItems = "center";
label.style.gap = "4px";
label.title = title;
const input = document.createElement("input");
input.type = "checkbox";
input.style.margin = "0";
label.appendChild(input);
label.appendChild(document.createTextNode(text));
return [label, input];
};
const [pressureSizeLabel, pressureSizeInput] = makeDynamicsToggle("Size pressure", "Pen pressure changes brush diameter");
const [pressureOpacityLabel, pressureOpacityInput] = makeDynamicsToggle("Opacity pressure", "Pen pressure changes brush opacity");
const [tiltSizeLabel, tiltSizeInput] = makeDynamicsToggle("Tilt size", "Pen tilt widens the brush footprint");
drawPressureSizeInput = pressureSizeInput;
drawPressureOpacityInput = pressureOpacityInput;
drawTiltSizeInput = tiltSizeInput;
dynamicsRow.appendChild(dynamicsLabel);
dynamicsRow.appendChild(pressureSizeLabel);
dynamicsRow.appendChild(pressureOpacityLabel);
dynamicsRow.appendChild(tiltSizeLabel);
drawControls.appendChild(topRow);
drawControls.appendChild(strokeRow);
drawControls.appendChild(softnessRow);
drawControls.appendChild(sizeRow);
drawControls.appendChild(dynamicsRow);
}
const progressWrap = document.createElement("div");
progressWrap.style.marginTop = "6px";
progressWrap.style.height = "6px";
progressWrap.style.borderRadius = "999px";
progressWrap.style.background = "rgba(255,255,255,0.12)";
progressWrap.style.overflow = "hidden";
progressWrap.style.display = "none";
const progressBar = document.createElement("div");
progressBar.style.height = "100%";
progressBar.style.width = "0%";
progressBar.style.borderRadius = "999px";
progressBar.style.background = "rgba(255,255,255,0.55)";
progressWrap.appendChild(progressBar);
root.appendChild(mediaWrap);
root.appendChild(canvas);
metaRow.appendChild(info);
if (cropResetButton) metaRow.appendChild(cropResetButton);
root.appendChild(metaRow);
if (previewControls) root.appendChild(previewControls);
if (colorControls) root.appendChild(colorControls);
if (drawControls) root.appendChild(drawControls);
if (compControls) root.appendChild(compControls);
root.appendChild(progressWrap);
root.style.pointerEvents = "auto";
const domMinHeight = getNodePreviewMinHeight(node);
if (typeof node.addDOMWidget === "function") {
node.addDOMWidget("preview", "ImageOpsPreview", root, {
serialize: false,
hideOnZoom: false,
getMinHeight: () => {
const mediaH = mediaWrap.style.display !== "none" ? mediaWrap.offsetHeight : 0;
const imageH = Math.max(canvas.offsetHeight, mediaH);
if (imageH === 0) return domMinHeight;
const metaH = metaRow.offsetHeight + 6;
const activeControls = previewControls ?? colorControls ?? drawControls ?? compControls;
const controlsH = activeControls ? activeControls.offsetHeight + 8 : 0;
const progressH = progressWrap.offsetHeight;
const naturalH = imageH + metaH + controlsH + progressH + 12;
return Math.max(domMinHeight, naturalH);
}
});
} else {
const domEl = node.domElement ?? node.element;
if (domEl instanceof HTMLElement) {
domEl.appendChild(root);
} else {
console.warn("[ImageOps] addDOMWidget unavailable and no DOM container found on node", node.id);
}
}
try {
const widgets = node.widgets ?? [];
const idx = widgets.findIndex((w) => w?.name === "preview");
if (idx > 0) {
const [removed] = widgets.splice(idx, 1);
widgets.unshift(removed);
}
} catch {
}
st.canvas = canvas;
st.info = info;
st.progressWrap = progressWrap;
st.progressBar = progressBar;
st.mediaWrap = mediaWrap;
st.mediaVideo = mediaVideo;
st.mediaImage = mediaImage;
st.cropResetButton = cropResetButton;
st.drawBrushButton = drawBrushButton;
st.drawEraserButton = drawEraserButton;
st.drawClearButton = drawClearButton;
st.drawColorInput = drawColorInput;
st.drawEdgeSelect = drawEdgeSelect;
st.drawSoftnessInput = drawSoftnessInput;
st.drawSoftnessLabel = drawSoftnessLabel;
st.drawOpacityInput = drawOpacityInput;
st.drawOpacityLabel = drawOpacityLabel;
st.drawSizeInput = drawSizeInput;
st.drawSizeLabel = drawSizeLabel;
st.drawWidthInput = drawWidthInput;
st.drawHeightInput = drawHeightInput;
st.drawLinkButton = drawLinkButton;
st.drawBgColorInput = drawBgColorInput;
st.drawOverlayFormatSelect = drawOverlayFormatSelect;
st.drawPressureSizeInput = drawPressureSizeInput;
st.drawPressureOpacityInput = drawPressureOpacityInput;
st.drawTiltSizeInput = drawTiltSizeInput;
st.colorWheelCanvas = colorWheelCanvas;
st.colorHueLabel = colorHueLabel;
st.colorSatLabel = colorSatLabel;
st.colorSwatch = colorSwatch;
st.colorResetButton = colorResetButton;
st.colorTemperatureInput = colorTemperatureInput;
st.colorTemperatureLabel = colorTemperatureLabel;
st.colorTintInput = colorTintInput;
st.colorTintLabel = colorTintLabel;
st.colorContrastInput = colorContrastInput;
st.colorContrastLabel = colorContrastLabel;
st.colorSaturationInput = colorSaturationInput;
st.colorSaturationValueLabel = colorSaturationValueLabel;
st.colorVibranceInput = colorVibranceInput;
st.colorVibranceLabel = colorVibranceLabel;
st.colorGammaInput = colorGammaInput;
st.colorGammaLabel = colorGammaLabel;
st.colorShadowWheelCanvas = colorShadowWheelCanvas;
st.colorShadowLabel = colorShadowLabel;
st.colorMidtoneWheelCanvas = colorMidtoneWheelCanvas;
st.colorMidtoneLabel = colorMidtoneLabel;
st.colorHighlightWheelCanvas = colorHighlightWheelCanvas;
st.colorHighlightLabel = colorHighlightLabel;
st.colorBrightnessInput = colorBrightnessInput;
st.colorBrightnessLabel = colorBrightnessLabel;
st.colorZoneTabsRow = colorZoneTabsRow;
st.colorZoneTabGlobal = colorZoneTabGlobal;
st.colorZoneTabShadows = colorZoneTabShadows;
st.colorZoneTabMidtones = colorZoneTabMidtones;
st.colorZoneTabHighlights = colorZoneTabHighlights;
st.compAddButton = compAddButton;
st.compResetButton = compResetButton;
st.compResizeButton = compResizeButton;
st.compCornerPinButton = compCornerPinButton;
st.compModeSelect = compModeSelect;
st.compOpacityInput = compOpacityInput;
st.compOpacityLabel = compOpacityLabel;
st.compLayerLabel = compLayerLabel;
try {
const cs = node.computeSize?.() ?? [360, domMinHeight];
node.setSize?.(getNodePreviewTargetSize(node, root, Math.max(cs[0], 360)));
node.resizable = true;
} catch {
}
setTimeout(() => {
try {
const cs2 = node.computeSize?.() ?? [360, domMinHeight];
node.setSize?.(getNodePreviewTargetSize(node, root, Math.max(cs2[0], 360)));
node.graph?.setDirtyCanvas(true, true);
} catch {
}
}, 100);
if (progress) {
progress.registerNodeWidget(node, progressWrap, progressBar);
}
return st;
}
export {
ensurePreviewWidget,
getNodePreviewMinHeight,
getNodePreviewTargetSize
};