Files
CosmicLaca-ComfyUI_Primere_…/front_end/primere_rasterix.js
T

450 lines
18 KiB
JavaScript

import { app } from "/scripts/app.js";
const CB_TONES = ["highlights", "midtones", "shadows"];
const HS_CHANNELS = ["master", "red", "green", "blue"];
const ST_ZONES = ["highlights", "midtones", "shadows", "blacks"];
const SH_MODES = ["fine", "medium", "broad"];
const POST_CHANNELS = ["Red", "Green", "Blue"];
function buildFilmPresetMap(allPresets) {
const byType = {};
for (const name of allPresets) {
const match = String(name).match(/_([A-Z][A-Z0-9]*)$/);
if (!match) continue;
const type = match[1];
if (!Array.isArray(byType[type])) byType[type] = [];
byType[type].push(name);
}
return byType;
}
const cbDefault = () => ({
highlights: { cyan_red: 0, magenta_green: 0, yellow_blue: 0 },
midtones: { cyan_red: 0, magenta_green: 0, yellow_blue: 0 },
shadows: { cyan_red: 0, magenta_green: 0, yellow_blue: 0 },
});
const hsDefault = () => ({
master: { hue: 0, saturation: 0, lightness: 0, vibrance: 0 },
red: { hue: 0, saturation: 0, lightness: 0, vibrance: 0 },
green: { hue: 0, saturation: 0, lightness: 0, vibrance: 0 },
blue: { hue: 0, saturation: 0, lightness: 0, vibrance: 0 },
});
const stDefault = () => ({ highlights: 0, midtones: 0, shadows: 0, blacks: 0 });
const shDefault = () => ({
fine: { shade_level: 0, shade_radius: 0 },
medium: { shade_level: 0, shade_radius: 0 },
broad: { shade_level: 0, shade_radius: 0 },
});
const postDefault = () => ({ Red: 255, Green: 255, Blue: 255 });
async function rasterixLoad() {
try {
const resp = await fetch('/primere_rasterix_read');
if (!resp.ok) return {
color_balance: cbDefault(), hue_saturation: hsDefault(),
selective_tone: stDefault(), shade: shDefault(), posterize: postDefault(),
};
const data = await resp.json();
const cb = data.color_balance || {};
for (const t of CB_TONES)
if (!cb[t]) cb[t] = { cyan_red: 0, magenta_green: 0, yellow_blue: 0 };
const hs = data.hue_saturation || {};
for (const ch of HS_CHANNELS)
if (!hs[ch]) hs[ch] = { hue: 0, saturation: 0, lightness: 0, vibrance: 0 };
const st = data.selective_tone || {};
for (const z of ST_ZONES)
if (st[z] === undefined) st[z] = 0;
const sh = data.shade || {};
for (const m of SH_MODES)
if (!sh[m]) sh[m] = { shade_level: 0, shade_radius: 0 };
const post = data.posterize || {};
for (const ch of POST_CHANNELS)
if (post[ch] === undefined) post[ch] = 255;
return { color_balance: cb, hue_saturation: hs, selective_tone: st, shade: sh, posterize: post };
} catch {
return {
color_balance: cbDefault(), hue_saturation: hsDefault(),
selective_tone: stDefault(), shade: shDefault(), posterize: postDefault(),
};
}
}
async function rasterixSave(section, data) {
try {
await fetch('/primere_rasterix_save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ section, data }),
});
} catch (e) {
console.warn('[Primere Rasterix] save failed:', e);
}
}
app.registerExtension({
name: "Primere.Rasterix",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
const rasterixNodes = [
"PrimereRasterix", "PrimereSelectiveTone", "PrimereColorBalance",
"PrimereHSL", "PrimereShadeDetailer", "PrimereHistogram",
"PrimereFilmRendering", "PrimerePosterize",
];
if (!rasterixNodes.includes(nodeData.name)) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
const node = this;
const fw = (name) => node.widgets?.find(w => w.name === name);
const wTone = fw("color_balance_tone");
const wCR = fw("color_balance_cyan_red");
const wMG = fw("color_balance_magenta_green");
const wYB = fw("color_balance_yellow_blue");
const wHsCh = fw("hsl_channel");
const wHsHue = fw("hsl_hue");
const wHsSat = fw("hsl_saturation");
const wHsLit = fw("hsl_lightness");
const wHsVib = fw("hsl_vibrance");
const wStZone = fw("selective_tone_zone");
const wStVal = fw("selective_tone_value");
const wShMode = fw("detail_mode");
const wShLvl = fw("shade_level");
const wShRad = fw("shade_radius");
const wPostCh = fw("channels");
const wPostSh = fw("shades");
let cbStore = cbDefault();
let hsStore = hsDefault();
let stStore = stDefault();
let shStore = shDefault();
let postStore = postDefault();
let prevTone = wTone?.value ?? "midtones";
let prevHsCh = wHsCh?.value ?? "master";
let prevStZn = wStZone?.value ?? "midtones";
let prevShMd = wShMode?.value ?? "medium";
let prevPostCh = wPostCh?.value ?? "Red";
let updating = false;
let histogramDebounceTimer = null;
function applyFilmTypeFilter() {
const filmTypeWidget = fw("film_type");
const filmRenderingWidget = fw("film_rendering");
if (!filmTypeWidget || !filmRenderingWidget) return;
if (!Array.isArray(node.__primereFilmAllPresets) || node.__primereFilmAllPresets.length === 0) {
const initialValues = filmRenderingWidget.options?.values || [];
node.__primereFilmAllPresets = [...initialValues];
node.__primereFilmByType = buildFilmPresetMap(node.__primereFilmAllPresets);
}
const selectedType = String(filmTypeWidget.value || "All");
const allValues = node.__primereFilmAllPresets;
const byType = node.__primereFilmByType || {};
const nextValues = selectedType === "All"
? allValues
: (byType[selectedType]?.length > 0 ? byType[selectedType] : allValues);
filmRenderingWidget.options = filmRenderingWidget.options || {};
filmRenderingWidget.options.values = [...nextValues];
if (!nextValues.includes(filmRenderingWidget.value)) {
filmRenderingWidget.value = nextValues[0] || filmRenderingWidget.value;
filmRenderingWidget.callback?.(filmRenderingWidget.value);
}
app.canvas?.setDirty(true);
}
applyFilmTypeFilter();
function currentNodeId() {
return String(node?.id ?? "global");
}
function histogramFileUrl(showInput, channel, style) {
const q = new URLSearchParams({
node_id: currentNodeId(),
histogram_source: String(!!showInput),
histogram_channel: channel || "RGB",
histogram_style: style || "bars",
});
return `/primere_rasterix_histogram_image?${q.toString()}`;
}
function updateHistogramDisplay(showInput, channel, style) {
const url = `${histogramFileUrl(showInput, channel, style)}&t=${Date.now()}`;
const img = new Image();
img.onload = () => {
if (!node.imgs) node.imgs = [img];
else node.imgs[0] = img;
app.canvas?.setDirty(true);
};
img.onerror = () => {};
img.src = url;
}
function showHistogramOffImage() {
const url = `/extensions/ComfyUI_Primere_Nodes/images/No_histogram_08.jpg?t=${Date.now()}`;
const img = new Image();
img.onload = () => {
if (!node.imgs) node.imgs = [img];
else node.imgs[0] = img;
app.canvas?.setDirty(true);
};
img.onerror = () => {};
img.src = url;
}
async function histogramFileExists(showInput, channel, style) {
const url = `${histogramFileUrl(showInput, channel, style)}&t=${Date.now()}`;
try {
const headResp = await fetch(url, { method: "HEAD" });
if (headResp.ok) return true;
} catch (_) {}
try {
const getResp = await fetch(url, { method: "GET" });
return getResp.ok;
} catch (_) {
return false;
}
}
async function generateHistogram(showInput, channel, style) {
try {
await fetch('/primere_rasterix_histogram_generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
node_id: currentNodeId(),
histogram_source: showInput,
histogram_channel: channel || "RGB",
histogram_style: style || "bars",
precision: fw("precision")?.value ?? false,
}),
});
} catch (e) {
console.warn('[Primere Rasterix] histogram render failed:', e);
}
}
function scheduleHistogramGenerate(showInput, channel, style, delayMs = 1000) {
if (histogramDebounceTimer) clearTimeout(histogramDebounceTimer);
histogramDebounceTimer = setTimeout(async () => {
await generateHistogram(showInput, channel, style);
updateHistogramDisplay(showInput, channel, style);
}, delayMs);
}
async function requestHistogramSwitch(showInput, channel, style) {
if (await histogramFileExists(showInput, channel, style)) {
if (histogramDebounceTimer) clearTimeout(histogramDebounceTimer);
updateHistogramDisplay(showInput, channel, style);
return;
}
scheduleHistogramGenerate(showInput, channel, style, 1000);
}
function currentHistState() {
return {
enabled: fw("show_histogram")?.value ?? false,
showInput: fw("histogram_source")?.value ?? false,
channel: fw("histogram_channel")?.value ?? "RGB",
style: fw("histogram_style")?.value ?? "bars",
};
}
node.onExecuted = async function () {
const { enabled, showInput, channel, style } = currentHistState();
if (!enabled) { showHistogramOffImage(); return; }
if (await histogramFileExists(showInput, channel, style)) {
updateHistogramDisplay(showInput, channel, style);
return;
}
await generateHistogram(showInput, channel, style);
updateHistogramDisplay(showInput, channel, style);
};
// Color Balance
function applyCbSliders(tone) {
if (!wCR) return;
updating = true;
const v = cbStore[tone] || { cyan_red: 0, magenta_green: 0, yellow_blue: 0 };
wCR.value = v.cyan_red;
wMG.value = v.magenta_green;
wYB.value = v.yellow_blue;
updating = false;
app.canvas?.setDirty(true);
}
function captureCbSliders(tone) {
if (!wCR) return;
cbStore[tone] = { cyan_red: wCR.value, magenta_green: wMG.value, yellow_blue: wYB.value };
rasterixSave('color_balance', cbStore);
}
// Hue / Saturation
function applyHsSliders(ch) {
if (!wHsHue) return;
updating = true;
const v = hsStore[ch] || { hue: 0, saturation: 0, lightness: 0, vibrance: 0 };
wHsHue.value = v.hue;
wHsSat.value = v.saturation;
wHsLit.value = v.lightness;
wHsVib.value = v.vibrance;
updating = false;
app.canvas?.setDirty(true);
}
function captureHsSliders(ch) {
if (!wHsHue) return;
hsStore[ch] = { hue: wHsHue.value, saturation: wHsSat.value, lightness: wHsLit.value, vibrance: wHsVib.value };
rasterixSave('hue_saturation', hsStore);
}
// Selective Tone
function applyStSlider(zone) {
if (!wStVal) return;
updating = true;
wStVal.value = stStore[zone] ?? 0;
updating = false;
app.canvas?.setDirty(true);
}
function captureStSlider(zone) {
if (!wStVal) return;
stStore[zone] = wStVal.value;
rasterixSave('selective_tone', stStore);
}
// Shade
function applyShSliders(mode) {
if (!wShLvl) return;
updating = true;
const v = shStore[mode] || { shade_level: 0, shade_radius: 0 };
wShLvl.value = v.shade_level;
wShRad.value = v.shade_radius;
updating = false;
app.canvas?.setDirty(true);
}
function captureShSliders(mode) {
if (!wShLvl) return;
shStore[mode] = { shade_level: wShLvl.value, shade_radius: wShRad.value };
rasterixSave('shade', shStore);
}
// Posterize
function applyPostSlider(ch) {
if (!wPostSh) return;
updating = true;
wPostSh.value = postStore[ch] ?? 255;
updating = false;
app.canvas?.setDirty(true);
}
function capturePostSlider(ch) {
if (!wPostSh) return;
postStore[ch] = wPostSh.value;
rasterixSave('posterize', postStore);
}
// Initial load
rasterixLoad().then(loaded => {
cbStore = loaded.color_balance;
hsStore = loaded.hue_saturation;
stStore = loaded.selective_tone;
shStore = loaded.shade;
postStore = loaded.posterize;
applyCbSliders(prevTone);
applyHsSliders(prevHsCh);
applyStSlider(prevStZn);
applyShSliders(prevShMd);
applyPostSlider(prevPostCh);
});
// Widget change handler
node.onWidgetChanged = function (name, value) {
if (updating) return;
if (name === "film_type") {
applyFilmTypeFilter();
} else if (name === "color_balance_tone") {
captureCbSliders(prevTone);
applyCbSliders(value);
prevTone = value;
} else if (
name === "color_balance_cyan_red" ||
name === "color_balance_magenta_green" ||
name === "color_balance_yellow_blue"
) {
captureCbSliders(wTone?.value ?? prevTone);
} else if (name === "hsl_channel") {
captureHsSliders(prevHsCh);
applyHsSliders(value);
prevHsCh = value;
} else if (
name === "hsl_hue" ||
name === "hsl_saturation" ||
name === "hsl_lightness" ||
name === "hsl_vibrance"
) {
captureHsSliders(wHsCh?.value ?? prevHsCh);
} else if (name === "selective_tone_zone") {
captureStSlider(prevStZn);
applyStSlider(value);
prevStZn = value;
} else if (name === "selective_tone_value") {
captureStSlider(wStZone?.value ?? prevStZn);
} else if (name === "detail_mode") {
captureShSliders(prevShMd);
applyShSliders(value);
prevShMd = value;
} else if (
name === "shade_level" ||
name === "shade_radius"
) {
captureShSliders(wShMode?.value ?? prevShMd);
} else if (name === "channels") {
capturePostSlider(prevPostCh);
applyPostSlider(value);
prevPostCh = value;
} else if (name === "shades") {
capturePostSlider(wPostCh?.value ?? prevPostCh);
} else if (name === "histogram_source") {
const { enabled, channel, style } = currentHistState();
if (enabled) requestHistogramSwitch(value, channel, style);
} else if (name === "histogram_channel") {
const { enabled, showInput, style } = currentHistState();
if (enabled) requestHistogramSwitch(showInput, value, style);
} else if (name === "histogram_style") {
const { enabled, showInput, channel } = currentHistState();
if (enabled) requestHistogramSwitch(showInput, channel, value);
} else if (name === "show_histogram") {
const { showInput, channel, style } = currentHistState();
if (value) {
requestHistogramSwitch(showInput, channel, style);
} else {
if (histogramDebounceTimer) clearTimeout(histogramDebounceTimer);
showHistogramOffImage();
}
}
};
};
},
});