| import { app } from "../../scripts/app.js"; |
|
|
| |
| class BaseVirtualNode extends LGraphNode { |
| constructor(title = "BaseVirtualNode") { |
| super(title); |
| this.comfyClass = "BaseVirtualNode"; |
| this.isVirtualNode = true; |
| this.properties = this.properties || {}; |
| this.widgets = this.widgets || []; |
| } |
|
|
| static setUp() { |
| if (!this.type) { |
| throw new Error(`Missing type for BaseVirtualNode: ${this.title}`); |
| } |
| LiteGraph.registerNodeType(this.type, this); |
| if (this._category) { |
| this.category = this._category; |
| } |
| } |
| } |
|
|
| |
| export class lg_note extends BaseVirtualNode { |
| constructor(title = lg_note.title) { |
| super(title); |
| this.comfyClass = "LG_Note"; |
| this.resizable = true; |
|
|
| |
| this.properties["fontSize"] = 12; |
| this.properties["fontFamily"] = "Arial"; |
| this.properties["fontColor"] = "#ffffff"; |
| this.properties["textAlign"] = "center"; |
| this.properties["backgroundColor"] = "transparent"; |
| this.properties["padding"] = 0; |
| this.properties["borderRadius"] = 0; |
| this.properties["autoScale"] = true; |
| this.properties["baseFontSize"] = 36; |
|
|
| |
| this.size = [160, 40]; |
| this.baseSize = [160, 40]; |
|
|
| this.color = "#fff0"; |
| this.bgcolor = "#fff0"; |
| } |
|
|
| |
| calculateScaledFontSize() { |
| if (!this.properties["autoScale"]) { |
| return this.properties["fontSize"]; |
| } |
|
|
| |
| const widthScale = this.size[0] / this.baseSize[0]; |
| const heightScale = this.size[1] / this.baseSize[1]; |
| const scale = (widthScale + heightScale) / 2; |
|
|
| |
| const scaledFontSize = this.properties["baseFontSize"] * scale; |
|
|
| |
| return Math.max(1, scaledFontSize); |
| } |
|
|
| draw(ctx) { |
| this.flags = this.flags || {}; |
| this.flags.allow_interaction = !this.flags.pinned; |
|
|
| ctx.save(); |
| this.color = "#fff0"; |
| this.bgcolor = "#fff0"; |
|
|
| const fontColor = this.properties["fontColor"] || "#ffffff"; |
| const backgroundColor = this.properties["backgroundColor"] || ""; |
|
|
| |
| const currentFontSize = this.calculateScaledFontSize(); |
| this.properties["fontSize"] = currentFontSize; |
|
|
| ctx.font = `${Math.max(currentFontSize || 0, 1)}px ${this.properties["fontFamily"] ?? "Arial"}`; |
|
|
| const padding = Number(this.properties["padding"]) ?? 0; |
| const lines = this.title.replace(/\n*$/, "").split("\n"); |
|
|
| |
| if (!this.properties["autoScale"]) { |
| const maxWidth = Math.max(...lines.map((s) => ctx.measureText(s).width)); |
| this.size[0] = maxWidth + padding * 2; |
| this.size[1] = currentFontSize * lines.length + padding * 2; |
| } |
|
|
| |
| if (backgroundColor) { |
| ctx.beginPath(); |
| const borderRadius = Number(this.properties["borderRadius"]) || 0; |
| ctx.roundRect(0, 0, this.size[0], this.size[1], [borderRadius]); |
| ctx.fillStyle = backgroundColor; |
| ctx.fill(); |
| } |
|
|
| |
| ctx.textAlign = "left"; |
| let textX = padding; |
| if (this.properties["textAlign"] === "center") { |
| ctx.textAlign = "center"; |
| textX = this.size[0] / 2; |
| } else if (this.properties["textAlign"] === "right") { |
| ctx.textAlign = "right"; |
| textX = this.size[0] - padding; |
| } |
|
|
| |
| ctx.textBaseline = "middle"; |
| ctx.fillStyle = fontColor; |
|
|
| const lineHeight = currentFontSize * 1.2; |
| const totalTextHeight = lines.length * lineHeight; |
|
|
| |
| let startY = (this.size[1] - totalTextHeight) / 2 + lineHeight / 2; |
|
|
| for (let i = 0; i < lines.length; i++) { |
| const currentY = startY + i * lineHeight; |
| ctx.fillText(lines[i] || " ", textX, currentY); |
| } |
|
|
| ctx.restore(); |
| } |
|
|
| onDblClick(event, pos, canvas) { |
| LGraphCanvas.active_canvas.showShowNodePanel(this); |
| } |
|
|
| onShowCustomPanelInfo(panel) { |
| |
| panel.querySelector('div.property[data-property="Mode"]')?.remove(); |
| panel.querySelector('div.property[data-property="Color"]')?.remove(); |
| panel.querySelector('div.property[data-property="baseFontSize"]')?.remove(); |
|
|
| |
| setTimeout(() => { |
| this.convertTitleToTextarea(panel); |
| this.addRealTimeCallbacks(panel); |
| this.createColorPickerRow(panel); |
| this.moveSliderPropertiesToEnd(panel); |
| this.convertToSlider(panel, "fontSize", 1, 500, 1); |
| this.convertToSlider(panel, "padding", 0, 50, 1); |
| this.convertToSlider(panel, "borderRadius", 0, 50, 1); |
| }, 10); |
| } |
|
|
| |
| convertTitleToTextarea(panel) { |
| const titleElement = panel.querySelector('div.property[data-property="Title"]'); |
| if (!titleElement) return; |
|
|
| const valueElement = titleElement.querySelector('.property_value'); |
| if (!valueElement) return; |
|
|
| |
| valueElement.removeAttribute('contenteditable'); |
|
|
| |
| const textarea = document.createElement('textarea'); |
| textarea.value = this.title || ''; |
| textarea.style.cssText = ` |
| width: 100%; |
| min-height: 60px; |
| max-height: 150px; |
| background: rgba(255,255,255,0.1); |
| border: 1px solid #555; |
| border-radius: 4px; |
| padding: 8px; |
| color: #fff; |
| font-size: 13px; |
| font-family: inherit; |
| resize: vertical; |
| outline: none; |
| box-sizing: border-box; |
| `; |
|
|
| |
| textarea.addEventListener('input', (e) => { |
| this.title = e.target.value; |
| this.setDirtyCanvas(true, true); |
| }); |
|
|
| |
| textarea.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter') { |
| |
| e.stopPropagation(); |
| } |
| }); |
|
|
| |
| textarea.addEventListener('blur', (e) => { |
| this.title = e.target.value; |
| this.setDirtyCanvas(true, true); |
| }); |
|
|
| |
| valueElement.style.display = 'none'; |
| titleElement.appendChild(textarea); |
|
|
| |
| titleElement.style.cssText = ` |
| display: block; |
| margin: 8px 0; |
| padding: 8px 0; |
| `; |
|
|
| |
| const nameElement = titleElement.querySelector('.property_name'); |
| if (nameElement) { |
| nameElement.style.cssText = ` |
| display: block; |
| margin-bottom: 6px; |
| font-weight: bold; |
| color: #ccc; |
| `; |
| } |
| } |
|
|
| |
| addRealTimeCallbacks(panel) { |
| |
| const textProperties = ["fontFamily"]; |
| textProperties.forEach(propName => { |
| const element = panel.querySelector(`div.property[data-property="${propName}"] .property_value`); |
| if (element) { |
| element.addEventListener('input', () => { |
| this.properties[propName] = element.textContent; |
| this.setDirtyCanvas(true, true); |
| }); |
| element.addEventListener('blur', () => { |
| this.properties[propName] = element.textContent; |
| this.setDirtyCanvas(true, true); |
| }); |
| } |
| }); |
|
|
| |
| const comboProperties = ["textAlign"]; |
| comboProperties.forEach(propName => { |
| const element = panel.querySelector(`div.property[data-property="${propName}"] .property_value`); |
| if (element) { |
| |
| } |
| }); |
|
|
| |
| const booleanProperties = ["autoScale"]; |
| booleanProperties.forEach(propName => { |
| const element = panel.querySelector(`div.property[data-property="${propName}"]`); |
| if (element) { |
| |
| element.addEventListener('click', () => { |
| |
| setTimeout(() => { |
| this.setDirtyCanvas(true, true); |
| |
| this.updateFontSizeSlider(panel); |
| }, 10); |
| }); |
| } |
| }); |
| } |
|
|
| |
| updateFontSizeSlider(panel) { |
| const fontSizeSlider = panel.querySelector('div.property[data-property="fontSize"] input[type="range"]'); |
| const fontSizeValue = panel.querySelector('div.property[data-property="fontSize"] .property_value'); |
|
|
| if (fontSizeSlider && fontSizeValue) { |
| let displayValue; |
| if (this.properties["autoScale"]) { |
| |
| displayValue = this.properties["baseFontSize"]; |
| } else { |
| |
| displayValue = this.properties["fontSize"]; |
| } |
|
|
| fontSizeSlider.value = displayValue.toString(); |
| fontSizeValue.textContent = displayValue.toString(); |
| } |
| } |
|
|
| |
| createColorPickerRow(panel) { |
| |
| const fontColorElement = panel.querySelector('div.property[data-property="fontColor"]'); |
| const backgroundColorElement = panel.querySelector('div.property[data-property="backgroundColor"]'); |
|
|
| if (!fontColorElement || !backgroundColorElement) return; |
|
|
| |
| const colorRow = document.createElement('div'); |
| colorRow.style.cssText = ` |
| display: flex; |
| gap: 10px; |
| margin: 10px 0; |
| padding: 8px; |
| background: rgba(255,255,255,0.05); |
| border-radius: 6px; |
| `; |
|
|
| |
| const fontColorPicker = this.createColorPicker("Font Color", this.properties.fontColor, (color) => { |
| this.properties.fontColor = color; |
| this.setDirtyCanvas(true, true); |
| }); |
|
|
| |
| const backgroundColorPicker = this.createColorPicker("Background", this.properties.backgroundColor, (color) => { |
| this.properties.backgroundColor = color; |
| this.setDirtyCanvas(true, true); |
| }); |
|
|
| colorRow.appendChild(fontColorPicker); |
| colorRow.appendChild(backgroundColorPicker); |
|
|
| |
| fontColorElement.remove(); |
| backgroundColorElement.remove(); |
|
|
| |
| const titleElement = panel.querySelector('div.property[data-property="Title"]'); |
| if (titleElement && titleElement.nextSibling) { |
| panel.content.insertBefore(colorRow, titleElement.nextSibling); |
| } else { |
| panel.content.appendChild(colorRow); |
| } |
| } |
|
|
| |
| createColorPicker(label, currentColor, callback) { |
| const container = document.createElement('div'); |
| container.style.cssText = ` |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 6px; |
| `; |
|
|
| |
| const labelElement = document.createElement('span'); |
| labelElement.textContent = label; |
| labelElement.style.cssText = ` |
| font-size: 12px; |
| color: #ccc; |
| font-weight: bold; |
| `; |
|
|
| |
| const pickerContainer = document.createElement('div'); |
| pickerContainer.style.cssText = ` |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| width: 100%; |
| `; |
|
|
| |
| const initialColor = currentColor || '#ffffff'; |
| const isHexColor = /^#[0-9A-Fa-f]{6}$/i.test(initialColor); |
|
|
| |
| const colorInput = document.createElement('input'); |
| colorInput.type = 'color'; |
| |
| colorInput.value = isHexColor ? initialColor : '#ffffff'; |
| colorInput.style.cssText = ` |
| width: 40px; |
| height: 30px; |
| border: none; |
| border-radius: 6px; |
| cursor: pointer; |
| background: none; |
| padding: 0; |
| `; |
|
|
| |
| const colorValue = document.createElement('input'); |
| colorValue.type = 'text'; |
| colorValue.value = initialColor; |
| colorValue.style.cssText = ` |
| flex: 1; |
| background: rgba(255,255,255,0.1); |
| border: 1px solid #555; |
| border-radius: 4px; |
| padding: 4px 8px; |
| color: #fff; |
| font-size: 11px; |
| font-family: monospace; |
| `; |
|
|
| |
| colorValue.placeholder = 'e.g. #ff0000, red, transparent'; |
|
|
| |
| colorInput.addEventListener('input', (e) => { |
| const color = e.target.value; |
| colorValue.value = color; |
| callback(color); |
| }); |
|
|
| |
| colorValue.addEventListener('input', (e) => { |
| const formattedColor = formatColor(e.target.value); |
| if (isValidColor(formattedColor)) { |
| |
| if (/^#[0-9A-Fa-f]{6}$/i.test(formattedColor)) { |
| colorInput.value = formattedColor; |
| } |
| callback(formattedColor); |
| } |
| }); |
|
|
| |
| const formatColor = (input) => { |
| let color = input.trim(); |
|
|
| |
| if (!color.startsWith('#') && /^[0-9A-Fa-f]{6}$/i.test(color)) { |
| color = '#' + color; |
| } |
|
|
| return color; |
| }; |
|
|
| |
| const isValidColor = (color) => { |
| |
| if (!color || color.trim() === '') return false; |
|
|
| |
| const tempElement = document.createElement('div'); |
| const originalColor = tempElement.style.color; |
|
|
| try { |
| tempElement.style.color = color; |
| |
| const isValid = tempElement.style.color !== originalColor || tempElement.style.color !== ''; |
| console.log(`Color validation: "${color}" -> ${isValid}`); |
| return isValid; |
| } catch (e) { |
| console.log(`Color validation error: "${color}" -> false`); |
| return false; |
| } |
| }; |
|
|
| |
| colorValue.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter') { |
| e.preventDefault(); |
| const formattedColor = formatColor(e.target.value); |
| if (isValidColor(formattedColor)) { |
| colorValue.value = formattedColor; |
| |
| if (/^#[0-9A-Fa-f]{6}$/i.test(formattedColor)) { |
| colorInput.value = formattedColor; |
| } |
| callback(formattedColor); |
| e.target.blur(); |
| } else { |
| |
| colorValue.value = colorInput.value; |
| } |
| } |
| }); |
|
|
| colorValue.addEventListener('blur', (e) => { |
| const formattedColor = formatColor(e.target.value); |
| if (isValidColor(formattedColor)) { |
| |
| colorValue.value = formattedColor; |
| |
| if (/^#[0-9A-Fa-f]{6}$/i.test(formattedColor)) { |
| colorInput.value = formattedColor; |
| } |
| callback(formattedColor); |
| } else { |
| |
| colorValue.value = colorInput.value; |
| } |
| }); |
|
|
| pickerContainer.appendChild(colorInput); |
| pickerContainer.appendChild(colorValue); |
| container.appendChild(labelElement); |
| container.appendChild(pickerContainer); |
|
|
| return container; |
| } |
|
|
| |
| moveSliderPropertiesToEnd(panel) { |
| const sliderProperties = ["fontSize", "padding", "borderRadius"]; |
| const content = panel.content; |
|
|
| sliderProperties.forEach(propName => { |
| const element = panel.querySelector(`div.property[data-property="${propName}"]`); |
| if (element) { |
| content.appendChild(element); |
| } |
| }); |
| } |
|
|
| |
| convertToSlider(panel, propertyName, min, max, step) { |
| const propertyElement = panel.querySelector(`div.property[data-property="${propertyName}"]`); |
| if (!propertyElement) return; |
|
|
| const valueElement = propertyElement.querySelector('.property_value'); |
| if (!valueElement) return; |
|
|
| |
| valueElement.removeAttribute('contenteditable'); |
|
|
| |
| propertyElement.style.cssText = ` |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 8px 0; |
| margin: 4px 0; |
| `; |
|
|
| |
| const sliderContainer = document.createElement('div'); |
| sliderContainer.style.cssText = ` |
| display: flex; |
| align-items: center; |
| flex: 1; |
| margin-left: 10px; |
| gap: 10px; |
| `; |
|
|
| |
| const slider = document.createElement('input'); |
| slider.type = 'range'; |
| slider.min = min.toString(); |
| slider.max = max.toString(); |
| slider.step = step.toString(); |
|
|
| |
| let initialValue; |
| if (propertyName === 'fontSize' && this.properties["autoScale"]) { |
| initialValue = this.properties["baseFontSize"]; |
| } else { |
| initialValue = this.properties[propertyName]; |
| } |
| slider.value = initialValue.toString(); |
|
|
| |
| slider.style.cssText = ` |
| flex: 1; |
| height: 6px; |
| background: linear-gradient(to right, #555, #777); |
| outline: none; |
| border-radius: 3px; |
| appearance: none; |
| -webkit-appearance: none; |
| cursor: pointer; |
| `; |
|
|
| |
| if (!document.getElementById('slider-styles')) { |
| const style = document.createElement('style'); |
| style.id = 'slider-styles'; |
| style.textContent = ` |
| input[type="range"]::-webkit-slider-thumb { |
| appearance: none; |
| -webkit-appearance: none; |
| height: 20px; |
| width: 20px; |
| border-radius: 50%; |
| background: linear-gradient(45deg, #4CAF50, #45a049); |
| cursor: pointer; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.3); |
| border: 2px solid #fff; |
| } |
| input[type="range"]::-webkit-slider-thumb:hover { |
| background: linear-gradient(45deg, #5CBF60, #55b059); |
| transform: scale(1.1); |
| } |
| input[type="range"]::-moz-range-thumb { |
| height: 20px; |
| width: 20px; |
| border-radius: 50%; |
| background: linear-gradient(45deg, #4CAF50, #45a049); |
| cursor: pointer; |
| border: 2px solid #fff; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.3); |
| } |
| input[type="range"]::-moz-range-track { |
| background: linear-gradient(to right, #555, #777); |
| height: 6px; |
| border-radius: 3px; |
| } |
| `; |
| document.head.appendChild(style); |
| } |
|
|
| |
| valueElement.style.cssText = ` |
| min-width: 35px; |
| text-align: center; |
| color: #fff; |
| font-size: 12px; |
| font-weight: bold; |
| background: rgba(255,255,255,0.1); |
| border-radius: 4px; |
| padding: 2px 6px; |
| `; |
|
|
| |
| valueElement.textContent = initialValue.toString(); |
|
|
| |
| slider.addEventListener('input', (e) => { |
| const newValue = parseFloat(e.target.value); |
|
|
| |
| if (propertyName === 'fontSize' && this.properties["autoScale"]) { |
| this.properties["baseFontSize"] = newValue; |
| |
| valueElement.textContent = newValue.toString(); |
| } else { |
| this.properties[propertyName] = newValue; |
| valueElement.textContent = newValue.toString(); |
| } |
|
|
| |
| this.setDirtyCanvas(true, true); |
| }); |
|
|
| |
| slider.addEventListener('change', (e) => { |
| const newValue = parseFloat(e.target.value); |
|
|
| |
| if (propertyName === 'fontSize' && this.properties["autoScale"]) { |
| this.properties["baseFontSize"] = newValue; |
| valueElement.textContent = newValue.toString(); |
| } else { |
| this.properties[propertyName] = newValue; |
| valueElement.textContent = newValue.toString(); |
| } |
|
|
| this.setDirtyCanvas(true, true); |
| }); |
|
|
| |
| const nameElement = propertyElement.querySelector('.property_name'); |
| propertyElement.innerHTML = ''; |
|
|
| |
| propertyElement.appendChild(nameElement); |
| sliderContainer.appendChild(slider); |
| sliderContainer.appendChild(valueElement); |
| propertyElement.appendChild(sliderContainer); |
| } |
|
|
|
|
| |
| onResize(size) { |
| if (this.properties["autoScale"]) { |
| |
| this.setDirtyCanvas(true, true); |
| } |
| } |
|
|
| } |
|
|
| |
| lg_note.type = "LG_Note"; |
| lg_note.title = "🎈LG_Note"; |
| lg_note.title_mode = LiteGraph.NO_TITLE; |
| lg_note.collapsable = false; |
| lg_note._category = "🎈LAOGOU/Utils"; |
|
|
| |
| lg_note["@fontSize"] = { type: "number" }; |
| lg_note["@baseFontSize"] = { type: "number" }; |
| lg_note["@fontFamily"] = { type: "string" }; |
| lg_note["@fontColor"] = { type: "string" }; |
| lg_note["@textAlign"] = { type: "combo", values: ["left", "center", "right"] }; |
| lg_note["@backgroundColor"] = { type: "string" }; |
| lg_note["@padding"] = { type: "number" }; |
| lg_note["@borderRadius"] = { type: "number" }; |
| lg_note["@autoScale"] = { type: "boolean" }; |
|
|
| |
| const labelNodeState = { |
| processingMouseDown: false, |
| lastAdjustedMouseEvent: null |
| }; |
|
|
| |
| const oldDrawNode = LGraphCanvas.prototype.drawNode; |
| LGraphCanvas.prototype.drawNode = function (node, ctx) { |
| if (node.constructor === lg_note) { |
| node.bgcolor = "transparent"; |
| node.color = "transparent"; |
| const v = oldDrawNode.apply(this, arguments); |
| node.draw(ctx); |
| return v; |
| } |
| const v = oldDrawNode.apply(this, arguments); |
| return v; |
| }; |
|
|
| |
| const oldGetNodeOnPos = LGraph.prototype.getNodeOnPos; |
| LGraph.prototype.getNodeOnPos = function (x, y, nodes_list, margin) { |
| if (nodes_list && |
| labelNodeState.processingMouseDown && |
| labelNodeState.lastAdjustedMouseEvent?.type.includes("down") && |
| labelNodeState.lastAdjustedMouseEvent?.which === 1) { |
| let isDoubleClick = LiteGraph.getTime() - LGraphCanvas.active_canvas.last_mouseclick < 300; |
| if (!isDoubleClick) { |
| nodes_list = [...nodes_list].filter((n) => !(n instanceof lg_note) || !n.flags?.pinned); |
| } |
| } |
| return oldGetNodeOnPos.apply(this, [x, y, nodes_list, margin]); |
| }; |
|
|
| |
| const processMouseDown = LGraphCanvas.prototype.processMouseDown; |
| LGraphCanvas.prototype.processMouseDown = function (e) { |
| labelNodeState.processingMouseDown = true; |
| const returnVal = processMouseDown.apply(this, [...arguments]); |
| labelNodeState.processingMouseDown = false; |
| return returnVal; |
| }; |
|
|
| const adjustMouseEvent = LGraphCanvas.prototype.adjustMouseEvent; |
| LGraphCanvas.prototype.adjustMouseEvent = function (e) { |
| adjustMouseEvent.apply(this, [...arguments]); |
| labelNodeState.lastAdjustedMouseEvent = e; |
| }; |
|
|
| |
| app.registerExtension({ |
| name: "LG_Note", |
| registerCustomNodes() { |
| lg_note.setUp(); |
| }, |
| }); |
|
|