Initial release of Niutonian Themes

This commit is contained in:
Niutonian
2026-01-02 09:54:53 +01:00
commit 456e7d1820
14 changed files with 893 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Niutonian
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+159
View File
@@ -0,0 +1,159 @@
# ComfyUI-Niutonian-Themes
A professional theme pack for ComfyUI that transforms the node editor with modern, visually stunning styles while preserving full functionality.
![Modern Dark Theme](assets/MODERN_DARK.png)
## Features
- **10 Unique Themes** - From minimal to cyberpunk, find your perfect style
- **Non-invasive** - Works with all existing nodes and extensions
- **Execution Glow** - Currently running nodes glow with theme-matched colors
- **Progress Bar** - Themed progress indicator for nodes like KSampler
- **Keyboard Shortcuts** - Quick theme switching with Alt+1 through Alt+0
- **Persistent Selection** - Your theme choice is saved automatically
## Available Themes
### Modern Dark (Alt+1)
![Modern Dark](assets/MODERN_DARK.png)
### Glassmorphism (Alt+2)
![Glassmorphism](assets/GLASSMORPHISM.png)
### Neon Glow (Alt+3)
![Neon Glow](assets/NEON_GLOW.png)
### Minimal Clean (Alt+4)
![Minimal Clean](assets/MINIMAL_CLEAN.png)
### Ocean Deep (Alt+5)
![Ocean Deep](assets/OCEAN_DEEP.png)
### Sunset Warm (Alt+6)
![Sunset Warm](assets/SUNSET_WARM.png)
### Cyberpunk (Alt+7)
![Cyberpunk](assets/CYBERPUNK.png)
### Forest Night (Alt+8)
![Forest Night](assets/FOREST_NIGHT.png)
### Midnight Purple (Alt+9)
![Midnight Purple](assets/MIDNIGHT_PURPLE.png)
### Ember Glow (Alt+0)
![Ember Glow](assets/EMBER_GLOW.png)
## Installation
### Method 1: ComfyUI Manager (Recommended)
1. Open ComfyUI Manager
2. Search for "Niutonian Themes"
3. Click Install
### Method 2: Manual Installation
1. Navigate to your ComfyUI custom nodes folder:
```
ComfyUI/custom_nodes/
```
2. Clone this repository:
```bash
git clone https://github.com/YOUR_USERNAME/ComfyUI-Niutonian-Themes.git
```
3. Restart ComfyUI
### Method 3: Download ZIP
1. Download the ZIP file from the releases page
2. Extract to `ComfyUI/custom_nodes/ComfyUI-Niutonian-Themes`
3. Restart ComfyUI
## Usage
1. Right-click on the canvas to open the context menu
2. Select **Niutonian Theme**
3. Choose your preferred theme from the submenu
Or use keyboard shortcuts:
- **Alt+1** through **Alt+9**: Select themes 1-9
- **Alt+0**: Select theme 10 (Ember Glow)
## Theme Customization
Each theme includes:
- Node background colors
- Title bar styling with accent gradients
- Border colors (normal and selected states)
- Shadow effects
- Execution glow colors
- Corner radius settings
- Special effects (glass, glow, scanlines)
## Customization
Edit `js/node_styles.js` to:
### Add Custom Style Packs
```javascript
const STYLE_PACKS = {
myCustomPack: {
name: "My Custom Pack",
node_bg: "#1a1a2e",
node_selected: "#252545",
node_title_bg: "#16213e",
node_title_color: "#ffffff",
border_color: "#0f3460",
border_selected: "#e94560",
shadow_color: "rgba(0,0,0,0.5)",
shadow_size: 12,
corner_radius: 8,
executing_color: "#e94560",
glass: false,
glow: false,
scanlines: false,
},
// ... existing themes
};
```
### Customize Node Type Colors
```javascript
const NODE_ACCENTS = {
"Load": "#4ecdc4",
"MyCustomNode": "#ff00ff",
"Checkpoint": "#f7b731",
// ... add your custom node types
"default": "#778ca3",
};
```
### Available Theme Properties
- `node_bg`: Main node background color
- `node_selected`: Selected node background color
- `node_title_bg`: Title bar background color
- `node_title_color`: Title text color
- `border_color`: Normal border color
- `border_selected`: Selected border color
- `shadow_color`: Drop shadow color
- `shadow_size`: Shadow blur radius
- `corner_radius`: Border radius for rounded corners
- `executing_color`: Color when node is running
- `glass`: Enable glass effect (boolean)
- `glow`: Enable glow effect for selected nodes (boolean)
- `scanlines`: Enable scanline effect (boolean)
## Contributing
Contributions are welcome! Feel free to:
- Submit bug reports
- Propose new themes
- Improve existing themes
- Add new features
## License
MIT License - Feel free to use, modify, and distribute.
## Credits
Created by Niutonian
+5
View File
@@ -0,0 +1,5 @@
# Niutonian Node Styles - ComfyUI custom node (frontend-only extension)
# Drop this folder into ComfyUI/custom_nodes/
# Restart ComfyUI to load the web extension.
WEB_DIRECTORY = "./js"
__all__ = ["WEB_DIRECTORY"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 KiB

+708
View File
@@ -0,0 +1,708 @@
// Niutonian Node Styles - Professional ComfyUI Theme
// Non-invasive styling that preserves LiteGraph layout
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
const NS = "niutonian_node_styles";
const STORAGE_KEY = `${NS}.stylePack`;
// ═══════════════════════════════════════════════════════════════════════════════
// STYLE PACKS - Only colors and visual effects, no layout changes
// ═══════════════════════════════════════════════════════════════════════════════
const STYLE_PACKS = {
modern: {
name: "Modern Dark",
node_bg: "#1a1a2e",
node_selected: "#252545",
node_title_bg: "#16213e",
node_title_color: "#ffffff",
border_color: "#0f3460",
border_selected: "#e94560",
shadow_color: "rgba(0,0,0,0.5)",
shadow_size: 12,
corner_radius: 8,
executing_color: "#e94560",
},
glass: {
name: "Glassmorphism",
node_bg: "#1e1e2d",
node_selected: "#2a2a40",
node_title_bg: "#2d2d44",
node_title_color: "#ffffff",
border_color: "rgba(255,255,255,0.15)",
border_selected: "#00d4ff",
shadow_color: "rgba(0,0,0,0.4)",
shadow_size: 20,
corner_radius: 12,
glass: true,
executing_color: "#00d4ff",
},
neon: {
name: "Neon Glow",
node_bg: "#0d0d0d",
node_selected: "#1a1a1a",
node_title_bg: "#151515",
node_title_color: "#ffffff",
border_color: "#333333",
border_selected: "#ff00ff",
shadow_color: "rgba(255,0,255,0.4)",
shadow_size: 25,
corner_radius: 6,
glow: true,
executing_color: "#ff00ff",
},
minimal: {
name: "Minimal Clean",
node_bg: "#2d2d2d",
node_selected: "#3d3d3d",
node_title_bg: "#383838",
node_title_color: "#e0e0e0",
border_color: "#4a4a4a",
border_selected: "#ffffff",
shadow_color: "rgba(0,0,0,0.25)",
shadow_size: 6,
corner_radius: 4,
executing_color: "#ffffff",
},
ocean: {
name: "Ocean Deep",
node_bg: "#0a192f",
node_selected: "#112240",
node_title_bg: "#1d3557",
node_title_color: "#ccd6f6",
border_color: "#457b9d",
border_selected: "#64ffda",
shadow_color: "rgba(0,0,0,0.5)",
shadow_size: 15,
corner_radius: 10,
executing_color: "#64ffda",
},
sunset: {
name: "Sunset Warm",
node_bg: "#1a1423",
node_selected: "#2d1f3d",
node_title_bg: "#3d2953",
node_title_color: "#ffeaa7",
border_color: "#6b4984",
border_selected: "#ff6b6b",
shadow_color: "rgba(0,0,0,0.4)",
shadow_size: 14,
corner_radius: 10,
executing_color: "#ff6b6b",
},
cyberpunk: {
name: "Cyberpunk",
node_bg: "#0a0a0f",
node_selected: "#12121a",
node_title_bg: "#1a1a25",
node_title_color: "#00ffff",
border_color: "#ff00ff",
border_selected: "#00ffff",
shadow_color: "rgba(255,0,255,0.5)",
shadow_size: 20,
corner_radius: 2,
glow: true,
scanlines: true,
executing_color: "#00ffff",
},
forest: {
name: "Forest Night",
node_bg: "#1a2f1a",
node_selected: "#2a4a2a",
node_title_bg: "#1f3d1f",
node_title_color: "#c8e6c9",
border_color: "#2e7d32",
border_selected: "#81c784",
shadow_color: "rgba(0,0,0,0.45)",
shadow_size: 14,
corner_radius: 8,
executing_color: "#69f0ae",
},
midnight: {
name: "Midnight Purple",
node_bg: "#1a1025",
node_selected: "#2d1b40",
node_title_bg: "#251538",
node_title_color: "#e1bee7",
border_color: "#4a148c",
border_selected: "#ce93d8",
shadow_color: "rgba(74,20,140,0.4)",
shadow_size: 16,
corner_radius: 10,
glow: true,
executing_color: "#ea80fc",
},
ember: {
name: "Ember Glow",
node_bg: "#1f1410",
node_selected: "#3d2518",
node_title_bg: "#2a1a12",
node_title_color: "#ffccbc",
border_color: "#bf360c",
border_selected: "#ff6e40",
shadow_color: "rgba(191,54,12,0.35)",
shadow_size: 18,
corner_radius: 6,
glow: true,
executing_color: "#ff9e80",
},
};
// Node type accent colors
const NODE_ACCENTS = {
"Load": "#4ecdc4",
"Checkpoint": "#f7b731",
"LoRA": "#fed330",
"CLIP": "#a55eea",
"VAE": "#20bf6b",
"KSampler": "#eb3b5a",
"Sampler": "#fc5c65",
"ControlNet": "#8854d0",
"IPAdapter": "#3867d6",
"Image": "#0fb9b1",
"Preview": "#2bcbba",
"Save": "#45aaf2",
"Latent": "#f368e0",
"Conditioning": "#a55eea",
"Encode": "#8854d0",
"Mask": "#fa8231",
"default": "#778ca3",
};
function getAccent(node) {
const type = node?.type || "";
for (const [key, color] of Object.entries(NODE_ACCENTS)) {
if (key !== "default" && type.includes(key)) return color;
}
return NODE_ACCENTS.default;
}
function getPackId() {
return localStorage.getItem(STORAGE_KEY) || "modern";
}
function setPackId(id) {
localStorage.setItem(STORAGE_KEY, id);
}
function getPack() {
return STYLE_PACKS[getPackId()] || STYLE_PACKS.modern;
}
// ═══════════════════════════════════════════════════════════════════════════════
// APPLY THEME BY MODIFYING LITEGRAPH DEFAULTS (non-invasive)
// ═══════════════════════════════════════════════════════════════════════════════
let applied = false;
function applyTheme() {
const LiteGraph = globalThis?.LiteGraph;
const LGraphCanvas = globalThis?.LGraphCanvas;
const LGraphNode = globalThis?.LGraphNode;
if (!LiteGraph || !LGraphCanvas || applied) return false;
applied = true;
const pack = getPack();
// Apply to LiteGraph defaults - this affects all nodes without breaking layout
LiteGraph.NODE_DEFAULT_COLOR = pack.node_bg;
LiteGraph.NODE_DEFAULT_BGCOLOR = pack.node_bg;
LiteGraph.NODE_DEFAULT_BOXCOLOR = "#ffffff"; // White box for visibility
LiteGraph.NODE_TITLE_COLOR = "#ffffff"; // Force white title text for visibility
LiteGraph.NODE_SELECTED_TITLE_COLOR = "#ffffff"; // Ensure selected title is visible
LiteGraph.NODE_TEXT_COLOR = "#ffffff"; // White text
LiteGraph.NODE_DEFAULT_SHAPE = "box";
// Also set the canvas title color
if (LGraphCanvas.prototype) {
LGraphCanvas.prototype.node_title_color = "#ffffff";
}
// Widget colors
LiteGraph.WIDGET_BGCOLOR = "rgba(0,0,0,0.3)";
LiteGraph.WIDGET_OUTLINE_COLOR = pack.border_color;
LiteGraph.WIDGET_TEXT_COLOR = "#ddd";
LiteGraph.WIDGET_SECONDARY_TEXT_COLOR = "#999";
// Link colors
LiteGraph.LINK_COLOR = "#9A9";
LiteGraph.EVENT_LINK_COLOR = "#A86";
LiteGraph.CONNECTING_LINK_COLOR = "#AFA";
// Override the running stroke style color for all nodes
// This hooks into ComfyUI's strokeStyles system
if (LGraphNode && LGraphNode.prototype) {
const originalOnAdded = LGraphNode.prototype.onAdded;
LGraphNode.prototype.onAdded = function(graph) {
if (originalOnAdded) {
originalOnAdded.call(this, graph);
}
// Override the running stroke style to use our theme color
if (this.strokeStyles && this.strokeStyles['running']) {
const originalRunning = this.strokeStyles['running'];
const execColor = getPack().executing_color || "#00ff88";
this.strokeStyles['running'] = function() {
const result = originalRunning.call(this);
if (result) {
return { color: execColor, lineWidth: 3 };
}
return result;
};
}
};
}
// Override drawTitleBarBackground to NOT draw a separate header
// This gives a cleaner unified look - the title text will draw on top of our node background
const originalDrawTitleBarBackground = LGraphNode.prototype.drawTitleBarBackground;
LGraphNode.prototype.drawTitleBarBackground = function(ctx, options = {}) {
const pack = getPack();
const accent = getAccent(this);
const titleH = options.title_height || LiteGraph.NODE_TITLE_HEIGHT || 30;
const w = this.size[0];
// Skip if transparent or no title
if (this.title_mode === LiteGraph.TRANSPARENT_TITLE) {
return;
}
// For collapsed nodes, we still need to draw the background
if (this.flags?.collapsed) {
const r = Math.min(pack.corner_radius, 12);
ctx.save();
ctx.beginPath();
ctx.roundRect(0, -titleH, w, titleH, r);
ctx.fillStyle = pack.node_title_bg;
if (LiteGraph.DEFAULT_SHADOW_COLOR) {
ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR;
}
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.restore();
return;
}
// For non-collapsed: just draw a subtle accent line at the bottom of title area
// The node background is already drawn by drawNodeShape
ctx.save();
ctx.fillStyle = accent;
ctx.fillRect(0, -2, w, 2);
ctx.restore();
};
// Store original drawNodeShape
const originalDrawNodeShape = LGraphCanvas.prototype.drawNodeShape;
// Override drawNodeShape for custom styling while preserving layout
LGraphCanvas.prototype.drawNodeShape = function(node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
const pack = getPack();
const accent = getAccent(node);
// Detect if node is currently executing
let isExecuting = false;
try {
// Check our custom flag first (set by API events)
if (node.is_executing === true) {
isExecuting = true;
}
// Also check ComfyUI's strokeStyles if available
if (!isExecuting && node.strokeStyles && typeof node.strokeStyles['running'] === 'function') {
const runningResult = node.strokeStyles['running'].call(node);
if (runningResult) {
isExecuting = true;
}
}
} catch(e) {}
// Get the node's bounding rect (includes title area)
// LiteGraph draws with body at (0,0) and title at negative Y
const titleMode = node.title_mode ?? node.constructor.title_mode ?? LiteGraph.NORMAL_TITLE;
const renderTitle = titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE;
const collapsed = node.flags?.collapsed;
const titleH = renderTitle ? (LiteGraph.NODE_TITLE_HEIGHT || 30) : 0;
// Body dimensions (what LiteGraph passes as size)
const w = size[0];
const h = size[1];
const r = Math.min(pack.corner_radius, 12);
// Full node area including title (title is at negative Y)
const fullY = renderTitle ? -titleH : 0;
const fullH = renderTitle ? h + titleH : h;
ctx.save();
// === SHADOW ===
const execColor = pack.executing_color || "#00ff88";
if (pack.shadow_size > 0) {
if (isExecuting) {
ctx.shadowColor = execColor;
ctx.shadowBlur = pack.shadow_size * 3;
} else if (pack.glow && selected) {
ctx.shadowColor = accent;
ctx.shadowBlur = pack.shadow_size * 1.5;
} else {
ctx.shadowColor = pack.shadow_color;
ctx.shadowBlur = pack.shadow_size;
}
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = isExecuting ? 0 : (selected ? 0 : 4);
}
// === FULL NODE BACKGROUND (title + body) ===
ctx.beginPath();
if (collapsed) {
// Collapsed node - just the title bar
ctx.roundRect(0, fullY, w, titleH, r);
} else {
ctx.roundRect(0, fullY, w, fullH, r);
}
if (isExecuting) {
ctx.fillStyle = pack.node_selected;
} else {
ctx.fillStyle = selected ? pack.node_selected : pack.node_bg;
}
ctx.fill();
// Clear shadow for rest
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
// === GLASS EFFECT ===
if (pack.glass && !collapsed) {
const grad = ctx.createLinearGradient(0, fullY, 0, fullY + fullH);
grad.addColorStop(0, "rgba(255,255,255,0.08)");
grad.addColorStop(0.3, "rgba(255,255,255,0.02)");
grad.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.roundRect(0, fullY, w, fullH, r);
ctx.fill();
}
// Note: Title bar is drawn by drawTitleBarBackground which we override
// to use our theme colors. We don't draw it here to avoid double-drawing.
// === BORDER ===
ctx.beginPath();
if (collapsed) {
ctx.roundRect(0.5, fullY + 0.5, w - 1, titleH - 1, r);
} else {
ctx.roundRect(0.5, fullY + 0.5, w - 1, fullH - 1, r);
}
if (isExecuting) {
ctx.strokeStyle = execColor;
ctx.lineWidth = 3;
} else {
ctx.strokeStyle = selected ? pack.border_selected : pack.border_color;
ctx.lineWidth = selected ? 2 : 1;
}
ctx.stroke();
// === GLOW FOR SELECTED (neon theme) ===
if (selected && pack.glow && !isExecuting) {
ctx.shadowColor = pack.border_selected;
ctx.shadowBlur = 20;
ctx.beginPath();
ctx.roundRect(0, fullY, w, collapsed ? titleH : fullH, r);
ctx.strokeStyle = pack.border_selected;
ctx.lineWidth = 2;
ctx.stroke();
}
// === EXECUTING GLOW ===
if (isExecuting) {
ctx.shadowColor = execColor;
ctx.shadowBlur = 25;
ctx.beginPath();
ctx.roundRect(0, fullY, w, collapsed ? titleH : fullH, r);
ctx.strokeStyle = execColor;
ctx.lineWidth = 2;
ctx.stroke();
}
// === SCANLINES (cyberpunk theme) ===
if (pack.scanlines && !collapsed) {
ctx.save();
ctx.globalAlpha = 0.06;
ctx.fillStyle = "#00ffff";
for (let sy = fullY; sy < fullY + fullH; sy += 3) {
ctx.fillRect(0, sy, w, 1);
}
ctx.restore();
}
// === PROGRESS BAR (for executing nodes like KSampler) ===
// Draw at top of body area (Y=0)
if (node.progress != null && node.progress > 0 && !collapsed) {
const progressColor = isExecuting ? execColor : accent;
ctx.save();
ctx.fillStyle = progressColor;
ctx.globalAlpha = 0.9;
ctx.fillRect(0, 0, w * node.progress, 6);
// Add a subtle glow effect
ctx.shadowColor = progressColor;
ctx.shadowBlur = 8;
ctx.fillRect(0, 0, w * node.progress, 4);
ctx.restore();
}
ctx.restore();
// CRITICAL: Call onDrawBackground for image/video previews
// This is where ComfyUI renders images in Preview/Save nodes
if (node.onDrawBackground) {
node.onDrawBackground(ctx);
}
// Draw title bar, title box, and title text
// These are normally called by the original drawNodeShape after onDrawBackground
const shouldRenderTitle = renderTitle; // reuse the variable from above
if (shouldRenderTitle || titleMode === LiteGraph.TRANSPARENT_TITLE) {
// Draw title bar background (our themed version)
node.drawTitleBarBackground(ctx, {
scale: this.ds?.scale || 1,
low_quality: this.low_quality || false
});
// Draw title box (collapse button)
node.drawTitleBox(ctx, {
scale: this.ds?.scale || 1,
low_quality: this.low_quality || false,
box_size: 10
});
// Draw title text
node.drawTitleText(ctx, {
scale: this.ds?.scale || 1,
default_title_color: this.node_title_color || "#ffffff",
low_quality: this.low_quality || false
});
// Custom title render callback
if (node.onDrawTitle) {
node.onDrawTitle(ctx);
}
}
};
console.log("[NiutonianNodeStyles] ✨ Theme applied:", pack.name);
return true;
}
// Re-apply theme when pack changes
function reapplyTheme() {
const LiteGraph = globalThis?.LiteGraph;
const LGraphCanvas = globalThis?.LGraphCanvas;
const pack = getPack();
// Update LiteGraph color constants for the new theme
if (LiteGraph) {
LiteGraph.NODE_DEFAULT_COLOR = pack.node_bg;
LiteGraph.NODE_DEFAULT_BGCOLOR = pack.node_bg;
LiteGraph.NODE_DEFAULT_BOXCOLOR = "#ffffff";
LiteGraph.NODE_TITLE_COLOR = "#ffffff"; // Force white for visibility
LiteGraph.NODE_SELECTED_TITLE_COLOR = "#ffffff";
LiteGraph.NODE_TEXT_COLOR = "#ffffff";
LiteGraph.WIDGET_OUTLINE_COLOR = pack.border_color;
}
// Update canvas title color
if (LGraphCanvas?.prototype) {
LGraphCanvas.prototype.node_title_color = "#ffffff";
}
// Also update the active canvas instance
if (app.canvas) {
app.canvas.node_title_color = "#ffffff";
}
// Force all nodes to recalculate their size to match the new theme
try {
if (app.graph && app.graph._nodes) {
for (const node of app.graph._nodes) {
// Recalculate the node size based on current content
const newSize = node.computeSize();
// Only resize if the new size is larger (don't shrink user-resized nodes)
if (newSize[0] > node.size[0] || newSize[1] > node.size[1]) {
node.size[0] = Math.max(node.size[0], newSize[0]);
node.size[1] = Math.max(node.size[1], newSize[1]);
}
// Mark node as dirty to force redraw
node.setDirtyCanvas(true, true);
}
}
} catch(e) {
console.warn("[NiutonianNodeStyles] Could not resize nodes:", e);
}
// Force redraw
try {
app.canvas?.setDirty(true, true);
app.graph?.setDirtyCanvas(true, true);
globalThis.LGraphCanvas?.active_canvas?.setDirty(true, true);
} catch(e) {}
}
// ═══════════════════════════════════════════════════════════════════════════════
// MENU
// ═══════════════════════════════════════════════════════════════════════════════
function buildMenu() {
const currentPack = getPackId();
return [
null,
{
content: "🎨 Niutonian Theme",
has_submenu: true,
submenu: {
options: Object.entries(STYLE_PACKS).map(([id, pack]) => ({
content: `${currentPack === id ? "✓ " : " "}${pack.name}`,
callback: () => {
setPackId(id);
reapplyTheme();
console.log("[NiutonianNodeStyles] Switched to:", pack.name);
},
})),
},
},
];
}
// ═══════════════════════════════════════════════════════════════════════════════
// EXTENSION
// ═══════════════════════════════════════════════════════════════════════════════
app.registerExtension({
name: "Niutonian.NodeStyles",
async setup() {
console.log("[NiutonianNodeStyles] 🚀 Loading...");
if (applyTheme()) return;
// Retry until LiteGraph is ready
let attempts = 0;
const interval = setInterval(() => {
attempts++;
if (applyTheme() || attempts >= 50) {
clearInterval(interval);
}
}, 200);
// Keyboard shortcuts (Alt+1 through Alt+0 for 10 themes)
document.addEventListener("keydown", (e) => {
if (e.altKey && e.key >= "0" && e.key <= "9") {
const packIds = Object.keys(STYLE_PACKS);
// Alt+1 = index 0, Alt+2 = index 1, ..., Alt+0 = index 9
const index = e.key === "0" ? 9 : parseInt(e.key) - 1;
if (index < packIds.length) {
setPackId(packIds[index]);
reapplyTheme();
}
e.preventDefault();
}
});
// Track currently executing node ID
let currentExecutingNodeId = null;
// Listen for execution events to track running nodes
api.addEventListener("executing", ({ detail }) => {
// detail is the node ID (string or null when execution completes)
currentExecutingNodeId = detail;
if (app.graph && app.graph._nodes) {
for (const node of app.graph._nodes) {
// Mark node as executing if its ID matches
node.is_executing = (detail !== null && String(node.id) === String(detail));
}
// Force immediate redraw
if (app.canvas) {
app.canvas.setDirty(true, true);
app.canvas.draw(true, true);
}
}
});
api.addEventListener("executed", ({ detail }) => {
// Clear execution state for the completed node
// detail.node contains the node ID that finished
if (app.graph && app.graph._nodes && detail?.node) {
for (const node of app.graph._nodes) {
if (String(node.id) === String(detail.node)) {
node.is_executing = false;
}
}
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
api.addEventListener("execution_start", ({ detail }) => {
// Clear all execution states at start of new prompt
currentExecutingNodeId = null;
if (app.graph && app.graph._nodes) {
for (const node of app.graph._nodes) {
node.is_executing = false;
}
}
});
api.addEventListener("execution_error", ({ detail }) => {
// Clear all execution states on error
currentExecutingNodeId = null;
if (app.graph && app.graph._nodes) {
for (const node of app.graph._nodes) {
node.is_executing = false;
}
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
// Also listen to progress events - these fire during node execution
api.addEventListener("progress", ({ detail }) => {
// detail contains: { value, max, prompt_id, node }
// The 'node' field is the currently executing node ID
if (detail?.node && app.graph && app.graph._nodes) {
const execNodeId = String(detail.node);
for (const node of app.graph._nodes) {
// Check if this node matches the executing node
// Handle both simple IDs and execution IDs (with colons for subgraphs)
const nodeIdStr = String(node.id);
node.is_executing = (nodeIdStr === execNodeId || execNodeId.endsWith(':' + nodeIdStr));
}
if (app.canvas) {
app.canvas.setDirty(true, true);
}
}
});
},
getCanvasMenuItems() {
return buildMenu();
},
});
// Also add to LiteGraph menu
setTimeout(() => {
const LGC = globalThis?.LGraphCanvas;
if (LGC) {
const orig = LGC.prototype.getCanvasMenuOptions;
LGC.prototype.getCanvasMenuOptions = function() {
return [...(orig?.call(this) || []), ...buildMenu()];
};
}
}, 2000);
export { STYLE_PACKS, getPackId, setPackId };