1925 lines
77 KiB
JavaScript
1925 lines
77 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
|
import { api } from "../../../scripts/api.js";
|
|
|
|
let activeNodeId = null;
|
|
|
|
const BASIC_PARAMS = ['brightness', 'contrast', 'hue', 'saturation'];
|
|
const HIGHLIGHT_PARAMS = [...BASIC_PARAMS, 'sharpen'];
|
|
const HIDDEN_WIDGETS = ['brightness', 'contrast', 'hue', 'saturation', 'sharpen'];
|
|
|
|
const DEFAULT_SLIDER_VALUES = {
|
|
brightness: 0,
|
|
contrast: 0,
|
|
sharpen: 0,
|
|
hue: 0,
|
|
saturation: 0,
|
|
vibrance: 0,
|
|
clarity: 0,
|
|
lut_intensity: 100,
|
|
input_black: 0,
|
|
gamma: 1.0,
|
|
input_white: 255,
|
|
exposure: 0,
|
|
offset: 0,
|
|
shadows_cyan_red: 0,
|
|
shadows_magenta_green: 0,
|
|
shadows_yellow_blue: 0,
|
|
midtones_cyan_red: 0,
|
|
midtones_magenta_green: 0,
|
|
midtones_yellow_blue: 0,
|
|
highlights_cyan_red: 0,
|
|
highlights_magenta_green: 0,
|
|
highlights_yellow_blue: 0,
|
|
bw_red: 40,
|
|
bw_yellow: 60,
|
|
bw_green: 40,
|
|
bw_cyan: 60,
|
|
bw_blue: 20,
|
|
bw_magenta: 80,
|
|
red_red: 100,
|
|
red_green: 0,
|
|
red_blue: 0,
|
|
green_red: 0,
|
|
green_green: 100,
|
|
green_blue: 0,
|
|
blue_red: 0,
|
|
blue_green: 0,
|
|
blue_blue: 100,
|
|
sc_cyan: 0,
|
|
sc_magenta: 0,
|
|
sc_yellow: 0,
|
|
sc_black: 0,
|
|
bw_enabled: false,
|
|
};
|
|
|
|
const DEFAULT_ADJUSTMENTS = {
|
|
brightness: 0,
|
|
contrast: 0,
|
|
sharpen: 0,
|
|
hue: 0,
|
|
saturation: 0,
|
|
vibrance: 0,
|
|
clarity: 0,
|
|
lut_path: "",
|
|
lut_intensity: 100,
|
|
levels: { input_black: 0, gamma: 1.0, input_white: 255 },
|
|
exposure: { exposure: 0, offset: 0 },
|
|
color_balance: {
|
|
shadows_cyan_red: 0, shadows_magenta_green: 0, shadows_yellow_blue: 0,
|
|
midtones_cyan_red: 0, midtones_magenta_green: 0, midtones_yellow_blue: 0,
|
|
highlights_cyan_red: 0, highlights_magenta_green: 0, highlights_yellow_blue: 0
|
|
},
|
|
black_white: { bw_red: 40, bw_yellow: 60, bw_green: 40, bw_cyan: 60, bw_blue: 20, bw_magenta: 80 },
|
|
bw_enabled: false,
|
|
channel_mixer: {
|
|
red_red: 100, red_green: 0, red_blue: 0,
|
|
green_red: 0, green_green: 100, green_blue: 0,
|
|
blue_red: 0, blue_green: 0, blue_blue: 100
|
|
},
|
|
selective_color: { color_name: "Reds", sc_cyan: 0, sc_magenta: 0, sc_yellow: 0, sc_black: 0 }
|
|
};
|
|
|
|
function applyBasicAdjustmentsJS(imageData, brightness, contrast, hue, saturation) {
|
|
const data = imageData.data;
|
|
const len = data.length;
|
|
|
|
for (let i = 0; i < len; i += 4) {
|
|
let r = data[i];
|
|
let g = data[i + 1];
|
|
let b = data[i + 2];
|
|
|
|
if (brightness !== 0 || contrast !== 0) {
|
|
const alpha = 1.0 + (contrast / 100.0);
|
|
const beta = brightness;
|
|
r = Math.min(255, Math.max(0, r * alpha + beta));
|
|
g = Math.min(255, Math.max(0, g * alpha + beta));
|
|
b = Math.min(255, Math.max(0, b * alpha + beta));
|
|
}
|
|
|
|
if (hue !== 0 || saturation !== 0) {
|
|
const rNorm = r / 255.0;
|
|
const gNorm = g / 255.0;
|
|
const bNorm = b / 255.0;
|
|
const max = Math.max(rNorm, gNorm, bNorm);
|
|
const min = Math.min(rNorm, gNorm, bNorm);
|
|
const delta = max - min;
|
|
let h = 0, s = 0, v = max;
|
|
|
|
if (delta !== 0) {
|
|
s = delta / max;
|
|
if (max === rNorm) h = ((gNorm - bNorm) / delta) % 6;
|
|
else if (max === gNorm) h = (bNorm - rNorm) / delta + 2;
|
|
else h = (rNorm - gNorm) / delta + 4;
|
|
h *= 60;
|
|
if (h < 0) h += 360;
|
|
}
|
|
|
|
h = Math.floor(h);
|
|
s = Math.floor(s * 255);
|
|
v = Math.floor(v * 255);
|
|
h = Math.floor(h / 2);
|
|
|
|
h = (h + hue) % 180;
|
|
if (h < 0) h += 180;
|
|
s = Math.min(255, Math.max(0, Math.floor(s * (1.0 + saturation / 100.0))));
|
|
|
|
h = h * 2;
|
|
s = s / 255;
|
|
v = v / 255;
|
|
|
|
const c = v * s;
|
|
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
|
const m = v - c;
|
|
|
|
let r2 = 0, g2 = 0, b2 = 0;
|
|
if (h < 60) { r2 = c; g2 = x; b2 = 0; }
|
|
else if (h < 120) { r2 = x; g2 = c; b2 = 0; }
|
|
else if (h < 180) { r2 = 0; g2 = c; b2 = x; }
|
|
else if (h < 240) { r2 = 0; g2 = x; b2 = c; }
|
|
else if (h < 300) { r2 = x; g2 = 0; b2 = c; }
|
|
else { r2 = c; g2 = 0; b2 = x; }
|
|
|
|
r = Math.floor((r2 + m) * 255);
|
|
g = Math.floor((g2 + m) * 255);
|
|
b = Math.floor((b2 + m) * 255);
|
|
}
|
|
|
|
data[i] = Math.min(255, Math.max(0, Math.floor(r)));
|
|
data[i + 1] = Math.min(255, Math.max(0, Math.floor(g)));
|
|
data[i + 2] = Math.min(255, Math.max(0, Math.floor(b)));
|
|
}
|
|
return imageData;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "RaykoStudio.RSImageAdjustments",
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name !== "RS_ImageAdjustments") return;
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function() {
|
|
if (onNodeCreated) onNodeCreated.apply(this, arguments);
|
|
|
|
this.adjustments = JSON.parse(JSON.stringify(DEFAULT_ADJUSTMENTS));
|
|
this.backgroundImage = null;
|
|
this.previewImage = null;
|
|
this.originalImageData = null;
|
|
this._tempCanvas = document.createElement('canvas');
|
|
this._tempCtx = this._tempCanvas.getContext('2d');
|
|
this.isEditing = false;
|
|
this.isLoading = false;
|
|
this.advancedMode = false;
|
|
this.currentSessionTimestamp = null;
|
|
this.pendingEditorData = null;
|
|
this.renderTimeout = null;
|
|
this.currentRenderAbortController = null;
|
|
this._previewTimestamp = null;
|
|
this._previewKey = null;
|
|
this._overlayRenderLoop = null;
|
|
this._ws = null;
|
|
this._wsConnected = false;
|
|
this._wsReconnectTimer = null;
|
|
this._lastHeavyRenderTime = 0;
|
|
this._isHeavyRenderPending = false;
|
|
|
|
// BATCH MODE VARIABLES
|
|
this.isBatchMode = false;
|
|
this._batchParams = null;
|
|
this.btnBatchHover = false;
|
|
this._queueWasRunning = false;
|
|
this._batchResetTimer = null;
|
|
|
|
console.log(`[RS Adjustments] Node ${this.id} created. Registering status listener.`);
|
|
|
|
this.displayWidth = 420;
|
|
this.displayHeight = 420;
|
|
this.canvasPixelSize = 420;
|
|
this.realBackground = { width: 0, height: 0 };
|
|
|
|
this.overlayContainer = null;
|
|
this.overlayCanvas = null;
|
|
this.overlayCtx = null;
|
|
this.overlayCanvasWrapper = null;
|
|
this.sidePanel = null;
|
|
this.overlayInputs = {};
|
|
this.sliderDisplays = {};
|
|
this._basicParamLabels = {};
|
|
this._allSectionsExpanded = false;
|
|
|
|
this.minWidth = 500;
|
|
this.minHeight = 450;
|
|
this.setSize([this.minWidth, this.minHeight]);
|
|
|
|
this.slidersY = 0;
|
|
this.sliderRects = [];
|
|
this.resetBtnRects = [];
|
|
this.sliderHover = [false, false, false, false, false];
|
|
this.resetBtnHover = [false, false, false, false, false];
|
|
this.sliderDragging = -1;
|
|
|
|
this.btnApplyHover = false;
|
|
this.btnCancelHover = false;
|
|
this.btnAdvancedHover = false;
|
|
this.btnResetAllHover = false;
|
|
|
|
this._syncingWidgets = false;
|
|
|
|
BASIC_PARAMS.forEach(n => {
|
|
const w = this.widgets?.find(w => w.name === n);
|
|
if (w) {
|
|
w.hidden = true;
|
|
w.computeSize = () => [0, 0];
|
|
w.y = 0;
|
|
const origCallback = w.callback;
|
|
w.callback = (v) => {
|
|
if (origCallback) origCallback.apply(w, arguments);
|
|
if (!this._syncingWidgets) {
|
|
this.adjustments[n] = v;
|
|
this.previewImage = null;
|
|
this.setDirtyCanvas(true);
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
const serverParams = ['sharpen'];
|
|
serverParams.forEach(n => {
|
|
const w = this.widgets?.find(w => w.name === n);
|
|
if (w) {
|
|
w.hidden = true;
|
|
w.computeSize = () => [0, 0];
|
|
w.y = 0;
|
|
const origCallback = w.callback;
|
|
w.callback = (v) => {
|
|
if (origCallback) origCallback.apply(w, arguments);
|
|
if (!this._syncingWidgets) {
|
|
this.adjustments[n] = v;
|
|
this.setDirtyCanvas(true);
|
|
this._scheduleHeavyRender();
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
const advWidget = this.widgets?.find(w => w.name === "advanced_params");
|
|
if (advWidget) {
|
|
advWidget.hidden = true;
|
|
advWidget.computeSize = () => [0, 0];
|
|
advWidget.y = 0;
|
|
}
|
|
|
|
this._hideNativeWidgets();
|
|
this._buildOverlayUI();
|
|
|
|
this._mouseLeaveHandler = () => {
|
|
if (this.sliderDragging >= 0) {
|
|
this.sliderDragging = -1;
|
|
this.setDirtyCanvas(true);
|
|
}
|
|
};
|
|
|
|
const originalOnMouseLeave = this.onMouseLeave;
|
|
this.onMouseLeave = function() {
|
|
if (originalOnMouseLeave) originalOnMouseLeave.apply(this, arguments);
|
|
this._mouseLeaveHandler();
|
|
};
|
|
|
|
api.addEventListener("rs-adjustments-start", (event) => {
|
|
if (event.detail.id != this.id) return;
|
|
this.pendingEditorData = event.detail;
|
|
this._openDeferredEditor();
|
|
});
|
|
|
|
api.addEventListener("interrupted", () => {
|
|
this._cleanup();
|
|
});
|
|
|
|
// STATUS LISTENER FOR BATCH RESET (moved inside onNodeCreated for correct 'this')
|
|
api.addEventListener("status", (event) => {
|
|
const remaining = event.detail?.exec_info?.queue_remaining;
|
|
console.log(`[RS Adjustments] Status event received. queue_remaining=${remaining}, this.id=${this.id}, isBatchMode=${this.isBatchMode}`);
|
|
|
|
if (remaining > 0) {
|
|
this._queueWasRunning = true;
|
|
if (this._batchResetTimer) {
|
|
clearTimeout(this._batchResetTimer);
|
|
this._batchResetTimer = null;
|
|
}
|
|
} else if (remaining === 0 && this._queueWasRunning) {
|
|
if (!this._batchResetTimer) {
|
|
console.log(`[RS Adjustments] Node ${this.id}: Queue finished. Starting batch reset timer (3s)...`);
|
|
this._batchResetTimer = setTimeout(() => {
|
|
console.log(`[RS Adjustments] Node ${this.id}: Timer fired! Resetting batch. isBatchMode was: ${this.isBatchMode}`);
|
|
this.isBatchMode = false;
|
|
this._batchParams = null;
|
|
this.setDirtyCanvas(true);
|
|
|
|
this._batchResetTimer = null;
|
|
this._queueWasRunning = false;
|
|
console.log(`[RS Adjustments] Node ${this.id}: Batch reset complete. isBatchMode is now: ${this.isBatchMode}`);
|
|
}, 500);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
nodeType.prototype._hideNativeWidgets = function() {
|
|
if (!this.widgets) return;
|
|
HIDDEN_WIDGETS.forEach(param => {
|
|
const widget = this.widgets.find(w => w.name === param);
|
|
if (widget) {
|
|
widget.hidden = true;
|
|
widget.computeSize = () => [0, 0];
|
|
widget.y = 0;
|
|
}
|
|
});
|
|
this.setSize([this.minWidth, this.minHeight]);
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
nodeType.prototype._syncWidgetsFromAdjustments = function() {
|
|
if (!this.widgets) return;
|
|
this._syncingWidgets = true;
|
|
try {
|
|
HIDDEN_WIDGETS.forEach(param => {
|
|
const widget = this.widgets.find(w => w.name === param);
|
|
if (widget && widget.value !== this.adjustments[param]) {
|
|
widget.value = this.adjustments[param];
|
|
}
|
|
});
|
|
} finally {
|
|
this._syncingWidgets = false;
|
|
}
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
nodeType.prototype._syncOverlayUI = function() {
|
|
if (!this.advancedMode) return;
|
|
BASIC_PARAMS.forEach(key => {
|
|
if (this.overlayInputs[key]) {
|
|
this.overlayInputs[key].value = this.adjustments[key];
|
|
}
|
|
if (this.sliderDisplays[key]) {
|
|
this.sliderDisplays[key].textContent = String(this.adjustments[key]);
|
|
}
|
|
});
|
|
|
|
if (this.overlayInputs.sharpen) {
|
|
this.overlayInputs.sharpen.value = this.adjustments.sharpen || 0;
|
|
}
|
|
if (this.sliderDisplays.sharpen) {
|
|
this.sliderDisplays.sharpen.textContent = String(this.adjustments.sharpen || 0);
|
|
}
|
|
};
|
|
|
|
nodeType.prototype._isParamModified = function(key, parentObj = this.adjustments) {
|
|
const def = DEFAULT_SLIDER_VALUES[key];
|
|
if (def === undefined) return false;
|
|
|
|
const val = parentObj[key];
|
|
if (val === undefined) return false;
|
|
|
|
if (typeof def === 'number') {
|
|
return Math.abs(val - def) > 0.001;
|
|
}
|
|
return val !== def;
|
|
};
|
|
|
|
nodeType.prototype._isSectionModified = function(sectionName) {
|
|
const checks = {
|
|
lut: () => (this.adjustments.lut_path && this.adjustments.lut_path !== "") || this._isParamModified('lut_intensity'),
|
|
advanced_tools: () => this._isParamModified('vibrance') || this._isParamModified('clarity'),
|
|
levels: () => this._isParamModified('input_black', this.adjustments.levels) ||
|
|
this._isParamModified('gamma', this.adjustments.levels) ||
|
|
this._isParamModified('input_white', this.adjustments.levels),
|
|
exposure: () => this._isParamModified('exposure', this.adjustments.exposure) ||
|
|
this._isParamModified('offset', this.adjustments.exposure),
|
|
color_balance: () => [
|
|
'shadows_cyan_red', 'shadows_magenta_green', 'shadows_yellow_blue',
|
|
'midtones_cyan_red', 'midtones_magenta_green', 'midtones_yellow_blue',
|
|
'highlights_cyan_red', 'highlights_magenta_green', 'highlights_yellow_blue'
|
|
].some(k => this._isParamModified(k, this.adjustments.color_balance)),
|
|
black_white: () => ['bw_red', 'bw_yellow', 'bw_green', 'bw_cyan', 'bw_blue', 'bw_magenta'].some(k => this._isParamModified(k, this.adjustments.black_white)),
|
|
channel_mixer: () => [
|
|
'red_red', 'red_green', 'red_blue',
|
|
'green_red', 'green_green', 'green_blue',
|
|
'blue_red', 'blue_green', 'blue_blue'
|
|
].some(k => this._isParamModified(k, this.adjustments.channel_mixer)),
|
|
selective_color: () => ['sc_cyan', 'sc_magenta', 'sc_yellow', 'sc_black'].some(k => this._isParamModified(k, this.adjustments.selective_color))
|
|
};
|
|
return checks[sectionName] ? checks[sectionName]() : false;
|
|
};
|
|
|
|
nodeType.prototype._updateSectionHeaders = function() {
|
|
if (!this._sectionRegistry) return;
|
|
this._sectionRegistry.forEach(entry => {
|
|
const modified = this._isSectionModified(entry.name);
|
|
if (entry.label) {
|
|
entry.label.style.color = modified ? '#00FF00' : '#666';
|
|
}
|
|
});
|
|
};
|
|
|
|
nodeType.prototype._updateBasicParamLabels = function() {
|
|
HIGHLIGHT_PARAMS.forEach(key => {
|
|
const lbl = this._basicParamLabels[key];
|
|
if (lbl) {
|
|
const isModified = this._isParamModified(key);
|
|
lbl.style.color = isModified ? '#00FF00' : '#aaa';
|
|
}
|
|
});
|
|
};
|
|
|
|
nodeType.prototype._getDefaultValueForKey = function(key) {
|
|
if (DEFAULT_SLIDER_VALUES.hasOwnProperty(key)) {
|
|
return DEFAULT_SLIDER_VALUES[key];
|
|
}
|
|
const nestedKeys = [
|
|
'levels', 'exposure', 'color_balance', 'black_white', 'channel_mixer', 'selective_color'
|
|
];
|
|
for (let section of nestedKeys) {
|
|
if (DEFAULT_ADJUSTMENTS[section] && DEFAULT_ADJUSTMENTS[section].hasOwnProperty(key)) {
|
|
return DEFAULT_ADJUSTMENTS[section][key];
|
|
}
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
nodeType.prototype._resetAllParameters = function() {
|
|
const resetRecursive = (target, source) => {
|
|
for (let key in source) {
|
|
if (source.hasOwnProperty(key)) {
|
|
if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
if (!target[key] || typeof target[key] !== 'object') target[key] = {};
|
|
resetRecursive(target[key], source[key]);
|
|
} else {
|
|
target[key] = source[key];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
resetRecursive(this.adjustments, DEFAULT_ADJUSTMENTS);
|
|
|
|
Object.keys(this.overlayInputs).forEach(key => {
|
|
const slider = this.overlayInputs[key];
|
|
if (!slider) return;
|
|
const defaultVal = this._getDefaultValueForKey(key);
|
|
slider.value = defaultVal;
|
|
if (this.sliderDisplays[key]) {
|
|
const isFloat = Number.isFinite(defaultVal) && !Number.isInteger(defaultVal);
|
|
this.sliderDisplays[key].textContent = isFloat ? defaultVal.toFixed(2) : String(Math.round(defaultVal));
|
|
}
|
|
});
|
|
|
|
if (this._bwToggle) {
|
|
this._bwToggle.checked = this.adjustments.bw_enabled || false;
|
|
}
|
|
|
|
const colorSelect = this.overlayContainer?.querySelector('#selective-color-select');
|
|
if (colorSelect) {
|
|
colorSelect.value = this.adjustments.selective_color.color_name || 'Reds';
|
|
}
|
|
|
|
const lutFileInput = this.overlayContainer?.querySelector('input[type="file"][accept=".cube"]');
|
|
const lutFilenameDisplay = this.overlayContainer?.querySelector('.lut-filename-display');
|
|
if (lutFileInput) lutFileInput.value = '';
|
|
if (lutFilenameDisplay) {
|
|
lutFilenameDisplay.textContent = 'File not selected';
|
|
lutFilenameDisplay.style.color = '#888';
|
|
}
|
|
const lutIntensitySlider = this.overlayInputs['lut_intensity'];
|
|
if (lutIntensitySlider) {
|
|
lutIntensitySlider.value = this.adjustments.lut_intensity;
|
|
if (this.sliderDisplays['lut_intensity']) {
|
|
this.sliderDisplays['lut_intensity'].textContent = String(this.adjustments.lut_intensity);
|
|
}
|
|
}
|
|
|
|
this._updateSectionHeaders();
|
|
this._updateBasicParamLabels();
|
|
this.previewImage = null;
|
|
this.setDirtyCanvas(true);
|
|
this._scheduleHeavyRender();
|
|
this._syncWidgetsFromAdjustments();
|
|
};
|
|
|
|
nodeType.prototype._buildOverlayUI = function() {
|
|
this.overlayContainer = document.createElement('div');
|
|
this.overlayContainer.style.cssText = 'position:fixed;top:60px;left:0;right:0;bottom:0;background:rgba(10,10,10,0.96);z-index:999;display:none;flex-direction:row;align-items:stretch;font-family:system-ui,-apple-system,sans-serif;';
|
|
|
|
this.overlayCanvasWrapper = document.createElement('div');
|
|
this.overlayCanvasWrapper.style.cssText = 'flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative;';
|
|
|
|
this.overlayCanvas = document.createElement('canvas');
|
|
this.overlayCanvas.style.cssText = 'box-shadow:0 8px 32px rgba(0,0,0,0.7);max-width:98%;max-height:98%;border-radius:8px;';
|
|
this.overlayCanvasWrapper.appendChild(this.overlayCanvas);
|
|
this.overlayCtx = this.overlayCanvas.getContext('2d');
|
|
|
|
this.sidePanel = document.createElement('div');
|
|
this.sidePanel.style.cssText = 'width:320px;background:#151515;border-left:1px solid #333;padding:12px;display:flex;flex-direction:column;gap:4px;box-sizing:border-box;overflow-y:auto;';
|
|
|
|
const buttonContainer = document.createElement('div');
|
|
buttonContainer.style.cssText = 'position:sticky;top:0;background:#151515;z-index:10;padding:0 0 8px 0;border-bottom:1px solid #333;margin-bottom:8px;';
|
|
this.sidePanel.appendChild(buttonContainer);
|
|
|
|
const makeBtn = (txt, col, onClick) => {
|
|
const b = document.createElement('button');
|
|
b.textContent = txt;
|
|
b.style.cssText = `width:100%;padding:10px;background:#222;color:${col};border:1px solid ${col};border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;margin-top:4px;transition:0.15s;`;
|
|
b.onmouseenter = () => { b.style.background = '#2a2a2a'; b.style.transform = 'translateY(-1px)'; };
|
|
b.onmouseleave = () => { b.style.background = '#222'; b.style.transform = 'none'; };
|
|
b.onclick = (e) => { e.stopPropagation(); onClick(); };
|
|
return b;
|
|
};
|
|
|
|
const btnNormalMode = makeBtn("🔵 NORMAL MODE", "#2196F3", () => { this._toggleAdvancedMode(); });
|
|
buttonContainer.appendChild(btnNormalMode);
|
|
const btnApply = makeBtn("✔️ APPLY", "#4CAF50", () => { this._sendAdjustments(); this._toggleAdvancedMode(); });
|
|
buttonContainer.appendChild(btnApply);
|
|
const btnCancel = makeBtn("❌ CANCEL", "#dc3545", () => { this._cancelEditing(); this._toggleAdvancedMode(); });
|
|
buttonContainer.appendChild(btnCancel);
|
|
|
|
const bottomRow = document.createElement('div');
|
|
bottomRow.style.cssText = 'display:flex;gap:8px;width:100%;';
|
|
|
|
const btnResetAll = makeBtn("🔄 RESET ALL", "#FF9800", () => { this._resetAllParameters(); });
|
|
btnResetAll.style.flex = '1';
|
|
bottomRow.appendChild(btnResetAll);
|
|
|
|
this._expandCollapseBtn = makeBtn("▶ EXPAND ALL", "#888", () => {
|
|
this._allSectionsExpanded = !this._allSectionsExpanded;
|
|
const display = this._allSectionsExpanded ? 'block' : 'none';
|
|
const icon = this._allSectionsExpanded ? '▼' : '▶';
|
|
const text = this._allSectionsExpanded ? '▼ COLLAPSE ALL' : '▶ EXPAND ALL';
|
|
|
|
this._sectionRegistry.forEach(entry => {
|
|
if (entry.content) entry.content.style.display = display;
|
|
if (entry.chevron) entry.chevron.textContent = icon;
|
|
});
|
|
this._expandCollapseBtn.textContent = text;
|
|
});
|
|
this._expandCollapseBtn.style.flex = '1';
|
|
bottomRow.appendChild(this._expandCollapseBtn);
|
|
|
|
buttonContainer.appendChild(bottomRow);
|
|
|
|
const makeSlider = (label, key, min, max, step, isFloat = false, parentObj = this.adjustments) => {
|
|
const container = document.createElement('div');
|
|
container.style.cssText = 'display:flex;flex-direction:column;gap:4px;';
|
|
const lbl = document.createElement('label');
|
|
lbl.textContent = label;
|
|
lbl.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;';
|
|
container.appendChild(lbl);
|
|
|
|
if (HIGHLIGHT_PARAMS.includes(key)) {
|
|
this._basicParamLabels[key] = lbl;
|
|
}
|
|
|
|
const row = document.createElement('div');
|
|
row.style.cssText = 'display:flex;align-items:center;gap:8px;';
|
|
const slider = document.createElement('input');
|
|
slider.type = 'range';
|
|
slider.min = min;
|
|
slider.max = max;
|
|
slider.step = step;
|
|
slider.value = parentObj[key];
|
|
slider.style.cssText = 'flex:1;height:4px;background:#252525;border-radius:2px;outline:none;cursor:pointer;-webkit-appearance:none;';
|
|
const valueDisplay = document.createElement('div');
|
|
valueDisplay.textContent = isFloat ? parseFloat(parentObj[key]).toFixed(2) : String(parseInt(parentObj[key]));
|
|
valueDisplay.style.cssText = 'min-width:50px;text-align:center;background:#252525;color:#4CAF50;border:1px solid #444;border-radius:4px;padding:4px 8px;font-size:12px;cursor:pointer;font-weight:600;transition:0.15s;';
|
|
|
|
valueDisplay.onclick = (e) => {
|
|
e.stopPropagation();
|
|
const popup = document.createElement('div');
|
|
popup.style.cssText = 'position:fixed;z-index:10004;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:8px 12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:8px;';
|
|
|
|
const input = document.createElement('input');
|
|
input.type = 'number';
|
|
input.value = parentObj[key];
|
|
input.min = min;
|
|
input.max = max;
|
|
input.step = step;
|
|
input.style.cssText = 'width:100px;background:#222;color:#fff;border:1px solid #444;border-radius:4px;padding:6px 10px;font-size:12px;outline:none;';
|
|
|
|
const saveBtn = document.createElement('button');
|
|
saveBtn.textContent = 'OK';
|
|
saveBtn.style.cssText = 'background:#4CAF50;color:#fff;border:none;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer;';
|
|
|
|
const doSave = () => {
|
|
let num = isFloat ? parseFloat(input.value) : parseInt(input.value);
|
|
if (isNaN(num)) num = parentObj[key];
|
|
num = Math.max(min, Math.min(max, num));
|
|
parentObj[key] = num;
|
|
slider.value = num;
|
|
valueDisplay.textContent = isFloat ? num.toFixed(2) : String(num);
|
|
this._updateSectionHeaders();
|
|
this._updateBasicParamLabels();
|
|
|
|
const widget = this.widgets?.find(w => w.name === key);
|
|
if (widget) widget.value = num;
|
|
|
|
this.setDirtyCanvas(true);
|
|
|
|
if (BASIC_PARAMS.includes(key)) {
|
|
this.previewImage = null;
|
|
} else {
|
|
this._scheduleHeavyRender();
|
|
}
|
|
popup.remove();
|
|
};
|
|
|
|
saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); };
|
|
input.onkeydown = (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); doSave(); } };
|
|
|
|
popup.appendChild(input);
|
|
popup.appendChild(saveBtn);
|
|
document.body.appendChild(popup);
|
|
|
|
const popupWidth = popup.offsetWidth || 180;
|
|
const popupHeight = popup.offsetHeight || 40;
|
|
const rect = valueDisplay.getBoundingClientRect();
|
|
let leftPos = rect.right + 8;
|
|
let topPos = rect.top + (rect.height - popupHeight) / 2;
|
|
|
|
if (leftPos + popupWidth > window.innerWidth - 10) {
|
|
leftPos = rect.left - popupWidth - 8;
|
|
}
|
|
if (topPos < 10) topPos = 10;
|
|
if (topPos + popupHeight > window.innerHeight - 10) {
|
|
topPos = window.innerHeight - popupHeight - 10;
|
|
}
|
|
|
|
popup.style.left = leftPos + 'px';
|
|
popup.style.top = topPos + 'px';
|
|
|
|
setTimeout(() => { input.focus(); input.select(); }, 50);
|
|
|
|
setTimeout(() => {
|
|
const closeHandler = (ev) => {
|
|
if (!popup.contains(ev.target)) {
|
|
popup.remove();
|
|
document.removeEventListener('mousedown', closeHandler);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', closeHandler);
|
|
}, 100);
|
|
};
|
|
|
|
const resetBtn = document.createElement('button');
|
|
resetBtn.textContent = '🔄';
|
|
resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;';
|
|
resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; };
|
|
resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; };
|
|
resetBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
const defaultVal = DEFAULT_SLIDER_VALUES[key] !== undefined ? DEFAULT_SLIDER_VALUES[key] : (isFloat ? 0.0 : 0);
|
|
parentObj[key] = defaultVal;
|
|
slider.value = defaultVal;
|
|
valueDisplay.textContent = isFloat ? parseFloat(defaultVal).toFixed(2) : String(parseInt(defaultVal));
|
|
this._updateSectionHeaders();
|
|
this._updateBasicParamLabels();
|
|
|
|
this.setDirtyCanvas(true);
|
|
if (BASIC_PARAMS.includes(key)) {
|
|
this.previewImage = null;
|
|
}
|
|
this._scheduleHeavyRender();
|
|
};
|
|
|
|
slider.oninput = () => {
|
|
const val = isFloat ? parseFloat(slider.value) : parseInt(slider.value);
|
|
parentObj[key] = val;
|
|
valueDisplay.textContent = isFloat ? val.toFixed(2) : String(val);
|
|
this._updateSectionHeaders();
|
|
this._updateBasicParamLabels();
|
|
|
|
this.setDirtyCanvas(true);
|
|
|
|
if (BASIC_PARAMS.includes(key)) {
|
|
this.previewImage = null;
|
|
const widget = this.widgets?.find(w => w.name === key);
|
|
if (widget) widget.value = val;
|
|
} else {
|
|
this._scheduleHeavyRender();
|
|
}
|
|
};
|
|
this.overlayInputs[key] = slider;
|
|
this.sliderDisplays[key] = valueDisplay;
|
|
row.appendChild(slider);
|
|
row.appendChild(valueDisplay);
|
|
row.appendChild(resetBtn);
|
|
container.appendChild(row);
|
|
return container;
|
|
};
|
|
|
|
const makeSection = (title, sectionName, contentBuilder) => {
|
|
const section = document.createElement('div');
|
|
section.style.cssText = 'margin-top:8px;';
|
|
const header = document.createElement('div');
|
|
header.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 0;cursor:pointer;user-select:none;';
|
|
const chevron = document.createElement('span');
|
|
chevron.textContent = '▶';
|
|
chevron.style.cssText = 'color:#666;font-size:12px;transition:transform 200ms;flex-shrink:0;';
|
|
const lbl = document.createElement('label');
|
|
lbl.textContent = title;
|
|
lbl.style.cssText = `flex:1;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;transition:color 200ms;color:#666;`;
|
|
header.appendChild(chevron);
|
|
header.appendChild(lbl);
|
|
const content = document.createElement('div');
|
|
content.style.cssText = 'display:none;padding-left:8px;';
|
|
header.addEventListener('click', (e) => {
|
|
const isOpen = content.style.display !== 'none';
|
|
content.style.display = isOpen ? 'none' : 'block';
|
|
chevron.textContent = isOpen ? '▶' : '▼';
|
|
});
|
|
section.appendChild(header);
|
|
section.appendChild(content);
|
|
contentBuilder(content);
|
|
|
|
if (!this._sectionRegistry) this._sectionRegistry = [];
|
|
this._sectionRegistry.push({ name: sectionName, label: lbl, content: content, chevron: chevron });
|
|
|
|
return section;
|
|
};
|
|
|
|
const div = () => { const d = document.createElement('div'); d.style.cssText = 'height:1px;background:#333;margin:4px 0;'; return d; };
|
|
|
|
const truncateFilename = (filename) => {
|
|
if (!filename) return 'File not selected';
|
|
if (filename.length <= 30) return filename;
|
|
return filename.substring(0, 27) + '...';
|
|
};
|
|
|
|
this.sidePanel.appendChild(div());
|
|
this.sidePanel.appendChild(makeSlider("BRIGHTNESS", "brightness", -100, 100, 1));
|
|
this.sidePanel.appendChild(makeSlider("CONTRAST", "contrast", -100, 100, 1));
|
|
this.sidePanel.appendChild(makeSlider("SHARPEN", "sharpen", 0, 200, 1));
|
|
this.sidePanel.appendChild(makeSlider("HUE", "hue", -180, 180, 1));
|
|
this.sidePanel.appendChild(makeSlider("SATURATION", "saturation", -100, 100, 1));
|
|
this.sidePanel.appendChild(div());
|
|
|
|
const lutSection = makeSection("COLOR LOOKUP (LUT)", "lut", (content) => {
|
|
const fileRow = document.createElement('div');
|
|
fileRow.style.cssText = 'display:flex;align-items:center;gap:5px;';
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.accept = '.cube';
|
|
fileInput.style.display = 'none';
|
|
const filenameDisplay = document.createElement('div');
|
|
filenameDisplay.className = 'lut-filename-display';
|
|
filenameDisplay.textContent = 'File not selected';
|
|
filenameDisplay.style.cssText = 'flex:1;padding:8px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-height:36px;display:flex;align-items:center;cursor:pointer;';
|
|
const browseBtn = document.createElement('button');
|
|
browseBtn.textContent = 'Browse';
|
|
browseBtn.style.cssText = 'padding:8px 12px;background:#252525;color:#aaa;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600;transition:0.15s;flex-shrink:0;';
|
|
browseBtn.onmouseenter = () => { browseBtn.style.background = '#2a2a2a'; browseBtn.style.color = '#fff'; browseBtn.style.borderColor = '#4CAF50'; };
|
|
browseBtn.onmouseleave = () => { browseBtn.style.background = '#252525'; browseBtn.style.color = '#aaa'; browseBtn.style.borderColor = '#444'; };
|
|
browseBtn.onclick = (e) => { e.stopPropagation(); fileInput.click(); };
|
|
const resetBtn = document.createElement('button');
|
|
resetBtn.textContent = '';
|
|
resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;';
|
|
resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; };
|
|
resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; };
|
|
resetBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
fileInput.value = '';
|
|
filenameDisplay.textContent = 'File not selected';
|
|
filenameDisplay.style.color = '#888';
|
|
this.adjustments.lut_path = '';
|
|
this.adjustments.lut_intensity = 100;
|
|
|
|
this.previewImage = null;
|
|
this.setDirtyCanvas(true);
|
|
this._updateSectionHeaders();
|
|
this._scheduleHeavyRender();
|
|
};
|
|
fileInput.onchange = async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
filenameDisplay.textContent = truncateFilename(file.name);
|
|
filenameDisplay.style.color = '#eee';
|
|
const reader = new FileReader();
|
|
reader.onload = async (ev) => {
|
|
const blob = new Blob([ev.target.result], { type: 'text/plain' });
|
|
const formData = new FormData();
|
|
formData.append('image', blob, file.name);
|
|
formData.append('subfolder', 'rs_adjustments');
|
|
formData.append('type', 'temp');
|
|
try {
|
|
const resp = await api.fetchApi('/upload/image', { method: 'POST', body: formData });
|
|
const data = await resp.json();
|
|
this.adjustments.lut_path = data.name;
|
|
this._updateSectionHeaders();
|
|
|
|
this.previewImage = null;
|
|
this.setDirtyCanvas(true);
|
|
this._scheduleHeavyRender();
|
|
} catch (err) {
|
|
filenameDisplay.textContent = 'File not selected';
|
|
filenameDisplay.style.color = '#888';
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
};
|
|
filenameDisplay.onclick = () => fileInput.click();
|
|
fileRow.appendChild(filenameDisplay);
|
|
fileRow.appendChild(browseBtn);
|
|
fileRow.appendChild(resetBtn);
|
|
fileRow.appendChild(fileInput);
|
|
content.appendChild(fileRow);
|
|
content.appendChild(div());
|
|
content.appendChild(makeSlider("INTENSITY", "lut_intensity", 0, 100, 1));
|
|
});
|
|
this.sidePanel.appendChild(lutSection);
|
|
|
|
const advancedToolsSection = makeSection("ADVANCED TOOLS", "advanced_tools", (content) => {
|
|
content.appendChild(makeSlider("VIBRANCE", "vibrance", -100, 100, 1));
|
|
content.appendChild(makeSlider("CLARITY", "clarity", -100, 100, 1));
|
|
});
|
|
this.sidePanel.appendChild(advancedToolsSection);
|
|
|
|
const levelsSection = makeSection("LEVELS", "levels", (content) => {
|
|
content.appendChild(makeSlider("INPUT BLACK", "input_black", 0, 255, 1, false, this.adjustments.levels));
|
|
content.appendChild(makeSlider("GAMMA", "gamma", 0.1, 3.0, 0.01, true, this.adjustments.levels));
|
|
content.appendChild(makeSlider("INPUT WHITE", "input_white", 0, 255, 1, false, this.adjustments.levels));
|
|
});
|
|
this.sidePanel.appendChild(levelsSection);
|
|
|
|
const exposureSection = makeSection("EXPOSURE", "exposure", (content) => {
|
|
content.appendChild(makeSlider("EXPOSURE", "exposure", -100, 100, 1, false, this.adjustments.exposure));
|
|
content.appendChild(makeSlider("OFFSET", "offset", -100, 100, 1, false, this.adjustments.exposure));
|
|
});
|
|
this.sidePanel.appendChild(exposureSection);
|
|
|
|
const colorBalanceSection = makeSection("COLOR BALANCE", "color_balance", (content) => {
|
|
const shadowsLabel = document.createElement('div');
|
|
shadowsLabel.textContent = 'SHADOWS';
|
|
shadowsLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;';
|
|
content.appendChild(shadowsLabel);
|
|
content.appendChild(makeSlider("CYAN / RED", "shadows_cyan_red", -100, 100, 1, false, this.adjustments.color_balance));
|
|
content.appendChild(makeSlider("MAGENTA / GREEN", "shadows_magenta_green", -100, 100, 1, false, this.adjustments.color_balance));
|
|
content.appendChild(makeSlider("YELLOW / BLUE", "shadows_yellow_blue", -100, 100, 1, false, this.adjustments.color_balance));
|
|
const midtonesLabel = document.createElement('div');
|
|
midtonesLabel.textContent = 'MIDTONES';
|
|
midtonesLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;';
|
|
content.appendChild(midtonesLabel);
|
|
content.appendChild(makeSlider("CYAN / RED", "midtones_cyan_red", -100, 100, 1, false, this.adjustments.color_balance));
|
|
content.appendChild(makeSlider("MAGENTA / GREEN", "midtones_magenta_green", -100, 100, 1, false, this.adjustments.color_balance));
|
|
content.appendChild(makeSlider("YELLOW / BLUE", "midtones_yellow_blue", -100, 100, 1, false, this.adjustments.color_balance));
|
|
const highlightsLabel = document.createElement('div');
|
|
highlightsLabel.textContent = 'HIGHLIGHTS';
|
|
highlightsLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;';
|
|
content.appendChild(highlightsLabel);
|
|
content.appendChild(makeSlider("CYAN / RED", "highlights_cyan_red", -100, 100, 1, false, this.adjustments.color_balance));
|
|
content.appendChild(makeSlider("MAGENTA / GREEN", "highlights_magenta_green", -100, 100, 1, false, this.adjustments.color_balance));
|
|
content.appendChild(makeSlider("YELLOW / BLUE", "highlights_yellow_blue", -100, 100, 1, false, this.adjustments.color_balance));
|
|
});
|
|
this.sidePanel.appendChild(colorBalanceSection);
|
|
|
|
const bwSection = makeSection("BLACK & WHITE", "black_white", (content) => {
|
|
const toggleRow = document.createElement('div');
|
|
toggleRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin:4px 0 8px 0;';
|
|
|
|
const toggleLabel = document.createElement('label');
|
|
toggleLabel.textContent = 'Enable B&W';
|
|
toggleLabel.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;';
|
|
|
|
const toggleInput = document.createElement('input');
|
|
toggleInput.type = 'checkbox';
|
|
toggleInput.checked = this.adjustments.bw_enabled || false;
|
|
toggleInput.style.cssText = 'width:18px;height:18px;accent-color:#4CAF50;cursor:pointer;';
|
|
|
|
this._bwToggle = toggleInput;
|
|
|
|
toggleInput.onchange = () => {
|
|
this.adjustments.bw_enabled = toggleInput.checked;
|
|
this.previewImage = null;
|
|
this.setDirtyCanvas(true);
|
|
this._scheduleHeavyRender();
|
|
};
|
|
|
|
toggleRow.appendChild(toggleLabel);
|
|
toggleRow.appendChild(toggleInput);
|
|
content.appendChild(toggleRow);
|
|
|
|
content.appendChild(makeSlider("RED", "bw_red", 0, 200, 1, false, this.adjustments.black_white));
|
|
content.appendChild(makeSlider("YELLOW", "bw_yellow", 0, 200, 1, false, this.adjustments.black_white));
|
|
content.appendChild(makeSlider("GREEN", "bw_green", 0, 200, 1, false, this.adjustments.black_white));
|
|
content.appendChild(makeSlider("CYAN", "bw_cyan", 0, 200, 1, false, this.adjustments.black_white));
|
|
content.appendChild(makeSlider("BLUE", "bw_blue", 0, 200, 1, false, this.adjustments.black_white));
|
|
content.appendChild(makeSlider("MAGENTA", "bw_magenta", 0, 200, 1, false, this.adjustments.black_white));
|
|
});
|
|
this.sidePanel.appendChild(bwSection);
|
|
|
|
const channelMixerSection = makeSection("CHANNEL MIXER", "channel_mixer", (content) => {
|
|
const redLabel = document.createElement('div');
|
|
redLabel.textContent = 'OUTPUT RED';
|
|
redLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;';
|
|
content.appendChild(redLabel);
|
|
content.appendChild(makeSlider("RED", "red_red", -200, 200, 1, false, this.adjustments.channel_mixer));
|
|
content.appendChild(makeSlider("GREEN", "red_green", -200, 200, 1, false, this.adjustments.channel_mixer));
|
|
content.appendChild(makeSlider("BLUE", "red_blue", -200, 200, 1, false, this.adjustments.channel_mixer));
|
|
const greenLabel = document.createElement('div');
|
|
greenLabel.textContent = 'OUTPUT GREEN';
|
|
greenLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;';
|
|
content.appendChild(greenLabel);
|
|
content.appendChild(makeSlider("RED", "green_red", -200, 200, 1, false, this.adjustments.channel_mixer));
|
|
content.appendChild(makeSlider("GREEN", "green_green", -200, 200, 1, false, this.adjustments.channel_mixer));
|
|
content.appendChild(makeSlider("BLUE", "green_blue", -200, 200, 1, false, this.adjustments.channel_mixer));
|
|
const blueLabel = document.createElement('div');
|
|
blueLabel.textContent = 'OUTPUT BLUE';
|
|
blueLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;';
|
|
content.appendChild(blueLabel);
|
|
content.appendChild(makeSlider("RED", "blue_red", -200, 200, 1, false, this.adjustments.channel_mixer));
|
|
content.appendChild(makeSlider("GREEN", "blue_green", -200, 200, 1, false, this.adjustments.channel_mixer));
|
|
content.appendChild(makeSlider("BLUE", "blue_blue", -200, 200, 1, false, this.adjustments.channel_mixer));
|
|
});
|
|
this.sidePanel.appendChild(channelMixerSection);
|
|
|
|
const selectiveColorSection = makeSection("SELECTIVE COLOR", "selective_color", (content) => {
|
|
const colorSelect = document.createElement('select');
|
|
colorSelect.id = 'selective-color-select';
|
|
colorSelect.style.cssText = 'width:100%;background:#252525;color:#eee;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px;outline:none;margin-bottom:8px;';
|
|
['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas', 'Whites', 'Neutrals', 'Blacks'].forEach(c => {
|
|
const opt = document.createElement('option');
|
|
opt.value = c;
|
|
opt.textContent = c;
|
|
if (c === this.adjustments.selective_color.color_name) opt.selected = true;
|
|
colorSelect.appendChild(opt);
|
|
});
|
|
colorSelect.onchange = () => {
|
|
this.adjustments.selective_color.color_name = colorSelect.value;
|
|
this.previewImage = null;
|
|
this.setDirtyCanvas(true);
|
|
this._scheduleHeavyRender();
|
|
};
|
|
content.appendChild(colorSelect);
|
|
content.appendChild(makeSlider("CYAN", "sc_cyan", -100, 100, 1, false, this.adjustments.selective_color));
|
|
content.appendChild(makeSlider("MAGENTA", "sc_magenta", -100, 100, 1, false, this.adjustments.selective_color));
|
|
content.appendChild(makeSlider("YELLOW", "sc_yellow", -100, 100, 1, false, this.adjustments.selective_color));
|
|
content.appendChild(makeSlider("BLACK", "sc_black", -100, 100, 1, false, this.adjustments.selective_color));
|
|
});
|
|
this.sidePanel.appendChild(selectiveColorSection);
|
|
|
|
this.overlayContainer.appendChild(this.overlayCanvasWrapper);
|
|
this.overlayContainer.appendChild(this.sidePanel);
|
|
document.body.appendChild(this.overlayContainer);
|
|
};
|
|
|
|
nodeType.prototype._scheduleBasicRender = function() {
|
|
this._syncWidgetsFromAdjustments();
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
nodeType.prototype._scheduleHeavyRender = function() {
|
|
if (this._isHeavyRenderPending) {
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
if (now - this._lastHeavyRenderTime < 50) {
|
|
return;
|
|
}
|
|
|
|
this._lastHeavyRenderTime = now;
|
|
this._syncWidgetsFromAdjustments();
|
|
|
|
if (this._wsConnected) {
|
|
this._sendPreviewViaWebSocket();
|
|
} else {
|
|
this._fetchPreviewFromServer();
|
|
}
|
|
};
|
|
|
|
nodeType.prototype._fetchPreviewFromServer = async function() {
|
|
if (!this.isEditing || !this.backgroundImage) return;
|
|
if (this.currentRenderAbortController) {
|
|
this.currentRenderAbortController.abort();
|
|
}
|
|
this.currentRenderAbortController = new AbortController();
|
|
const signal = this.currentRenderAbortController.signal;
|
|
|
|
const payload = {
|
|
node_id: String(this.id),
|
|
image_file: this.pendingEditorData.bg_file,
|
|
adjustments: this.adjustments
|
|
};
|
|
|
|
try {
|
|
const response = await api.fetchApi('/rayko/rs_adjustments/preview', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
signal: signal
|
|
});
|
|
const data = await response.json();
|
|
if (data.preview_file) {
|
|
const img = new Image();
|
|
img.crossOrigin = "Anonymous";
|
|
const ts = data.timestamp;
|
|
this._previewTimestamp = ts;
|
|
img.onload = () => {
|
|
if (this._previewTimestamp === ts) {
|
|
this.previewImage = img;
|
|
this.setDirtyCanvas(true);
|
|
}
|
|
};
|
|
img.onerror = () => {};
|
|
img.src = `/view?filename=${data.preview_file}&type=temp&t=${ts}`;
|
|
}
|
|
} catch (e) {
|
|
if (e.name === 'AbortError') return;
|
|
}
|
|
};
|
|
|
|
nodeType.prototype._initWebSocket = function() {
|
|
if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
|
return;
|
|
}
|
|
|
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${protocol}//${location.host}/rayko/rs_adjustments/ws`;
|
|
|
|
this._ws = new WebSocket(wsUrl);
|
|
this._ws.binaryType = 'blob';
|
|
|
|
this._ws.onopen = () => {
|
|
console.log('[RS Adjustments] WebSocket connected');
|
|
this._wsConnected = true;
|
|
if (this._wsReconnectTimer) {
|
|
clearTimeout(this._wsReconnectTimer);
|
|
this._wsReconnectTimer = null;
|
|
}
|
|
};
|
|
|
|
this._ws.onmessage = (event) => {
|
|
if (event.data instanceof Blob) {
|
|
const url = URL.createObjectURL(event.data);
|
|
const img = new Image();
|
|
img.crossOrigin = "Anonymous";
|
|
img.onload = () => {
|
|
this.previewImage = img;
|
|
this.setDirtyCanvas(true);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
img.onerror = () => {
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
img.src = url;
|
|
} else if (event.data instanceof ArrayBuffer) {
|
|
const blob = new Blob([event.data], { type: 'image/jpeg' });
|
|
const url = URL.createObjectURL(blob);
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
this.previewImage = img;
|
|
this.setDirtyCanvas(true);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
img.src = url;
|
|
}
|
|
};
|
|
|
|
this._ws.onclose = () => {
|
|
console.log('[RS Adjustments] WebSocket disconnected');
|
|
this._wsConnected = false;
|
|
if (!this._wsReconnectTimer) {
|
|
this._wsReconnectTimer = setTimeout(() => {
|
|
this._initWebSocket();
|
|
}, 2000);
|
|
}
|
|
};
|
|
|
|
this._ws.onerror = (error) => {
|
|
console.error('[RS Adjustments] WebSocket error:', error);
|
|
this._ws.close();
|
|
};
|
|
};
|
|
|
|
nodeType.prototype._sendPreviewViaWebSocket = function() {
|
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
|
|
this._fetchPreviewFromServer();
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
node_id: String(this.id),
|
|
image_file: this.pendingEditorData.bg_file,
|
|
adjustments: this.adjustments
|
|
};
|
|
|
|
this._ws.send(JSON.stringify(payload));
|
|
};
|
|
|
|
nodeType.prototype._openDeferredEditor = function() {
|
|
// BATCH MODE: SKIP EDITOR AND AUTO-APPLY SAVED PARAMS
|
|
if (this.isBatchMode && this._batchParams) {
|
|
this.adjustments = JSON.parse(JSON.stringify(this._batchParams));
|
|
this._syncWidgetsFromAdjustments();
|
|
this.previewImage = null;
|
|
this.setDirtyCanvas(true);
|
|
this._scheduleHeavyRender();
|
|
setTimeout(() => this._sendAdjustments(), 100);
|
|
return;
|
|
}
|
|
|
|
if (!this.pendingEditorData) return;
|
|
this.backgroundImage = null;
|
|
this.previewImage = null;
|
|
this.originalImageData = null;
|
|
this.isLoading = true;
|
|
this.isEditing = false;
|
|
this._hideNativeWidgets();
|
|
|
|
const data = this.pendingEditorData;
|
|
this.adjustments.brightness = data.brightness || 0;
|
|
this.adjustments.contrast = data.contrast || 0;
|
|
this.adjustments.sharpen = data.sharpen || 0;
|
|
this.adjustments.hue = data.hue || 0;
|
|
this.adjustments.saturation = data.saturation || 0;
|
|
|
|
try {
|
|
const adv = JSON.parse(data.advanced_params || "{}");
|
|
|
|
const mergeRecursive = (target, source) => {
|
|
for (let key in source) {
|
|
if (source.hasOwnProperty(key)) {
|
|
if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
if (!target[key] || typeof target[key] !== 'object') target[key] = {};
|
|
mergeRecursive(target[key], source[key]);
|
|
} else {
|
|
target[key] = source[key];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
mergeRecursive(this.adjustments, adv);
|
|
} catch (e) {}
|
|
|
|
this.realBackground = { width: data.bg_width, height: data.bg_height };
|
|
this._updateDisplaySize(this.canvasPixelSize);
|
|
|
|
const bgFile = data.bg_file, ts = data.timestamp;
|
|
const img = new Image();
|
|
img.crossOrigin = "Anonymous";
|
|
const imageUrl = `/view?filename=${bgFile}&type=temp&t=${ts}_${Date.now()}`;
|
|
|
|
img.onload = () => {
|
|
this.backgroundImage = img;
|
|
this.isLoading = false;
|
|
this.isEditing = true;
|
|
|
|
this._tempCanvas.width = img.width;
|
|
this._tempCanvas.height = img.height;
|
|
this._tempCtx.drawImage(img, 0, 0);
|
|
this.originalImageData = this._tempCtx.getImageData(0, 0, img.width, img.height);
|
|
|
|
this.setDirtyCanvas(true, true);
|
|
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
|
this._syncWidgetsFromAdjustments();
|
|
this._syncOverlayUI();
|
|
this._updateBasicParamLabels();
|
|
this._initWebSocket();
|
|
this._scheduleHeavyRender();
|
|
};
|
|
img.onerror = () => {
|
|
this.isLoading = false;
|
|
this.isEditing = false;
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
img.src = imageUrl;
|
|
};
|
|
|
|
nodeType.prototype._updateDisplaySize = function(cS) {
|
|
this.canvasPixelSize = cS;
|
|
const safeHeight = this.realBackground.height || 1;
|
|
const bgAR = this.realBackground.width / safeHeight;
|
|
if (bgAR >= 1) {
|
|
this.displayWidth = cS;
|
|
this.displayHeight = cS / bgAR;
|
|
} else {
|
|
this.displayHeight = cS;
|
|
this.displayWidth = cS * bgAR;
|
|
}
|
|
};
|
|
|
|
nodeType.prototype._toggleAdvancedMode = function(forceClose = false) {
|
|
if (forceClose || this.advancedMode) {
|
|
this.advancedMode = false;
|
|
if (activeNodeId === this.id) activeNodeId = null;
|
|
this.overlayContainer.style.display = 'none';
|
|
if (this._overlayRenderLoop) {
|
|
cancelAnimationFrame(this._overlayRenderLoop);
|
|
this._overlayRenderLoop = null;
|
|
}
|
|
this.setDirtyCanvas(true);
|
|
return;
|
|
}
|
|
if (activeNodeId && activeNodeId !== this.id) {
|
|
const otherNode = app.graph?.nodes.find(n => n.id == activeNodeId);
|
|
if (otherNode && typeof otherNode._toggleAdvancedMode === 'function') {
|
|
otherNode._toggleAdvancedMode(true);
|
|
}
|
|
}
|
|
this.advancedMode = true;
|
|
activeNodeId = this.id;
|
|
this.overlayContainer.style.display = 'flex';
|
|
this._syncOverlayUI();
|
|
requestAnimationFrame(() => {
|
|
this._resizeOverlayCanvas();
|
|
this._updateSectionHeaders();
|
|
this._startOverlayRenderLoop();
|
|
});
|
|
};
|
|
|
|
nodeType.prototype._resizeOverlayCanvas = function() {
|
|
if (!this.overlayCanvasWrapper || !this.overlayCanvas) return;
|
|
const w = this.overlayCanvasWrapper.clientWidth;
|
|
const h = this.overlayCanvasWrapper.clientHeight;
|
|
if (w > 0 && h > 0) {
|
|
this.overlayCanvas.width = w;
|
|
this.overlayCanvas.height = h;
|
|
this.setDirtyCanvas(true);
|
|
}
|
|
};
|
|
|
|
nodeType.prototype._startOverlayRenderLoop = function() {
|
|
if (this._overlayRenderLoop) return;
|
|
const render = () => {
|
|
if (!this.advancedMode) return;
|
|
this._drawOverlayCanvas();
|
|
this._overlayRenderLoop = requestAnimationFrame(render);
|
|
};
|
|
this._overlayRenderLoop = requestAnimationFrame(render);
|
|
};
|
|
|
|
nodeType.prototype._drawOverlayCanvas = function() {
|
|
if (!this.advancedMode || !this.overlayCtx) return;
|
|
const ctx = this.overlayCtx;
|
|
const w = this.overlayCanvas.width;
|
|
const h = this.overlayCanvas.height;
|
|
ctx.clearRect(0, 0, w, h);
|
|
if (this.isLoading) {
|
|
ctx.fillStyle = "#888";
|
|
ctx.font = "14px Arial";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText("Loading...", w / 2, h / 2);
|
|
return;
|
|
}
|
|
if (!this.backgroundImage) return;
|
|
const safeDW = this.displayWidth > 0 ? this.displayWidth : 100;
|
|
const safeDH = this.displayHeight > 0 ? this.displayHeight : 100;
|
|
const scale = Math.min(w / safeDW, h / safeDH);
|
|
const drawW = safeDW * scale;
|
|
const drawH = safeDH * scale;
|
|
const drawX = (w - drawW) / 2;
|
|
const drawY = (h - drawH) / 2;
|
|
|
|
if (this.previewImage) {
|
|
ctx.drawImage(this.previewImage, drawX, drawY, drawW, drawH);
|
|
} else if (this.originalImageData) {
|
|
const processed = new ImageData(
|
|
new Uint8ClampedArray(this.originalImageData.data),
|
|
this.originalImageData.width,
|
|
this.originalImageData.height
|
|
);
|
|
applyBasicAdjustmentsJS(processed, this.adjustments.brightness, this.adjustments.contrast, this.adjustments.hue, this.adjustments.saturation);
|
|
this._tempCanvas.width = processed.width;
|
|
this._tempCanvas.height = processed.height;
|
|
this._tempCtx.putImageData(processed, 0, 0);
|
|
ctx.drawImage(this._tempCanvas, drawX, drawY, drawW, drawH);
|
|
} else {
|
|
ctx.drawImage(this.backgroundImage, drawX, drawY, drawW, drawH);
|
|
}
|
|
};
|
|
|
|
nodeType.prototype._sendAdjustments = async function() {
|
|
if (this.currentRenderAbortController) {
|
|
this.currentRenderAbortController.abort();
|
|
this.currentRenderAbortController = null;
|
|
}
|
|
const payload = { id: String(this.id), adjustments: this.adjustments, is_batch: this.isBatchMode };
|
|
try {
|
|
await api.fetchApi("/rayko/rs_adjustments", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
this.isEditing = false;
|
|
this.setDirtyCanvas(true);
|
|
} catch(e) {}
|
|
};
|
|
|
|
nodeType.prototype._cancelEditing = async function() {
|
|
if (this.currentRenderAbortController) {
|
|
this.currentRenderAbortController.abort();
|
|
this.currentRenderAbortController = null;
|
|
}
|
|
clearTimeout(this.renderTimeout);
|
|
try { await api.interrupt(); } catch(e) {}
|
|
await fetch("/rayko/rs_adjustments/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ node_id: String(this.id) }) });
|
|
this._cleanup();
|
|
};
|
|
|
|
nodeType.prototype._cleanup = function() {
|
|
this.isEditing = false;
|
|
this.isLoading = false;
|
|
this.backgroundImage = null;
|
|
this.previewImage = null;
|
|
this.originalImageData = null;
|
|
this.pendingEditorData = null;
|
|
this.sliderDragging = -1;
|
|
if (this._overlayRenderLoop) {
|
|
cancelAnimationFrame(this._overlayRenderLoop);
|
|
this._overlayRenderLoop = null;
|
|
}
|
|
if (this._ws) {
|
|
this._ws.close();
|
|
this._ws = null;
|
|
this._wsConnected = false;
|
|
}
|
|
if (this._wsReconnectTimer) {
|
|
clearTimeout(this._wsReconnectTimer);
|
|
this._wsReconnectTimer = null;
|
|
}
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
nodeType.prototype.onResize = function(size) {
|
|
if (size[0] < this.minWidth) size[0] = this.minWidth;
|
|
const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30;
|
|
const canvasTopPadding = 30;
|
|
const cSize = Math.max(200, size[0] - 40);
|
|
this.canvasPixelSize = cSize;
|
|
|
|
const slidersH = 5 * 30 + 10;
|
|
const btnH = 30;
|
|
const gaps = 10 + 8 + 8 + 20 + 15;
|
|
const neededHeight = titleH + canvasTopPadding + cSize + slidersH + btnH + gaps;
|
|
if (size[1] < neededHeight) size[1] = neededHeight;
|
|
|
|
this.setDirtyCanvas(true);
|
|
if (this.isEditing && this.backgroundImage) {
|
|
this._updateDisplaySize(cSize);
|
|
}
|
|
};
|
|
|
|
nodeType.prototype._getCanvasMetrics = function() {
|
|
const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30;
|
|
const canvasTopPadding = 30;
|
|
let cSize, rectX, rectY;
|
|
if (this.advancedMode) {
|
|
cSize = Math.min(this.overlayCanvas.width || 1000, this.overlayCanvas.height || 800);
|
|
rectX = 0; rectY = 0;
|
|
} else {
|
|
cSize = Math.max(200, this.size[0] - 40);
|
|
rectX = (this.size[0] - cSize) / 2;
|
|
rectY = titleH + canvasTopPadding;
|
|
}
|
|
return { cSize, rectX, rectY };
|
|
};
|
|
|
|
nodeType.prototype._roundRect = function(ctx, x, y, w, h, r) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + r, y);
|
|
ctx.lineTo(x + w - r, y);
|
|
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
ctx.lineTo(x + w, y + h - r);
|
|
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
ctx.lineTo(x + r, y + h);
|
|
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
ctx.lineTo(x, y + r);
|
|
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
ctx.closePath();
|
|
};
|
|
|
|
nodeType.prototype._drawSlider = function(ctx, label, value, min, max, x, y, w, h, isHover, isActive) {
|
|
ctx.fillStyle = isHover ? "#2a2a2a" : "#252525";
|
|
ctx.strokeStyle = isActive ? "#4CAF50" : (isHover ? "#4CAF50" : "#444");
|
|
ctx.lineWidth = 1;
|
|
this._roundRect(ctx, x, y, w, h, 4);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = "#aaa";
|
|
ctx.font = "10px sans-serif";
|
|
ctx.textAlign = "left";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(label, x + 8, y + h / 2);
|
|
|
|
const trackX = x + 90;
|
|
const trackW = w - 190;
|
|
const trackY = y + h / 2;
|
|
const trackH = 3;
|
|
|
|
ctx.fillStyle = "#444";
|
|
ctx.fillRect(trackX, trackY - trackH / 2, trackW, trackH);
|
|
|
|
const ratio = Math.max(0, Math.min(1, (value - min) / (max - min)));
|
|
const fillW = trackW * ratio;
|
|
ctx.fillStyle = "#4CAF50";
|
|
ctx.fillRect(trackX, trackY - trackH / 2, fillW, trackH);
|
|
|
|
const handleX = trackX + fillW;
|
|
const handleSize = 12;
|
|
ctx.fillStyle = "#fff";
|
|
ctx.beginPath();
|
|
ctx.arc(handleX, trackY, handleSize / 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
const valueW = 50;
|
|
const valueX = x + w - valueW - 38;
|
|
ctx.fillStyle = "#222";
|
|
ctx.strokeStyle = "#444";
|
|
this._roundRect(ctx, valueX, y + 3, valueW, h - 6, 3);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = "#4CAF50";
|
|
ctx.font = "10px monospace";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText(String(Math.round(value)), valueX + valueW / 2, y + h / 2);
|
|
|
|
const resetBtnSize = 24;
|
|
const resetBtnX = x + w - resetBtnSize - 8;
|
|
const resetBtnY = y + (h - resetBtnSize) / 2;
|
|
ctx.fillStyle = "#252525";
|
|
ctx.strokeStyle = "#444";
|
|
this._roundRect(ctx, resetBtnX, resetBtnY, resetBtnSize, resetBtnSize, 4);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = "#888";
|
|
ctx.font = "14px sans-serif";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("🔄", resetBtnX + resetBtnSize / 2, resetBtnY + resetBtnSize / 2);
|
|
|
|
return { x: trackX - 6, y: y, w: trackW + 12, h: h, resetBtnX, resetBtnY, resetBtnSize, valueX, valueY: y, valueW, valueH: h };
|
|
};
|
|
|
|
nodeType.prototype.onDrawForeground = function(ctx) {
|
|
if (this.advancedMode) {
|
|
ctx.clearRect(0, 0, this.size[0], this.size[1]);
|
|
return;
|
|
}
|
|
|
|
const { cSize, rectX, rectY } = this._getCanvasMetrics();
|
|
ctx.fillStyle = "#1e1e1e";
|
|
ctx.fillRect(rectX, rectY, cSize, cSize);
|
|
ctx.strokeStyle = "#555";
|
|
ctx.strokeRect(rectX, rectY, cSize, cSize);
|
|
|
|
this._updateDisplaySize(cSize);
|
|
|
|
if (this.isLoading) {
|
|
ctx.fillStyle = "#888";
|
|
ctx.font = "12px Arial";
|
|
ctx.fillText("Loading...", rectX + cSize / 2 - 35, rectY + cSize / 2);
|
|
} else if (this.isEditing && this.backgroundImage) {
|
|
try {
|
|
const safeDW = this.displayWidth > 0 ? this.displayWidth : 100;
|
|
const safeDH = this.displayHeight > 0 ? this.displayHeight : 100;
|
|
|
|
ctx.save();
|
|
ctx.translate(rectX, rectY);
|
|
|
|
const scale = Math.min(cSize / safeDW, cSize / safeDH);
|
|
const drawW = safeDW * scale;
|
|
const drawH = safeDH * scale;
|
|
const drawX = (cSize - drawW) / 2;
|
|
const drawY = (cSize - drawH) / 2;
|
|
|
|
if (this.previewImage) {
|
|
ctx.drawImage(this.previewImage, drawX, drawY, drawW, drawH);
|
|
} else if (this.originalImageData) {
|
|
const processed = new ImageData(
|
|
new Uint8ClampedArray(this.originalImageData.data),
|
|
this.originalImageData.width,
|
|
this.originalImageData.height
|
|
);
|
|
applyBasicAdjustmentsJS(processed, this.adjustments.brightness, this.adjustments.contrast, this.adjustments.hue, this.adjustments.saturation);
|
|
this._tempCanvas.width = processed.width;
|
|
this._tempCanvas.height = processed.height;
|
|
this._tempCtx.putImageData(processed, 0, 0);
|
|
ctx.drawImage(this._tempCanvas, drawX, drawY, drawW, drawH);
|
|
} else {
|
|
ctx.drawImage(this.backgroundImage, drawX, drawY, drawW, drawH);
|
|
}
|
|
|
|
ctx.restore();
|
|
} catch (e) {
|
|
console.error('[RS Adjustments] FATAL DRAW ERROR:', e);
|
|
}
|
|
} else {
|
|
ctx.fillStyle = "#888";
|
|
ctx.font = "12px Arial";
|
|
ctx.fillText("▶ Run queue to start", rectX + cSize / 2 - 65, rectY + cSize / 2);
|
|
}
|
|
|
|
const btnW = 140;
|
|
const btnH = 24;
|
|
const gap = 10;
|
|
const totalW = btnW * 2 + gap;
|
|
const startX = (this.size[0] - totalW) / 2;
|
|
const btnY = 20;
|
|
|
|
const advBtnX = startX;
|
|
ctx.fillStyle = this.btnAdvancedHover ? "#3a3a3a" : "#2a2a2a";
|
|
ctx.strokeStyle = "#2196F3";
|
|
ctx.lineWidth = 2;
|
|
this._roundRect(ctx, advBtnX, btnY, btnW, btnH, 6);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = "#2196F3";
|
|
ctx.font = "bold 11px Arial";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "alphabetic";
|
|
ctx.fillText("✨ ADVANCED", advBtnX + btnW/2, btnY + btnH/2 + 4);
|
|
|
|
const resetBtnX = startX + btnW + gap;
|
|
ctx.fillStyle = this.btnResetAllHover ? "#3a3a3a" : "#2a2a2a";
|
|
ctx.strokeStyle = "#FF9800";
|
|
ctx.lineWidth = 2;
|
|
this._roundRect(ctx, resetBtnX, btnY, btnW, btnH, 6);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = "#FF9800";
|
|
ctx.font = "bold 11px Arial";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "alphabetic";
|
|
ctx.fillText("🔄 RESET ALL", resetBtnX + btnW/2, btnY + btnH/2 + 4);
|
|
|
|
const widgetX = 15;
|
|
const widgetW = this.size[0] - 30;
|
|
const slidersY = rectY + cSize + 10;
|
|
this.slidersY = slidersY;
|
|
this.sliderRects = [];
|
|
this.resetBtnRects = [];
|
|
|
|
const sliderH = 30;
|
|
const gapSlider = 8;
|
|
const sliderConfigs = [
|
|
{ label: "BRIGHTNESS", key: "brightness", min: -100, max: 100 },
|
|
{ label: "CONTRAST", key: "contrast", min: -100, max: 100 },
|
|
{ label: "SHARPEN", key: "sharpen", min: 0, max: 200 },
|
|
{ label: "HUE", key: "hue", min: -180, max: 180 },
|
|
{ label: "SATURATION", key: "saturation", min: -100, max: 100 }
|
|
];
|
|
|
|
sliderConfigs.forEach((cfg, i) => {
|
|
const y = slidersY + i * (sliderH + gapSlider);
|
|
const rect = this._drawSlider(ctx, cfg.label, this.adjustments[cfg.key], cfg.min, cfg.max, widgetX, y, widgetW, sliderH, this.sliderHover[i], this.sliderDragging === i);
|
|
this.sliderRects.push({ ...rect, key: cfg.key, min: cfg.min, max: cfg.max });
|
|
|
|
const resetBtnRect = {
|
|
x: rect.resetBtnX,
|
|
y: rect.resetBtnY,
|
|
w: rect.resetBtnSize,
|
|
h: rect.resetBtnSize,
|
|
key: cfg.key
|
|
};
|
|
this.resetBtnRects.push(resetBtnRect);
|
|
|
|
if (this.resetBtnHover[i]) {
|
|
ctx.strokeStyle = "#4CAF50";
|
|
ctx.lineWidth = 1.5;
|
|
this._roundRect(ctx, resetBtnRect.x, resetBtnRect.y, resetBtnRect.w, resetBtnRect.h, 4);
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
|
|
// THREE BUTTON ROW: BATCH | APPLY | CANCEL
|
|
const buttonsY = slidersY + 5 * (sliderH + gapSlider) + 10;
|
|
const btnH2 = 30;
|
|
const btnGap2 = 8;
|
|
const btnW3 = (this.size[0] - 46) / 3;
|
|
|
|
// BATCH BUTTON
|
|
const batchBtnX = 15;
|
|
const isBatchActive = this.isBatchMode;
|
|
ctx.fillStyle = isBatchActive ? "#2196F3" : (this.btnBatchHover ? "#3a3a3a" : "#2a2a2a");
|
|
ctx.strokeStyle = isBatchActive ? "#2196F3" : "#666";
|
|
ctx.lineWidth = isBatchActive ? 2 : 1;
|
|
this._roundRect(ctx, batchBtnX, buttonsY, btnW3, btnH2, 6);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = isBatchActive ? "#fff" : "#aaa";
|
|
ctx.font = "bold 11px Arial";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "alphabetic";
|
|
ctx.fillText(isBatchActive ? "⚪ BATCH ON" : "️🔘 BATCH OFF", batchBtnX + btnW3/2, buttonsY + btnH2/2 + 4);
|
|
|
|
// APPLY BUTTON
|
|
const applyBtnX = batchBtnX + btnW3 + btnGap2;
|
|
ctx.fillStyle = this.btnApplyHover ? "#444" : "#2a2a2a";
|
|
this._roundRect(ctx, applyBtnX, buttonsY, btnW3, btnH2, 6);
|
|
ctx.fill();
|
|
ctx.strokeStyle = "#4CAF50";
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
ctx.fillStyle = "#4CAF50";
|
|
ctx.fillText("✔️ APPLY", applyBtnX + btnW3/2, buttonsY + btnH2/2 + 4);
|
|
|
|
// CANCEL BUTTON
|
|
const cancelBtnX = applyBtnX + btnW3 + btnGap2;
|
|
ctx.fillStyle = this.btnCancelHover ? "#444" : "#2a2a2a";
|
|
this._roundRect(ctx, cancelBtnX, buttonsY, btnW3, btnH2, 6);
|
|
ctx.fill();
|
|
ctx.strokeStyle = "#dc3545";
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
ctx.fillStyle = "#dc3545";
|
|
ctx.fillText("❌ CANCEL", cancelBtnX + btnW3/2, buttonsY + btnH2/2 + 4);
|
|
};
|
|
|
|
nodeType.prototype.onMouseDown = function(event, pos) {
|
|
if (!this.advancedMode && pos) {
|
|
const btnW = 140;
|
|
const btnH = 24;
|
|
const gap = 10;
|
|
const totalW = btnW * 2 + gap;
|
|
const startX = (this.size[0] - totalW) / 2;
|
|
const btnY = 20;
|
|
const advBtnX = startX;
|
|
const resetBtnX = startX + btnW + gap;
|
|
|
|
if (pos[0] >= advBtnX && pos[0] <= advBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH) {
|
|
this._toggleAdvancedMode();
|
|
return true;
|
|
}
|
|
if (pos[0] >= resetBtnX && pos[0] <= resetBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH) {
|
|
this._resetAllParameters();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (this.sliderRects && pos) {
|
|
for (let i = 0; i < this.sliderRects.length; i++) {
|
|
const rect = this.sliderRects[i];
|
|
if (rect.valueX !== undefined && pos[0] >= rect.valueX && pos[0] <= rect.valueX + rect.valueW && pos[1] >= rect.valueY && pos[1] <= rect.valueY + rect.valueH) {
|
|
this._showValueInputPopup(i, event);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.sliderRects && pos) {
|
|
for (let i = 0; i < this.sliderRects.length; i++) {
|
|
const rect = this.sliderRects[i];
|
|
if (pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h) {
|
|
this.sliderDragging = i;
|
|
this._updateSliderFromMouse(i, pos[0]);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.resetBtnRects && pos) {
|
|
for (let i = 0; i < this.resetBtnRects.length; i++) {
|
|
const rect = this.resetBtnRects[i];
|
|
if (pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h) {
|
|
const defaultVal = DEFAULT_SLIDER_VALUES[rect.key] !== undefined ? DEFAULT_SLIDER_VALUES[rect.key] : 0;
|
|
this.adjustments[rect.key] = defaultVal;
|
|
const widget = this.widgets?.find(w => w.name === rect.key);
|
|
if (widget) widget.value = defaultVal;
|
|
this.previewImage = null;
|
|
this.setDirtyCanvas(true);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pos && this.slidersY !== undefined) {
|
|
const btnH2 = 30;
|
|
const btnGap2 = 8;
|
|
const btnW3 = (this.size[0] - 46) / 3;
|
|
const buttonsY = this.slidersY + 5 * 38 + 10;
|
|
|
|
// BATCH CLICK
|
|
const batchBtnX = 15;
|
|
if (pos[0] >= batchBtnX && pos[0] <= batchBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) {
|
|
this.isBatchMode = !this.isBatchMode;
|
|
console.log(`[RS Adjustments] Node ${this.id}: Batch toggled to ${this.isBatchMode}`);
|
|
if (!this.isBatchMode) {
|
|
this._batchParams = null;
|
|
}
|
|
this.setDirtyCanvas(true);
|
|
return true;
|
|
}
|
|
|
|
// APPLY CLICK
|
|
const applyBtnX = batchBtnX + btnW3 + btnGap2;
|
|
if (pos[0] >= applyBtnX && pos[0] <= applyBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) {
|
|
if (this.isBatchMode) {
|
|
this._batchParams = JSON.parse(JSON.stringify(this.adjustments));
|
|
}
|
|
this._sendAdjustments();
|
|
return true;
|
|
}
|
|
|
|
// CANCEL CLICK
|
|
const cancelBtnX = applyBtnX + btnW3 + btnGap2;
|
|
if (pos[0] >= cancelBtnX && pos[0] <= cancelBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) {
|
|
this.isBatchMode = false;
|
|
this._batchParams = null;
|
|
this._cancelEditing();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
nodeType.prototype.onMouseMove = function(event, pos) {
|
|
if (this.sliderDragging >= 0 && event && event.buttons === 0) {
|
|
this.sliderDragging = -1;
|
|
this.setDirtyCanvas(true);
|
|
return;
|
|
}
|
|
|
|
if (this.sliderDragging >= 0 && pos) {
|
|
this._updateSliderFromMouse(this.sliderDragging, pos[0]);
|
|
return;
|
|
}
|
|
|
|
if (pos && !this.advancedMode) {
|
|
const btnW = 140;
|
|
const btnH = 24;
|
|
const gap = 10;
|
|
const totalW = btnW * 2 + gap;
|
|
const startX = (this.size[0] - totalW) / 2;
|
|
const btnY = 20;
|
|
const advBtnX = startX;
|
|
const resetBtnX = startX + btnW + gap;
|
|
|
|
const prevAdv = this.btnAdvancedHover;
|
|
const prevReset = this.btnResetAllHover;
|
|
this.btnAdvancedHover = pos[0] >= advBtnX && pos[0] <= advBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH;
|
|
this.btnResetAllHover = pos[0] >= resetBtnX && pos[0] <= resetBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH;
|
|
if (prevAdv !== this.btnAdvancedHover || prevReset !== this.btnResetAllHover) {
|
|
this.setDirtyCanvas(true);
|
|
}
|
|
}
|
|
|
|
const prevHover = [...this.sliderHover];
|
|
const prevResetHover = [...this.resetBtnHover];
|
|
|
|
if (this.sliderRects && pos) {
|
|
for (let i = 0; i < this.sliderRects.length; i++) {
|
|
const rect = this.sliderRects[i];
|
|
this.sliderHover[i] = pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h;
|
|
}
|
|
}
|
|
|
|
if (this.resetBtnRects && pos) {
|
|
for (let i = 0; i < this.resetBtnRects.length; i++) {
|
|
const rect = this.resetBtnRects[i];
|
|
this.resetBtnHover[i] = pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h;
|
|
}
|
|
}
|
|
|
|
if (prevHover.some((v, i) => v !== this.sliderHover[i]) || prevResetHover.some((v, i) => v !== this.resetBtnHover[i])) {
|
|
this.setDirtyCanvas(true);
|
|
}
|
|
|
|
if (pos && this.slidersY !== undefined) {
|
|
const btnH2 = 30;
|
|
const btnGap2 = 8;
|
|
const btnW3 = (this.size[0] - 46) / 3;
|
|
const buttonsY = this.slidersY + 5 * 38 + 10;
|
|
|
|
const prevBatch = this.btnBatchHover;
|
|
const batchBtnX = 15;
|
|
this.btnBatchHover = pos[0] >= batchBtnX && pos[0] <= batchBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2;
|
|
|
|
const prevApply = this.btnApplyHover;
|
|
const prevCancel = this.btnCancelHover;
|
|
const applyBtnX = batchBtnX + btnW3 + btnGap2;
|
|
const cancelBtnX = applyBtnX + btnW3 + btnGap2;
|
|
this.btnApplyHover = pos[0] >= applyBtnX && pos[0] <= applyBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2;
|
|
this.btnCancelHover = pos[0] >= cancelBtnX && pos[0] <= cancelBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2;
|
|
|
|
if (prevBatch !== this.btnBatchHover || prevApply !== this.btnApplyHover || prevCancel !== this.btnCancelHover) {
|
|
this.setDirtyCanvas(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
nodeType.prototype.onMouseUp = function() {
|
|
if (this.sliderDragging >= 0) {
|
|
this.sliderDragging = -1;
|
|
this.setDirtyCanvas(true);
|
|
}
|
|
};
|
|
|
|
nodeType.prototype._updateSliderFromMouse = function(index, mouseX) {
|
|
if (!this.sliderRects || !this.sliderRects[index]) return;
|
|
const rect = this.sliderRects[index];
|
|
const ratio = Math.max(0, Math.min(1, (mouseX - rect.x) / rect.w));
|
|
const value = Math.round(rect.min + ratio * (rect.max - rect.min));
|
|
this.adjustments[rect.key] = value;
|
|
|
|
const widget = this.widgets?.find(w => w.name === rect.key);
|
|
if (widget) widget.value = value;
|
|
|
|
if (this.advancedMode) {
|
|
if (this.overlayInputs[rect.key]) {
|
|
this.overlayInputs[rect.key].value = value;
|
|
}
|
|
if (this.sliderDisplays[rect.key]) {
|
|
this.sliderDisplays[rect.key].textContent = String(value);
|
|
}
|
|
}
|
|
|
|
if (BASIC_PARAMS.includes(rect.key)) {
|
|
this.previewImage = null;
|
|
} else {
|
|
this._scheduleHeavyRender();
|
|
}
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
nodeType.prototype._showValueInputPopup = function(index, event) {
|
|
if (!this.sliderRects || !this.sliderRects[index]) return;
|
|
const rect = this.sliderRects[index];
|
|
const currentValue = this.adjustments[rect.key];
|
|
|
|
const screenX = event.clientX;
|
|
const screenY = event.clientY;
|
|
|
|
const popup = document.createElement('div');
|
|
popup.style.cssText = 'position:fixed;z-index:10003;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:8px 12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:8px;';
|
|
|
|
const input = document.createElement('input');
|
|
input.type = 'number';
|
|
input.value = currentValue;
|
|
input.min = rect.min;
|
|
input.max = rect.max;
|
|
input.step = 1;
|
|
input.style.cssText = 'width:100px;background:#222;color:#fff;border:1px solid #444;border-radius:4px;padding:6px 10px;font-size:12px;outline:none;';
|
|
|
|
const saveBtn = document.createElement('button');
|
|
saveBtn.textContent = 'OK';
|
|
saveBtn.style.cssText = 'background:#4CAF50;color:#fff;border:none;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer;';
|
|
|
|
const doSave = () => {
|
|
let num = parseInt(input.value);
|
|
if (isNaN(num)) num = currentValue;
|
|
num = Math.max(rect.min, Math.min(rect.max, num));
|
|
this.adjustments[rect.key] = num;
|
|
|
|
const widget = this.widgets?.find(w => w.name === rect.key);
|
|
if (widget) widget.value = num;
|
|
|
|
if (this.advancedMode) {
|
|
if (this.overlayInputs[rect.key]) {
|
|
this.overlayInputs[rect.key].value = num;
|
|
}
|
|
if (this.sliderDisplays[rect.key]) {
|
|
this.sliderDisplays[rect.key].textContent = String(num);
|
|
}
|
|
}
|
|
|
|
popup.remove();
|
|
this.previewImage = null;
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); };
|
|
input.onkeydown = (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); doSave(); } };
|
|
|
|
popup.appendChild(input);
|
|
popup.appendChild(saveBtn);
|
|
document.body.appendChild(popup);
|
|
|
|
const popupWidth = popup.offsetWidth || 180;
|
|
const popupHeight = popup.offsetHeight || 40;
|
|
|
|
let leftPos = screenX + 15;
|
|
let topPos = screenY - popupHeight / 2;
|
|
|
|
if (leftPos + popupWidth > window.innerWidth - 10) {
|
|
leftPos = screenX - popupWidth - 15;
|
|
}
|
|
if (topPos < 10) topPos = 10;
|
|
if (topPos + popupHeight > window.innerHeight - 10) {
|
|
topPos = window.innerHeight - popupHeight - 10;
|
|
}
|
|
|
|
popup.style.left = leftPos + 'px';
|
|
popup.style.top = topPos + 'px';
|
|
|
|
setTimeout(() => { input.focus(); input.select(); }, 50);
|
|
|
|
setTimeout(() => {
|
|
const closeHandler = (ev) => {
|
|
if (!popup.contains(ev.target)) {
|
|
popup.remove();
|
|
document.removeEventListener('mousedown', closeHandler);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', closeHandler);
|
|
}, 100);
|
|
};
|
|
}
|
|
}); |