commit 456e7d1820d52485109ddafab7917b50bc06eb4c Author: Niutonian Date: Fri Jan 2 09:54:53 2026 +0100 Initial release of Niutonian Themes diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e716418 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e329b5 --- /dev/null +++ b/README.md @@ -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 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..75f8c75 --- /dev/null +++ b/__init__.py @@ -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"] diff --git a/assets/CYBERPUNK.png b/assets/CYBERPUNK.png new file mode 100644 index 0000000..b1a44b2 Binary files /dev/null and b/assets/CYBERPUNK.png differ diff --git a/assets/EMBER_GLOW.png b/assets/EMBER_GLOW.png new file mode 100644 index 0000000..4dc7726 Binary files /dev/null and b/assets/EMBER_GLOW.png differ diff --git a/assets/FOREST_NIGHT.png b/assets/FOREST_NIGHT.png new file mode 100644 index 0000000..dc6a198 Binary files /dev/null and b/assets/FOREST_NIGHT.png differ diff --git a/assets/GLASSMORPHISM.png b/assets/GLASSMORPHISM.png new file mode 100644 index 0000000..558e20b Binary files /dev/null and b/assets/GLASSMORPHISM.png differ diff --git a/assets/MIDNIGHT_PURPLE.png b/assets/MIDNIGHT_PURPLE.png new file mode 100644 index 0000000..95c3db1 Binary files /dev/null and b/assets/MIDNIGHT_PURPLE.png differ diff --git a/assets/MINIMAL_CLEAN.png b/assets/MINIMAL_CLEAN.png new file mode 100644 index 0000000..d1bd787 Binary files /dev/null and b/assets/MINIMAL_CLEAN.png differ diff --git a/assets/MODERN_DARK.png b/assets/MODERN_DARK.png new file mode 100644 index 0000000..64dab7c Binary files /dev/null and b/assets/MODERN_DARK.png differ diff --git a/assets/NEON_GLOW.png b/assets/NEON_GLOW.png new file mode 100644 index 0000000..32a4f75 Binary files /dev/null and b/assets/NEON_GLOW.png differ diff --git a/assets/OCEAN_DEEP.png b/assets/OCEAN_DEEP.png new file mode 100644 index 0000000..2a6a706 Binary files /dev/null and b/assets/OCEAN_DEEP.png differ diff --git a/assets/SUNSET_WARM.png b/assets/SUNSET_WARM.png new file mode 100644 index 0000000..40bc21d Binary files /dev/null and b/assets/SUNSET_WARM.png differ diff --git a/js/node_styles.js b/js/node_styles.js new file mode 100644 index 0000000..eb3593a --- /dev/null +++ b/js/node_styles.js @@ -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 };