- 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
211 lines
6.2 KiB
JavaScript
211 lines
6.2 KiB
JavaScript
import { isNode as isPreviewNode } from "../nodes/preview.js";
|
|
import { getPreviewConfig } from "../config.js";
|
|
import { isStressed } from "./fps-monitor.js";
|
|
function ensureState(node) {
|
|
node.__imageops_state ?? (node.__imageops_state = {
|
|
hooked: false,
|
|
canvas: null,
|
|
info: null,
|
|
progressWrap: null,
|
|
progressBar: null,
|
|
mediaWrap: null,
|
|
mediaVideo: null,
|
|
mediaImage: null,
|
|
interactionUntil: 0,
|
|
rafId: null,
|
|
debounceTimer: null,
|
|
lastKey: null,
|
|
lastRenderTick: null,
|
|
renderInFlight: false,
|
|
queuedRenderTick: null,
|
|
isPreview: isPreviewNode(node),
|
|
nativeAnimated: false,
|
|
nativeDirty: false,
|
|
previewZoom: 1,
|
|
previewPanX: 0,
|
|
previewPanY: 0,
|
|
previewPanDrag: null,
|
|
previewNavigationHooked: false,
|
|
previewLastSource: null,
|
|
previewSourceWidth: 0,
|
|
previewSourceHeight: 0,
|
|
cropAspectRatio: null,
|
|
cropGeometry: null,
|
|
cropDrag: null,
|
|
cropResetButton: null,
|
|
cropInteractiveHooked: false,
|
|
drawAspectRatio: null,
|
|
drawGeometry: null,
|
|
drawHover: null,
|
|
drawSizeHintUntil: 0,
|
|
drawStroke: null,
|
|
drawCanvas: null,
|
|
drawBaseCanvas: null,
|
|
drawUndoStack: [],
|
|
drawOverlayKey: null,
|
|
drawBrushButton: null,
|
|
drawEraserButton: null,
|
|
drawClearButton: null,
|
|
drawColorInput: null,
|
|
drawEdgeSelect: null,
|
|
drawSoftnessInput: null,
|
|
drawSoftnessLabel: null,
|
|
drawOpacityInput: null,
|
|
drawOpacityLabel: null,
|
|
drawSizeInput: null,
|
|
drawSizeLabel: null,
|
|
drawWidthInput: null,
|
|
drawHeightInput: null,
|
|
drawLinkButton: null,
|
|
drawBgColorInput: null,
|
|
drawOverlayFormatSelect: null,
|
|
drawPressureSizeInput: null,
|
|
drawPressureOpacityInput: null,
|
|
drawTiltSizeInput: null,
|
|
drawInteractiveHooked: false,
|
|
colorWheelCanvas: null,
|
|
colorHueLabel: null,
|
|
colorSatLabel: null,
|
|
colorSwatch: null,
|
|
colorResetButton: null,
|
|
colorTemperatureInput: null,
|
|
colorTemperatureLabel: null,
|
|
colorTintInput: null,
|
|
colorTintLabel: null,
|
|
colorContrastInput: null,
|
|
colorContrastLabel: null,
|
|
colorSaturationInput: null,
|
|
colorSaturationValueLabel: null,
|
|
colorVibranceInput: null,
|
|
colorVibranceLabel: null,
|
|
colorGammaInput: null,
|
|
colorGammaLabel: null,
|
|
colorShadowWheelCanvas: null,
|
|
colorShadowLabel: null,
|
|
colorMidtoneWheelCanvas: null,
|
|
colorMidtoneLabel: null,
|
|
colorHighlightWheelCanvas: null,
|
|
colorHighlightLabel: null,
|
|
colorBrightnessInput: null,
|
|
colorBrightnessLabel: null,
|
|
colorZoneTabsRow: null,
|
|
colorZoneTabGlobal: null,
|
|
colorZoneTabShadows: null,
|
|
colorZoneTabMidtones: null,
|
|
colorZoneTabHighlights: null,
|
|
colorActiveZone: "global",
|
|
colorInteractiveHooked: false,
|
|
compLayers: [],
|
|
compOutputWidth: 1,
|
|
compOutputHeight: 1,
|
|
compSelectedSlot: null,
|
|
compDrag: null,
|
|
compAddButton: null,
|
|
compResetButton: null,
|
|
compResizeButton: null,
|
|
compCornerPinButton: null,
|
|
compModeSelect: null,
|
|
compOpacityInput: null,
|
|
compOpacityLabel: null,
|
|
compLayerLabel: null,
|
|
compEditMode: "resize",
|
|
compInteractiveHooked: false,
|
|
cornerPinGeometry: null,
|
|
cornerPinDrag: null,
|
|
cornerPinInteractiveHooked: false,
|
|
padOutGeometry: null,
|
|
padOutDrag: null,
|
|
padOutInteractiveHooked: false,
|
|
padOutSourceWidth: 0,
|
|
padOutSourceHeight: 0,
|
|
padOutSourceCanvas: null
|
|
});
|
|
return node.__imageops_state;
|
|
}
|
|
function setInfo(st, text) {
|
|
if (st.info) st.info.textContent = text;
|
|
}
|
|
function schedule(node, fn, delayMs = 120) {
|
|
const st = ensureState(node);
|
|
if (st.debounceTimer) clearTimeout(st.debounceTimer);
|
|
st.debounceTimer = setTimeout(fn, delayMs);
|
|
}
|
|
function stopRAF(st) {
|
|
if (st?.rafId) {
|
|
cancelAnimationFrame(st.rafId);
|
|
st.rafId = null;
|
|
}
|
|
}
|
|
function markPreviewInteraction(node, holdMs = 350) {
|
|
const st = ensureState(node);
|
|
st.interactionUntil = Math.max(st.interactionUntil ?? 0, performance.now() + holdMs);
|
|
}
|
|
function getRenderCanvasSize(st) {
|
|
const cfg = getPreviewConfig();
|
|
const now = performance.now();
|
|
const stressed = isStressed();
|
|
if ((st.interactionUntil ?? 0) > now) {
|
|
return stressed ? Math.max(1, Math.round(cfg.interactionCanvasSize * 0.75)) : cfg.interactionCanvasSize;
|
|
}
|
|
if (st.rafId != null) {
|
|
return stressed ? Math.max(1, Math.round(cfg.playbackCanvasSize * 0.75)) : cfg.playbackCanvasSize;
|
|
}
|
|
return stressed ? Math.max(1, Math.round(cfg.canvasSize * 0.85)) : cfg.canvasSize;
|
|
}
|
|
function hashText(value) {
|
|
let hash = 2166136261;
|
|
for (let index = 0; index < value.length; index++) {
|
|
hash ^= value.charCodeAt(index);
|
|
hash = Math.imul(hash, 16777619);
|
|
}
|
|
return (hash >>> 0).toString(16);
|
|
}
|
|
function serializePreviewValue(value) {
|
|
if (value == null) return "null";
|
|
if (typeof value === "string") {
|
|
return value.length > 120 ? `str:${value.length}:${hashText(value)}` : `str:${value}`;
|
|
}
|
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
if (Array.isArray(value)) {
|
|
return `arr:${value.length}:${value.map((entry) => serializePreviewValue(entry)).join(",")}`;
|
|
}
|
|
try {
|
|
const json = JSON.stringify(value);
|
|
if (!json) return String(value);
|
|
return json.length > 120 ? `json:${json.length}:${hashText(json)}` : `json:${json}`;
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
function buildPreviewRenderKey(node, tick, st, renderCanvasSize) {
|
|
const parts = [
|
|
String(node.id),
|
|
String(node.comfyClass ?? ""),
|
|
`tick:${tick}`,
|
|
`canvas:${renderCanvasSize}`,
|
|
`dirty:${st.nativeDirty ? 1 : 0}`,
|
|
`anim:${st.nativeAnimated ? 1 : 0}`,
|
|
`imageIndex:${typeof node.imageIndex === "number" ? node.imageIndex : -1}`,
|
|
`imgs:${Array.isArray(node.imgs) ? node.imgs.length : 0}`
|
|
];
|
|
for (const widget of node.widgets ?? []) {
|
|
parts.push(`${widget?.name ?? "widget"}=${serializePreviewValue(widget?.value)}`);
|
|
}
|
|
for (let index = 0; index < (node.inputs?.length ?? 0); index++) {
|
|
const input = node.inputs?.[index];
|
|
parts.push(`in${index}:${input?.name ?? ""}:${input?.link ?? "null"}`);
|
|
}
|
|
return parts.join("|");
|
|
}
|
|
export {
|
|
buildPreviewRenderKey,
|
|
ensureState,
|
|
getRenderCanvasSize,
|
|
hashText,
|
|
markPreviewInteraction,
|
|
schedule,
|
|
serializePreviewValue,
|
|
setInfo,
|
|
stopRAF
|
|
};
|