Merge pull request #26 from palealloy2999-prog/agent/add-detail-daemon-sampler-gui
Add interactive Detail Daemon sampler graph
This commit is contained in:
@@ -8,8 +8,9 @@ Here is a video demo showing Detail Daemon in action, provided by [MuseMachine](
|
||||
|
||||
# ComfyUI-Detail-Daemon
|
||||
|
||||
A port of muerrilla's [sd-webui-Detail-Daemon](https://github.com/muerrilla/sd-webui-detail-daemon/) as a node for ComfyUI, to adjust sigmas that generally enhance details, and possibly remove unwanted bokeh or background blurring, particularly with Flux models (but also works with SDXL, SD1.5, Qwen, Z-Image, and likely other models). If the values are taken too far it results in an oversharpened and/or HDR effect. There are four nodes here. Multiply Sigmas and Lying Sigma Sampler are also included as alternative methods of generally enhancing details.
|
||||
A port of muerrilla's [sd-webui-Detail-Daemon](https://github.com/muerrilla/sd-webui-detail-daemon/) as nodes for ComfyUI, to adjust sigmas that generally enhance details and possibly remove unwanted bokeh or background blurring, particularly with Flux models (but also works with SDXL, SD1.5, Qwen, Z-Image, and likely other models). If the values are taken too far it results in an oversharpened and/or HDR effect. There are five nodes here, including an interactive sampler GUI. Multiply Sigmas and Lying Sigma Sampler are also included as alternative methods of generally enhancing details.
|
||||
|
||||
- [**Detail Daemon Sampler GUI**](#detail-daemon-sampler-gui) (sampler and interactive schedule graph in one node)
|
||||
- [**Detail Daemon Sampler**](#detail-daemon-sampler)
|
||||
- [**Detail Daemon Graph Sigmas**](#detail-daemon-graph-sigmas) (to graph the sigmas adjustment visually)
|
||||
- [**Multiply Sigmas**](#multiply-sigmas) (stateless)
|
||||
@@ -39,6 +40,25 @@ Parameters (the graphing node below can help visualize these parameters):
|
||||
- `smooth`: (true/false), do you want the adjustment curve to be smooth or not.
|
||||
- `cfg_scale_override`: if set to 0 (default), the sampler will automatically determine the CFG scale (if possible). Set to some other value to override (should probably match the CFG used in your workflow).
|
||||
|
||||
### Detail Daemon Sampler GUI
|
||||
|
||||
<img width="630" height="866" alt="image" src="https://github.com/user-attachments/assets/ce6feab8-9d96-4055-b6aa-5e203dd825f6" />
|
||||
|
||||
Combines the Detail Daemon sampler with a live, interactive schedule graph. Connect both `sampler` and `sigmas`; the node outputs the wrapped sampler followed by the unchanged sigmas.
|
||||
|
||||
- Drag the blue start handle to edit `start` and `start_offset`.
|
||||
- Drag either purple `E` handle vertically to edit `exponent`; both handles stay synchronized.
|
||||
- Drag the yellow peak handle to edit `bias` and `detail_amount`.
|
||||
- Drag the red end handle to edit `end` and `end_offset`.
|
||||
- Editing any numeric widget redraws the graph immediately.
|
||||
- Double-click the graph to reset the curve controls to their defaults.
|
||||
|
||||
The horizontal axis is normalized from 0% to 100%, so the graph is usable before queueing and remains independent of the workflow's actual step count. Graph state is derived from the node widgets and is preserved by normal ComfyUI workflow serialization.
|
||||
|
||||
Interactive demo:
|
||||
|
||||
https://github.com/user-attachments/assets/5d6b5797-7797-4995-b7ac-56b799f41751
|
||||
|
||||
### Detail Daemon Graph Sigmas
|
||||
|
||||

|
||||
|
||||
+6
-2
@@ -1,9 +1,10 @@
|
||||
# __init__.py
|
||||
|
||||
from .detail_daemon_node import DetailDaemonSamplerNode, DetailDaemonGraphSigmasNode, MultiplySigmas, LyingSigmaSamplerNode
|
||||
from .detail_daemon_node import DetailDaemonSamplerNode, DetailDaemonSamplerGUINode, DetailDaemonGraphSigmasNode, MultiplySigmas, LyingSigmaSamplerNode
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DetailDaemonSamplerNode": DetailDaemonSamplerNode,
|
||||
"DetailDaemonSamplerGUINode": DetailDaemonSamplerGUINode,
|
||||
"DetailDaemonGraphSigmasNode": DetailDaemonGraphSigmasNode,
|
||||
"MultiplySigmas": MultiplySigmas,
|
||||
"LyingSigmaSampler": LyingSigmaSamplerNode
|
||||
@@ -11,10 +12,13 @@ NODE_CLASS_MAPPINGS = {
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DetailDaemonSamplerNode": "Detail Daemon Sampler",
|
||||
"DetailDaemonSamplerGUINode": "Detail Daemon Sampler GUI",
|
||||
"DetailDaemonGraphSigmasNode": "Detail Daemon Graph Sigmas",
|
||||
"MultiplySigmas": "Multiply Sigmas (stateless)",
|
||||
"LyingSigmaSampler": "Lying Sigma Sampler",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS"]
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
|
||||
@@ -409,6 +409,59 @@ class DetailDaemonSamplerNode:
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class DetailDaemonSamplerGUINode(DetailDaemonSamplerNode):
|
||||
DESCRIPTION = "Detail Daemon sampler with an interactive schedule graph. Drag the start, exponent, peak, and end handles to edit the sampler settings."
|
||||
RETURN_TYPES = ("SAMPLER", "SIGMAS")
|
||||
RETURN_NAMES = ("sampler", "sigmas")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict:
|
||||
sampler_inputs = DetailDaemonSamplerNode.INPUT_TYPES()["required"]
|
||||
return {
|
||||
"required": {
|
||||
"sampler": sampler_inputs["sampler"],
|
||||
"sigmas": ("SIGMAS", {"forceInput": True}),
|
||||
**{
|
||||
name: input_type
|
||||
for name, input_type in sampler_inputs.items()
|
||||
if name != "sampler"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def go(
|
||||
cls,
|
||||
sampler: object,
|
||||
sigmas,
|
||||
*,
|
||||
detail_amount,
|
||||
start,
|
||||
end,
|
||||
bias,
|
||||
exponent,
|
||||
start_offset,
|
||||
end_offset,
|
||||
fade,
|
||||
smooth,
|
||||
cfg_scale_override,
|
||||
) -> tuple:
|
||||
wrapped_sampler = DetailDaemonSamplerNode.go(
|
||||
sampler,
|
||||
detail_amount=detail_amount,
|
||||
start=start,
|
||||
end=end,
|
||||
bias=bias,
|
||||
exponent=exponent,
|
||||
start_offset=start_offset,
|
||||
end_offset=end_offset,
|
||||
fade=fade,
|
||||
smooth=smooth,
|
||||
cfg_scale_override=cfg_scale_override,
|
||||
)[0]
|
||||
return (wrapped_sampler, sigmas)
|
||||
|
||||
#MultiplySigmas Node
|
||||
class MultiplySigmas:
|
||||
@classmethod
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
const NODE_NAME = "DetailDaemonSamplerGUINode";
|
||||
const GRAPH_HEIGHT = 250;
|
||||
const MIN_WIDTH = 420;
|
||||
const PARAMETER_NAMES = [
|
||||
"detail_amount",
|
||||
"start",
|
||||
"end",
|
||||
"bias",
|
||||
"exponent",
|
||||
"start_offset",
|
||||
"end_offset",
|
||||
"fade",
|
||||
"smooth",
|
||||
];
|
||||
|
||||
function isSamplerGUINode(node) {
|
||||
return node?.comfyClass === NODE_NAME || node?.type === NODE_NAME;
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function round(value, places = 2) {
|
||||
const factor = 10 ** places;
|
||||
return Math.round(value * factor) / factor;
|
||||
}
|
||||
|
||||
function formatTooltipValue(value) {
|
||||
return Number(value.toFixed(2)).toString();
|
||||
}
|
||||
|
||||
function getHandleTooltip(drag, parameters) {
|
||||
const values = {
|
||||
start: [
|
||||
["start", parameters.start],
|
||||
["start_offset", parameters.startOffset],
|
||||
],
|
||||
exponent_start: [["exponent", parameters.exponent]],
|
||||
peak: [
|
||||
["detail_amount", parameters.detailAmount],
|
||||
["bias", parameters.bias],
|
||||
],
|
||||
exponent_end: [["exponent", parameters.exponent]],
|
||||
end: [
|
||||
["end", parameters.end],
|
||||
["end_offset", parameters.endOffset],
|
||||
],
|
||||
}[drag];
|
||||
|
||||
return values?.map(([name, value]) => `${name}:${formatTooltipValue(value)}`).join(" / ") ?? "";
|
||||
}
|
||||
|
||||
function findWidget(node, name) {
|
||||
return node.widgets?.find((widget) => widget.name === name);
|
||||
}
|
||||
|
||||
function readParameters(node) {
|
||||
const value = (name, fallback) => {
|
||||
const current = Number(findWidget(node, name)?.value);
|
||||
return Number.isFinite(current) ? current : fallback;
|
||||
};
|
||||
|
||||
const end = clamp(value("end", 0.8), 0, 1);
|
||||
const start = Math.min(clamp(value("start", 0.2), 0, 1), end);
|
||||
return {
|
||||
detailAmount: clamp(value("detail_amount", 0.1), -5, 5),
|
||||
start,
|
||||
end,
|
||||
bias: clamp(value("bias", 0.5), 0, 1),
|
||||
exponent: clamp(value("exponent", 1), 0, 10),
|
||||
startOffset: clamp(value("start_offset", 0), -1, 1),
|
||||
endOffset: clamp(value("end_offset", 0), -1, 1),
|
||||
fade: clamp(value("fade", 0), 0, 1),
|
||||
smooth: Boolean(findWidget(node, "smooth")?.value ?? true),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSchedule(parameters, steps = 121) {
|
||||
const schedule = new Float64Array(steps);
|
||||
const mid = parameters.start + parameters.bias * (parameters.end - parameters.start);
|
||||
const startIndex = Math.round(parameters.start * (steps - 1));
|
||||
const midIndex = Math.round(mid * (steps - 1));
|
||||
const endIndex = Math.round(parameters.end * (steps - 1));
|
||||
|
||||
schedule.fill(parameters.startOffset, 0, startIndex);
|
||||
for (let index = startIndex; index <= midIndex; index++) {
|
||||
const length = midIndex - startIndex;
|
||||
let value = length ? (index - startIndex) / length : 0;
|
||||
if (parameters.smooth) value = 0.5 * (1 - Math.cos(value * Math.PI));
|
||||
value **= parameters.exponent;
|
||||
schedule[index] = value * (parameters.detailAmount - parameters.startOffset) + parameters.startOffset;
|
||||
}
|
||||
for (let index = midIndex; index <= endIndex; index++) {
|
||||
const length = endIndex - midIndex;
|
||||
let value = length ? 1 - (index - midIndex) / length : 1;
|
||||
if (parameters.smooth) value = 0.5 * (1 - Math.cos(value * Math.PI));
|
||||
value **= parameters.exponent;
|
||||
schedule[index] = value * (parameters.detailAmount - parameters.endOffset) + parameters.endOffset;
|
||||
}
|
||||
schedule.fill(parameters.endOffset, endIndex + 1);
|
||||
|
||||
const fadeScale = 1 - parameters.fade;
|
||||
for (let index = 0; index < schedule.length; index++) schedule[index] *= fadeScale;
|
||||
return schedule;
|
||||
}
|
||||
|
||||
function setWidgetValue(node, name, value) {
|
||||
const widget = findWidget(node, name);
|
||||
if (!widget || Object.is(widget.value, value)) return;
|
||||
widget.value = value;
|
||||
widget.callback?.(value, app.canvas, node);
|
||||
node.graph?.setDirtyCanvas?.(true, true);
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
|
||||
function chainCallback(object, name, callback) {
|
||||
const original = object[name];
|
||||
object[name] = function (...args) {
|
||||
const result = original?.apply(this, args);
|
||||
callback.apply(this, args);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function createGraph(node) {
|
||||
if (node._detailDaemonGraph) return;
|
||||
|
||||
const root = document.createElement("div");
|
||||
root.style.cssText = 'width:100%;min-height:200px;box-sizing:border-box;padding:6px 0;overflow:hidden;'
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.style.cssText =
|
||||
'display:block;width:100%;height:100%;border:1px solid rgba(255,255,255,.16);border-radius:6px;background:#17191c;touch-action:none;cursor:default;'
|
||||
canvas.title = "Drag the start, exponent handles, peak, and end. Double-click the graph to restore the default curve.";
|
||||
root.appendChild(canvas);
|
||||
|
||||
const state = {
|
||||
drag: null,
|
||||
dragStartY: 0,
|
||||
dragStartExponent: 1,
|
||||
dragYMax: 1,
|
||||
frame: 0,
|
||||
layout: null,
|
||||
removed: false,
|
||||
};
|
||||
node._detailDaemonGraph = state;
|
||||
|
||||
const requestDraw = (force = false) => {
|
||||
if (state.removed) return;
|
||||
if (state.frame) {
|
||||
if (!force) return;
|
||||
cancelAnimationFrame(state.frame);
|
||||
state.frame = 0;
|
||||
}
|
||||
state.frame = requestAnimationFrame(() => {
|
||||
state.frame = 0;
|
||||
draw();
|
||||
});
|
||||
};
|
||||
node._detailDaemonRequestDraw = requestDraw;
|
||||
|
||||
const draw = () => {
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight;
|
||||
if (width < 1 || height < 1) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const pixelWidth = Math.round(width * dpr);
|
||||
const pixelHeight = Math.round(height * dpr);
|
||||
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
|
||||
canvas.width = pixelWidth;
|
||||
canvas.height = pixelHeight;
|
||||
}
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
context.clearRect(0, 0, width, height);
|
||||
|
||||
const parameters = readParameters(node);
|
||||
const schedule = makeSchedule(parameters);
|
||||
const fadeScale = 1 - parameters.fade;
|
||||
const peakX = parameters.start + parameters.bias * (parameters.end - parameters.start);
|
||||
const exponentStrength = 0.5 ** parameters.exponent;
|
||||
const exponentStartY = (parameters.startOffset + (parameters.detailAmount - parameters.startOffset) * exponentStrength) * fadeScale;
|
||||
const exponentEndY = (parameters.endOffset + (parameters.detailAmount - parameters.endOffset) * exponentStrength) * fadeScale;
|
||||
const handles = [
|
||||
{ name: "start", x: parameters.start, y: parameters.startOffset * fadeScale, color: "#5ac8fa" },
|
||||
{ name: "exponent_start", x: (parameters.start + peakX) / 2, y: exponentStartY, color: "#bf5af2", label: "E" },
|
||||
{ name: "peak", x: peakX, y: parameters.detailAmount * fadeScale, color: "#ffcc00" },
|
||||
{ name: "exponent_end", x: (peakX + parameters.end) / 2, y: exponentEndY, color: "#bf5af2", label: "E" },
|
||||
{ name: "end", x: parameters.end, y: parameters.endOffset * fadeScale, color: "#ff6b6b" },
|
||||
];
|
||||
const largest = Math.max(1, ...schedule.map(Math.abs), ...handles.map((handle) => Math.abs(handle.y)));
|
||||
const yMax = state.drag ? state.dragYMax : Math.min(5.5, Math.max(1, largest * 1.2));
|
||||
const padding = { left: 42, right: 12, top: 25, bottom: 27 };
|
||||
const plotWidth = Math.max(1, width - padding.left - padding.right);
|
||||
const plotHeight = Math.max(1, height - padding.top - padding.bottom);
|
||||
const pointX = (value) => padding.left + value * plotWidth;
|
||||
const pointY = (value) => padding.top + (1 - (value + yMax) / (2 * yMax)) * plotHeight;
|
||||
state.layout = { padding, plotWidth, plotHeight, yMax, handles };
|
||||
|
||||
context.fillStyle = "#17191c";
|
||||
context.fillRect(0, 0, width, height);
|
||||
context.font = "11px sans-serif";
|
||||
context.lineWidth = 1;
|
||||
|
||||
for (let tick = 0; tick <= 4; tick++) {
|
||||
const ratio = tick / 4;
|
||||
const x = pointX(ratio);
|
||||
context.strokeStyle = "rgba(255,255,255,.09)";
|
||||
context.beginPath();
|
||||
context.moveTo(x, padding.top);
|
||||
context.lineTo(x, padding.top + plotHeight);
|
||||
context.stroke();
|
||||
context.fillStyle = "rgba(255,255,255,.55)";
|
||||
context.textAlign = "center";
|
||||
context.fillText(`${Math.round(ratio * 100)}%`, x, height - 8);
|
||||
}
|
||||
|
||||
for (let tick = -2; tick <= 2; tick++) {
|
||||
const value = tick * yMax / 2;
|
||||
const y = pointY(value);
|
||||
context.strokeStyle = tick === 0 ? "rgba(255,255,255,.28)" : "rgba(255,255,255,.09)";
|
||||
context.beginPath();
|
||||
context.moveTo(padding.left, y);
|
||||
context.lineTo(padding.left + plotWidth, y);
|
||||
context.stroke();
|
||||
context.fillStyle = "rgba(255,255,255,.55)";
|
||||
context.textAlign = "right";
|
||||
context.fillText(value.toFixed(yMax < 1 ? 2 : 1), padding.left - 6, y + 4);
|
||||
}
|
||||
|
||||
const gradient = context.createLinearGradient(padding.left, 0, padding.left + plotWidth, 0);
|
||||
gradient.addColorStop(0, "#5ac8fa");
|
||||
gradient.addColorStop(0.5, "#ffcc00");
|
||||
gradient.addColorStop(1, "#ff6b6b");
|
||||
context.strokeStyle = gradient;
|
||||
context.lineWidth = 2.5;
|
||||
context.beginPath();
|
||||
schedule.forEach((value, index) => {
|
||||
const x = pointX(index / (schedule.length - 1));
|
||||
const y = pointY(value);
|
||||
if (index === 0) context.moveTo(x, y);
|
||||
else context.lineTo(x, y);
|
||||
});
|
||||
context.stroke();
|
||||
|
||||
for (const handle of handles) {
|
||||
const x = pointX(handle.x);
|
||||
const y = pointY(handle.y);
|
||||
handle.canvasX = x;
|
||||
handle.canvasY = y;
|
||||
context.fillStyle = handle.color;
|
||||
context.strokeStyle = "#101214";
|
||||
context.lineWidth = 2;
|
||||
context.beginPath();
|
||||
context.arc(x, y, state.drag === handle.name ? 7 : 6, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.stroke();
|
||||
if (handle.label) {
|
||||
context.fillStyle = "#fff";
|
||||
context.font = "bold 9px sans-serif";
|
||||
context.textAlign = "center";
|
||||
context.fillText(handle.label, x, y + 3);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.drag) {
|
||||
const activeHandle = handles.find((handle) => handle.name === state.drag);
|
||||
const tooltip = getHandleTooltip(state.drag, parameters);
|
||||
if (activeHandle && tooltip) {
|
||||
context.font = "bold 11px sans-serif";
|
||||
const tooltipPadding = 8;
|
||||
const tooltipHeight = 24;
|
||||
const tooltipWidth = context.measureText(tooltip).width + tooltipPadding * 2;
|
||||
const tooltipGap = 12;
|
||||
const tooltipX = clamp(activeHandle.canvasX - tooltipWidth / 2, 4, width - tooltipWidth - 4);
|
||||
let tooltipY = activeHandle.canvasY - tooltipHeight - tooltipGap;
|
||||
if (tooltipY < 4) tooltipY = activeHandle.canvasY + tooltipGap;
|
||||
tooltipY = clamp(tooltipY, 4, height - tooltipHeight - 4);
|
||||
|
||||
context.fillStyle = "rgba(8,10,12,.94)";
|
||||
context.strokeStyle = activeHandle.color;
|
||||
context.lineWidth = 1;
|
||||
context.beginPath();
|
||||
if (typeof context.roundRect === "function") context.roundRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight, 5);
|
||||
else context.rect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
|
||||
context.fill();
|
||||
context.stroke();
|
||||
|
||||
context.fillStyle = "rgba(255,255,255,.92)";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.fillText(tooltip, tooltipX + tooltipWidth / 2, tooltipY + tooltipHeight / 2);
|
||||
context.textBaseline = "alphabetic";
|
||||
}
|
||||
}
|
||||
|
||||
context.fillStyle = "rgba(255,255,255,.78)";
|
||||
context.textAlign = "left";
|
||||
context.font = "12px sans-serif";
|
||||
context.fillText("Detail adjustment schedule", padding.left, 16);
|
||||
context.textAlign = "right";
|
||||
context.fillStyle = "rgba(255,255,255,.48)";
|
||||
context.font = "10px sans-serif";
|
||||
context.fillText("drag handles", width - padding.right, 16);
|
||||
};
|
||||
|
||||
const pointerPosition = (event) => {
|
||||
const bounds = canvas.getBoundingClientRect();
|
||||
const scaleX = bounds.width ? canvas.clientWidth / bounds.width : 1;
|
||||
const scaleY = bounds.height ? canvas.clientHeight / bounds.height : 1;
|
||||
return {
|
||||
x: (event.clientX - bounds.left) * scaleX,
|
||||
y: (event.clientY - bounds.top) * scaleY,
|
||||
};
|
||||
};
|
||||
|
||||
const updateFromPointer = (event) => {
|
||||
if (!state.drag || !state.layout) return;
|
||||
const position = pointerPosition(event);
|
||||
const { padding, plotWidth, plotHeight, yMax } = state.layout;
|
||||
const x = clamp((position.x - padding.left) / plotWidth, 0, 1);
|
||||
const displayedY = clamp((1 - (position.y - padding.top) / plotHeight) * 2 * yMax - yMax, -yMax, yMax);
|
||||
const parameters = readParameters(node);
|
||||
const fadeScale = Math.max(0.01, 1 - parameters.fade);
|
||||
|
||||
if (state.drag.startsWith("exponent_")) {
|
||||
const delta = (position.y - state.dragStartY) / plotHeight * 10;
|
||||
const exponent = Math.round(clamp(state.dragStartExponent + delta, 0, 10) / 0.05) * 0.05;
|
||||
setWidgetValue(node, "exponent", round(exponent));
|
||||
} else if (state.drag === "start") {
|
||||
setWidgetValue(node, "start", round(clamp(x, 0, parameters.end)));
|
||||
setWidgetValue(node, "start_offset", round(clamp(displayedY / fadeScale, -1, 1)));
|
||||
} else if (state.drag === "peak") {
|
||||
const span = parameters.end - parameters.start;
|
||||
const bias = span > 0 ? (x - parameters.start) / span : 0;
|
||||
setWidgetValue(node, "bias", round(clamp(bias, 0, 1)));
|
||||
setWidgetValue(node, "detail_amount", round(clamp(displayedY / fadeScale, -5, 5)));
|
||||
} else if (state.drag === "end") {
|
||||
setWidgetValue(node, "end", round(clamp(x, parameters.start, 1)));
|
||||
setWidgetValue(node, "end_offset", round(clamp(displayedY / fadeScale, -1, 1)));
|
||||
}
|
||||
requestDraw();
|
||||
};
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
if (event.button !== 0 || !state.layout) return;
|
||||
const position = pointerPosition(event);
|
||||
let closest = null;
|
||||
let distance = 14;
|
||||
for (const handle of state.layout.handles) {
|
||||
const current = Math.hypot(position.x - handle.canvasX, position.y - handle.canvasY);
|
||||
if (current < distance) {
|
||||
closest = handle;
|
||||
distance = current;
|
||||
}
|
||||
}
|
||||
if (!closest) return;
|
||||
node.graph?.beforeChange?.();
|
||||
state.drag = closest.name;
|
||||
state.dragStartY = position.y;
|
||||
state.dragStartExponent = readParameters(node).exponent;
|
||||
state.dragYMax = state.layout.yMax;
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
canvas.style.cursor = "grabbing";
|
||||
requestDraw();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
if (state.drag) {
|
||||
updateFromPointer(event);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (!state.layout) return;
|
||||
const position = pointerPosition(event);
|
||||
canvas.style.cursor = state.layout.handles.some((handle) => Math.hypot(position.x - handle.canvasX, position.y - handle.canvasY) < 14) ? "grab" : "default";
|
||||
});
|
||||
const endDrag = (event) => {
|
||||
if (!state.drag) return;
|
||||
state.drag = null;
|
||||
canvas.style.cursor = "default";
|
||||
try {
|
||||
canvas.releasePointerCapture(event.pointerId);
|
||||
} catch (_) {
|
||||
}
|
||||
node.graph?.afterChange?.();
|
||||
node.graph?.change?.();
|
||||
requestDraw();
|
||||
};
|
||||
canvas.addEventListener("pointerup", endDrag);
|
||||
canvas.addEventListener("pointercancel", endDrag);
|
||||
canvas.addEventListener("dblclick", (event) => {
|
||||
node.graph?.beforeChange?.();
|
||||
setWidgetValue(node, "detail_amount", 0.1);
|
||||
setWidgetValue(node, "start", 0.2);
|
||||
setWidgetValue(node, "end", 0.8);
|
||||
setWidgetValue(node, "bias", 0.5);
|
||||
setWidgetValue(node, "exponent", 1);
|
||||
setWidgetValue(node, "start_offset", 0);
|
||||
setWidgetValue(node, "end_offset", 0);
|
||||
setWidgetValue(node, "fade", 0);
|
||||
setWidgetValue(node, "smooth", true);
|
||||
node.graph?.afterChange?.();
|
||||
node.graph?.change?.();
|
||||
requestDraw();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
for (const name of PARAMETER_NAMES) {
|
||||
const widget = findWidget(node, name);
|
||||
if (widget) chainCallback(widget, "callback", requestDraw);
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(requestDraw);
|
||||
resizeObserver.observe(root);
|
||||
const graphWidget = node.addDOMWidget("detail_daemon_schedule", "detail_daemon_schedule", root, {
|
||||
serialize: false,
|
||||
getMinHeight: () => GRAPH_HEIGHT,
|
||||
getMaxHeight: () => GRAPH_HEIGHT,
|
||||
});
|
||||
graphWidget.serialize = false;
|
||||
graphWidget.options.serialize = false;
|
||||
|
||||
chainCallback(node, "onConfigure", () => requestDraw(true));
|
||||
chainCallback(node, "onRemoved", () => {
|
||||
state.removed = true;
|
||||
resizeObserver.disconnect();
|
||||
if (state.frame) cancelAnimationFrame(state.frame);
|
||||
node._detailDaemonGraph = null;
|
||||
node._detailDaemonRequestDraw = null;
|
||||
});
|
||||
|
||||
if (!app.configuringGraph && node.size?.[0] < MIN_WIDTH) {
|
||||
const computed = node.computeSize?.() ?? node.size;
|
||||
node.setSize?.([MIN_WIDTH, Math.max(node.size[1], computed?.[1] ?? 0)]);
|
||||
}
|
||||
requestDraw();
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "DetailDaemon.SamplerGUI",
|
||||
|
||||
beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name !== NODE_NAME) return;
|
||||
|
||||
const originalCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = originalCreated?.apply(this, arguments);
|
||||
createGraph(this);
|
||||
return result;
|
||||
};
|
||||
},
|
||||
|
||||
loadedGraphNode(node) {
|
||||
if (!isSamplerGUINode(node)) return;
|
||||
setTimeout(() => node._detailDaemonRequestDraw?.(true), 0);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user