import { app } from "../../../scripts/app.js"; import { api } from "../../../scripts/api.js"; let activeNodeId = null; const HEAVY_PARAMS = ['text', 'font_name', 'outline_thickness', 'effect_mode', 'text_scale_x', 'text_scale_y', 'text_align', 'line_spacing', 'letter_spacing', 'enable_glow', 'glow_color', 'glow_size', 'glow_spread', 'glow_opacity']; const GLOW_PARAMS = ['enable_glow', 'glow_color', 'glow_size', 'glow_spread', 'glow_opacity']; const SHADOW_PARAMS = ['enable_shadow', 'shadow_color', 'shadow_offset_x', 'shadow_offset_y', 'shadow_opacity', 'shadow_blur']; const DEFAULT_EFFECTS = { outline: { outline_thickness: 0, outline_color: "#808080", outline_opacity: 1.0 }, glow: { enable_glow: false, glow_color: "#FFFFFF", glow_size: 100, glow_spread: 150, glow_opacity: 1.0 }, shadow: { enable_shadow: false, shadow_color: "#333333", shadow_offset_x: 10, shadow_offset_y: 10, shadow_opacity: 0.8, shadow_blur: 15 } }; app.registerExtension({ name: "RaykoStudio.RSOverlayPro", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== "RS_OverlayPro") return; function createColorPickerRow(node, labelText, paramName, defaultColor) { const container = document.createElement('div'); container.style.cssText = 'display:flex;align-items:center;gap:6px;width:100%;'; const label = document.createElement('div'); label.textContent = labelText; label.style.cssText = 'flex:6;font-size:11px;color:#aaa;text-transform:uppercase;letter-spacing:0.5px;'; const hexInput = document.createElement('input'); hexInput.type = 'text'; hexInput.value = defaultColor; hexInput.style.cssText = 'flex:3.5;padding:4px 6px;background:#1a1a1a;color:#fff;border:1px solid #444;border-radius:4px;font-family:monospace;font-size:11px;outline:none;transition:0.15s;'; hexInput.onmouseenter = () => { hexInput.style.borderColor = '#666'; }; hexInput.onmouseleave = () => { if (document.activeElement !== hexInput) hexInput.style.borderColor = '#444'; }; hexInput.onfocus = () => { hexInput.style.borderColor = '#4CAF50'; }; hexInput.onblur = () => { hexInput.style.borderColor = '#444'; validateAndUpdate(); }; hexInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); validateAndUpdate(); hexInput.blur(); } }; const colorBox = document.createElement('div'); colorBox.style.cssText = 'flex:0 0 24px;width:24px;height:24px;background:' + defaultColor + ';border:1px solid #444;border-radius:4px;position:relative;cursor:pointer;transition:0.15s;'; colorBox.onmouseenter = () => { colorBox.style.borderColor = '#888'; }; colorBox.onmouseleave = () => { colorBox.style.borderColor = '#444'; }; const nativePicker = document.createElement('input'); nativePicker.type = 'color'; nativePicker.value = defaultColor; nativePicker.style.cssText = 'opacity:0;width:100%;height:100%;position:absolute;top:0;left:0;cursor:pointer;'; nativePicker.oninput = (e) => { const color = e.target.value.toUpperCase(); hexInput.value = color; colorBox.style.background = color; node.textParams[paramName] = color; if (typeof node.scheduleRender === 'function') node.scheduleRender(paramName); node.syncAllWidgets(); }; colorBox.appendChild(nativePicker); function validateAndUpdate() { let value = hexInput.value.trim(); if (!value.startsWith('#')) value = '#' + value; const hexRegex = /^#([0-9A-Fa-f]{3}){1,2}$/; if (hexRegex.test(value)) { if (value.length === 4) value = '#' + value[1] + value[1] + value[2] + value[2] + value[3] + value[3]; value = value.toUpperCase(); hexInput.value = value; colorBox.style.background = value; nativePicker.value = value; node.textParams[paramName] = value; if (typeof node.scheduleRender === 'function') node.scheduleRender(paramName); node.syncAllWidgets(); } else { hexInput.value = node.textParams[paramName] || defaultColor; } } container.appendChild(label); container.appendChild(hexInput); container.appendChild(colorBox); if (!node.colorHexInputs) node.colorHexInputs = {}; if (!node.colorBoxes) node.colorBoxes = {}; node.colorHexInputs[paramName] = hexInput; node.colorBoxes[paramName] = colorBox; return container; } const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (onNodeCreated) onNodeCreated.apply(this, arguments); this.overlay = { x: 0, y: 0, width: 100, height: 100, rotation: 0 }; this.overlayRelative = { x: 0.5, y: 0.5, width: 0.3, height: 0.3, rotation: 0 }; this.realOverlay = { width: 0, height: 0 }; this.realBackground = { width: 0, height: 0 }; this.displayWidth = 420; this.displayHeight = 420; this.canvasPixelSize = 420; this.viewScale = 1.0; this.viewOffsetX = 0; this.viewOffsetY = 0; this.backgroundImage = null; this.isEditing = false; this.isLoading = false; this.dragType = null; this.dragState = null; this.currentSessionTimestamp = null; this.isDragging = false; this.awaitingServerRender = false; this.debug = false; this.textMask = null; this.outlineMask = null; this._offCanvas = null; this._offCtx = null; this.baseWidth = 0; this.baseHeight = 0; this._glowMask = null; this._textMaskFile = null; this._glowMaskTimeout = null; this._glowMaskTimestamp = null; this._glowExtraPadding = 0; this.glowPaddingPreview = 0; this._glowMaskAbortController = null; this._glowKey = null; this._glowDrawLogged = false; this._glowFallbackLogged = false; this._shadowMask = null; this._shadowMaskTimeout = null; this._shadowMaskTimestamp = null; this._shadowMaskAbortController = null; this._shadowKey = null; this.colorHexInputs = {}; this.colorBoxes = {}; this._updatingWidgets = false; this._sectionStates = { text: true, outline: false, glow: false, shadow: false }; this.textParams = { text: "", font_name: "", text_color: "#FFFFFF", outline_thickness: 0, outline_color: "#808080", text_opacity: 1.0, outline_opacity: 1.0, effect_mode: "on", text_scale_x: 1.0, text_scale_y: 1.0, min_font_size: 4, max_font_size: 2000, padding: 5, padding_y: 10, text_align: "center", line_spacing: 1.0, letter_spacing: 0.0, enable_glow: false, glow_color: "#FFFFFF", glow_size: 100, glow_spread: 150, glow_opacity: 1.0, enable_shadow: false, shadow_color: "#333333", shadow_offset_x: 10, shadow_offset_y: 10, shadow_opacity: 0.8, shadow_blur: 15 }; this.fontList = []; this.advancedMode = false; this.fontMenu = null; (async () => { try { const response = await api.fetchApi("/rayko/rs_overlay_pro/get_fonts"); const data = await response.json(); if (data.font_list && data.font_list.length > 0) { this.fontList = data.font_list; if (!this.textParams.font_name || this.textParams.font_name === "") { this.textParams.font_name = data.default_font || this.fontList[0]; } if (this.overlayInputs && this.overlayInputs.font_name) { this.overlayInputs.font_name.innerHTML = ''; this.fontList.forEach(f => { const op = document.createElement('option'); op.value = f; op.textContent = f; if (f === this.textParams.font_name) op.selected = true; this.overlayInputs.font_name.appendChild(op); }); } this.setDirtyCanvas(true); } } catch(e) { console.error("Failed to load font list:", e); } })(); this.overlayContainer = null; this.overlayCanvas = null; this.overlayCtx = null; this.overlayRenderLoop = null; this.overlayInputs = {}; this.sliderDisplays = {}; this.currentRenderAbortController = null; this.canvasRealWidth = 0; this.canvasRealHeight = 0; this.minWidth = 500; this.minHeight = 420; this.setSize([this.minWidth, this.minHeight]); this.btnApplyHover = false; this.btnCancelHover = false; this.textareaState = { isOpen: false, element: null }; this.textareaRect = null; this.textareaHover = false; this.lastClickCoords = null; this.fontSelectRect = null; this.hexRect = null; this.colorBoxRect = null; this.fontSelectHover = false; this.hexHover = false; this.colorBoxHover = false; this.alignButtonsRects = []; this.alignButtonsHover = [false, false, false]; this.lineSpacingRect = null; this.lineSpacingHover = false; this.lineSpacingDragging = false; this.lineSpacingPopup = null; this.lineSpacingValueRect = null; this.lineSpacingValueHover = false; this.lineSpacingTrackRect = null; this.fontColorY = 0; this.buttonsY = 0; this.pendingEditorData = null; this.renderTimeout = null; this.syncAllWidgets = function() { if (this._updatingWidgets) return; this._updatingWidgets = true; try { if (this.overlayInputs.text) this.overlayInputs.text.value = this.textParams.text || ''; if (this.overlayInputs.font_name) this.overlayInputs.font_name.value = this.textParams.font_name || ''; for (const key in this.overlayInputs) { const el = this.overlayInputs[key]; if (!el) continue; if (el.type === 'range') { el.value = this.textParams[key]; if (this.sliderDisplays[key]) { const step = parseFloat(el.step) || 1; this.sliderDisplays[key].textContent = step < 1 ? parseFloat(this.textParams[key]).toFixed(2) : String(parseInt(this.textParams[key])); } } else if (el.type === 'checkbox') { el.checked = !!this.textParams[key]; } } if (this.alignButtons) { const align = this.textParams.text_align || 'center'; this.alignButtons.forEach((btn, i) => { const types = ['left', 'center', 'right']; const isActive = types[i] === align; if (isActive) { btn.style.background = '#4CAF50'; btn.style.color = '#fff'; btn.style.borderColor = '#4CAF50'; } else { btn.style.background = '#252525'; btn.style.color = '#aaa'; btn.style.borderColor = '#444'; } }); } if (this.colorHexInputs) { for (const key in this.colorHexInputs) { const hexInput = this.colorHexInputs[key]; const colorBox = this.colorBoxes[key]; if (hexInput && this.textParams[key] !== undefined) { hexInput.value = this.textParams[key]; if (colorBox) colorBox.style.background = this.textParams[key]; } } } } finally { this._updatingWidgets = false; } }; if (!this.overlayContainer) { this.overlayContainer = document.createElement('div'); this.overlayContainer.style.cssText = 'position:fixed;top:60px;left:0;right:0;bottom:0;background:rgba(10,10,10,0.96);z-index:999;display:none;flex-direction:row;align-items:stretch;font-family:system-ui,-apple-system,sans-serif;'; this.overlayCanvasWrapper = document.createElement('div'); this.overlayCanvasWrapper.style.cssText = 'flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative;'; this.overlayCanvas = document.createElement('canvas'); this.overlayCanvas.style.cssText = 'box-shadow:0 8px 32px rgba(0,0,0,0.7);cursor:crosshair;max-width:98%;max-height:98%;border-radius:8px;'; this.overlayCanvasWrapper.appendChild(this.overlayCanvas); this.overlayCtx = this.overlayCanvas.getContext('2d'); this.sidePanel = document.createElement('div'); this.sidePanel.style.cssText = 'width:320px;background:#151515;border-left:1px solid #333;padding:12px;display:flex;flex-direction:column;gap:4px;box-sizing:border-box;overflow-y:auto;'; const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'position:sticky;top:0;background:#151515;z-index:10;padding:0 0 8px 0;border-bottom:1px solid #333;margin-bottom:8px;'; this.sidePanel.appendChild(buttonContainer); const makeLabel = (txt) => { const l = document.createElement('label'); l.textContent = txt; l.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;margin-bottom:-4px;display:block;'; return l; }; const makeSectionHeader = (label, effectName, isEnabled, onToggle, onExpand) => { const header = document.createElement('div'); header.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 0;cursor:pointer;user-select:none;'; const chevron = document.createElement('span'); chevron.textContent = '▶'; chevron.style.cssText = 'color:#666;font-size:12px;transition:transform 200ms;flex-shrink:0;'; const lbl = document.createElement('label'); lbl.textContent = label; lbl.style.cssText = `flex:1;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;transition:color 200ms;`; lbl.style.color = isEnabled ? '#00FF00' : '#666'; header.appendChild(chevron); header.appendChild(lbl); if (effectName) { const toggle = document.createElement('input'); toggle.type = 'checkbox'; toggle.checked = isEnabled; toggle.style.cssText = 'width:40px;height:20px;cursor:pointer;accent-color:#4CAF50;'; toggle.addEventListener('change', (e) => { e.stopPropagation(); onToggle(toggle.checked); }); header.appendChild(toggle); } header.addEventListener('click', (e) => { if (e.target.type !== 'checkbox') { onExpand(); } }); return { header, chevron, lbl }; }; const makeSelect = (opts, val) => { const s = document.createElement('select'); opts.forEach(o => { const op = document.createElement('option'); op.value = o; op.textContent = o; if (o === val) op.selected = true; s.appendChild(op); }); s.style.cssText = 'width:100%;background:#252525;color:#eee;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px;outline:none;'; return s; }; const makeTextarea = (value) => { const t = document.createElement('textarea'); t.value = value; t.style.cssText = 'width:100%;background:#252525;color:#eee;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px;outline:none;resize:vertical;min-height:60px;'; return t; }; const makeBtn = (txt, col, onClick) => { const b = document.createElement('button'); b.textContent = txt; b.style.cssText = `width:100%;padding:10px;background:#222;color:${col};border:1px solid ${col};border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;margin-top:4px;transition:0.15s;`; b.onmouseenter = () => { b.style.background = '#2a2a2a'; b.style.transform = 'translateY(-1px)'; }; b.onmouseleave = () => { b.style.background = '#222'; b.style.transform = 'none'; }; b.onclick = (e) => { e.stopPropagation(); onClick(); }; return b; }; const div = () => { const d = document.createElement('div'); d.style.cssText = 'height:1px;background:#333;margin:4px 0;'; return d; }; const formatValue = (val, step) => { if (step < 1) return parseFloat(val).toFixed(2); return String(parseInt(val)); }; const makeToggle = (label, key, currentValue) => { const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;align-items:center;gap:8px;'; const lbl = document.createElement('label'); lbl.textContent = label; lbl.style.cssText = 'flex:1;color:#aaa;font-size:11px;font-weight:600;'; const toggle = document.createElement('input'); toggle.type = 'checkbox'; toggle.checked = currentValue; toggle.style.cssText = 'width:40px;height:20px;cursor:pointer;accent-color:#4CAF50;'; toggle.addEventListener('change', () => { this.textParams[key] = toggle.checked; scheduleRender(key); this.syncAllWidgets(); }); wrap.appendChild(lbl); wrap.appendChild(toggle); this.overlayInputs[key] = toggle; return wrap; }; const scheduleRender = (key, immediate = false) => { if (SHADOW_PARAMS.includes(key)) { this.setDirtyCanvas(true); this.syncAllWidgets(); this.requestShadowMask(); return; } const isHeavy = HEAVY_PARAMS.includes(key); const isGlowParam = GLOW_PARAMS.includes(key); if (isHeavy && !isGlowParam) { if (this.currentRenderAbortController) { this.currentRenderAbortController.abort(); this.currentRenderAbortController = null; } if (this.renderTimeout) clearTimeout(this.renderTimeout); const delay = immediate ? 0 : 40; this.renderTimeout = setTimeout(() => { this.currentRenderAbortController = new AbortController(); const signal = this.currentRenderAbortController.signal; this.renderMasks(signal); }, delay); } else if (isGlowParam) { this.requestGlowMask(); } else { this.setDirtyCanvas(true); } this.syncAllWidgets(); }; this.scheduleRender = scheduleRender; const makeSlider = (label, key, min, max, step, isFloat = false) => { const container = document.createElement('div'); container.style.cssText = 'display:flex;flex-direction:column;gap:4px;'; const lbl = document.createElement('label'); lbl.textContent = label; lbl.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;'; container.appendChild(lbl); const row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:8px;'; const slider = document.createElement('input'); slider.type = 'range'; slider.min = min; slider.max = max; slider.step = step; slider.value = this.textParams[key]; slider.style.cssText = 'flex:1;height:4px;background:#252525;border-radius:2px;outline:none;cursor:pointer;-webkit-appearance:none;'; const valueDisplay = document.createElement('div'); valueDisplay.textContent = formatValue(this.textParams[key], step); valueDisplay.style.cssText = 'min-width:50px;text-align:center;background:#252525;color:#4CAF50;border:1px solid #444;border-radius:4px;padding:4px 8px;font-size:12px;cursor:pointer;font-weight:600;transition:0.15s;'; valueDisplay.onmouseenter = () => { valueDisplay.style.background = '#2a2a2a'; valueDisplay.style.borderColor = '#4CAF50'; }; valueDisplay.onmouseleave = () => { valueDisplay.style.background = '#252525'; valueDisplay.style.borderColor = '#444'; }; valueDisplay.onclick = (e) => { e.stopPropagation(); const currentValue = this.textParams[key]; const popup = document.createElement('div'); popup.style.cssText = 'position:fixed;z-index:10003;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:8px 12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:8px;'; const input = document.createElement('input'); input.type = 'number'; input.value = currentValue; input.min = min; input.max = max; input.step = step; input.style.cssText = 'width:100px;background:#222;color:#fff;border:1px solid #444;border-radius:4px;padding:6px 10px;font-size:12px;outline:none;'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'OK'; saveBtn.style.cssText = 'background:#4CAF50;color:#fff;border:none;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer;'; const doSave = () => { let num = isFloat ? parseFloat(input.value) : parseInt(input.value); if (isNaN(num)) num = currentValue; num = Math.max(min, Math.min(max, num)); this.textParams[key] = num; slider.value = num; valueDisplay.textContent = formatValue(num, step); popup.remove(); scheduleRender(key); this.syncAllWidgets(); }; saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); }; input.onkeydown = (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); doSave(); } }; popup.appendChild(input); popup.appendChild(saveBtn); document.body.appendChild(popup); const popupWidth = popup.offsetWidth; const popupHeight = popup.offsetHeight; let leftPos = e.clientX - popupWidth - 8; let topPos = e.clientY; if (leftPos < 8) leftPos = e.clientX + 8; if (topPos + popupHeight > window.innerHeight - 8) topPos = window.innerHeight - popupHeight - 8; popup.style.left = leftPos + 'px'; popup.style.top = topPos + 'px'; setTimeout(() => { input.focus(); input.select(); }, 50); setTimeout(() => { const closeHandler = (ev) => { if (!popup.contains(ev.target)) { popup.remove(); document.removeEventListener('mousedown', closeHandler); } }; document.addEventListener('mousedown', closeHandler); }, 100); }; slider.oninput = () => { const val = isFloat ? parseFloat(slider.value) : parseInt(slider.value); this.textParams[key] = val; valueDisplay.textContent = formatValue(val, step); scheduleRender(key); this.syncAllWidgets(); }; this.overlayInputs[key] = slider; this.sliderDisplays[key] = valueDisplay; row.appendChild(slider); row.appendChild(valueDisplay); container.appendChild(row); return container; }; const btnNormalMode = makeBtn("🟢 NORMAL MODE", "#2196F3", () => { this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnNormalMode); const btnApply = makeBtn("✔️ APPLY", "#4CAF50", () => { this.sendTransforms(); this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnApply); const btnCancel = makeBtn("❌ CANCEL", "#dc3545", () => { this.cancelEditing(); this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnCancel); const textHeader = document.createElement('div'); textHeader.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 0;'; const textChevron = document.createElement('span'); textChevron.textContent = '▼'; textChevron.style.cssText = 'color:#666;font-size:12px;'; const textLabel = document.createElement('label'); textLabel.textContent = 'TEXT'; textLabel.style.cssText = 'flex:1;color:#aaa;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;'; textHeader.appendChild(textChevron); textHeader.appendChild(textLabel); this.sidePanel.appendChild(textHeader); const textContent = document.createElement('div'); textContent.style.cssText = 'display:block;'; this.overlayInputs.text = makeTextarea(this.textParams.text); this.overlayInputs.text.addEventListener('input', () => { this.textParams.text = this.overlayInputs.text.value; scheduleRender('text'); this.syncAllWidgets(); }); textContent.appendChild(this.overlayInputs.text); const alignContainer = document.createElement('div'); alignContainer.style.cssText = 'display:flex;gap:8px;margin-top:8px;'; const alignTypes = ['left', 'center', 'right']; const alignIcons = [ '', '', '' ]; this.alignButtons = []; alignTypes.forEach((type, i) => { const btn = document.createElement('button'); btn.innerHTML = alignIcons[i]; btn.style.cssText = `flex:1;padding:4px;background:#252525;color:#aaa;border:1px solid #444;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:0.15s;`; btn.onmouseenter = () => { btn.style.background = '#2a2a2a'; btn.style.color = '#fff'; }; btn.onmouseleave = () => { if (this.textParams.text_align !== type) { btn.style.background = '#252525'; btn.style.color = '#aaa'; } }; btn.onclick = (e) => { e.stopPropagation(); this.textParams.text_align = type; this.alignButtons.forEach((b, j) => { if (j === i) { b.style.background = '#4CAF50'; b.style.color = '#fff'; b.style.borderColor = '#4CAF50'; } else { b.style.background = '#252525'; b.style.color = '#aaa'; b.style.borderColor = '#444'; } }); scheduleRender('text_align'); this.syncAllWidgets(); }; if (this.textParams.text_align === type) { btn.style.background = '#4CAF50'; btn.style.color = '#fff'; btn.style.borderColor = '#4CAF50'; } this.alignButtons.push(btn); alignContainer.appendChild(btn); }); textContent.appendChild(alignContainer); textContent.appendChild(div()); this.overlayInputs.font_name = makeSelect([], ""); this.overlayInputs.font_name.addEventListener('change', () => { this.textParams.font_name = this.overlayInputs.font_name.value; scheduleRender('font_name'); this.syncAllWidgets(); }); textContent.appendChild(this.overlayInputs.font_name); textContent.appendChild(div()); textContent.appendChild(createColorPickerRow(this, "TEXT COLOR", "text_color", this.textParams.text_color)); textContent.appendChild(div()); textContent.appendChild(makeSlider("TEXT OPACITY", "text_opacity", 0, 1, 0.05, true)); textContent.appendChild(makeSlider("LINE SPACING", "line_spacing", 0.5, 3, 0.1, true)); textContent.appendChild(makeSlider("LETTER SPACING", "letter_spacing", -20, 100, 0.5, true)); textContent.appendChild(div()); this.sidePanel.appendChild(textContent); const outlineHeader = document.createElement('div'); outlineHeader.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 0;cursor:pointer;user-select:none;'; const outlineChevron = document.createElement('span'); outlineChevron.textContent = this._sectionStates.outline ? '▼' : '▶'; outlineChevron.style.cssText = 'color:#666;font-size:12px;transition:transform 200ms;flex-shrink:0;'; const outlineLabel = document.createElement('label'); outlineLabel.textContent = 'OUTLINE SETTINGS'; outlineLabel.style.cssText = `flex:1;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;transition:color 200ms;`; outlineLabel.style.color = this.textParams.outline_thickness > 0 ? '#00FF00' : '#666'; outlineHeader.appendChild(outlineChevron); outlineHeader.appendChild(outlineLabel); outlineHeader.addEventListener('click', () => { this._sectionStates.outline = !this._sectionStates.outline; outlineChevron.textContent = this._sectionStates.outline ? '▼' : '▶'; outlineContent.style.display = this._sectionStates.outline ? 'block' : 'none'; }); this.sidePanel.appendChild(outlineHeader); const outlineContent = document.createElement('div'); outlineContent.style.cssText = this._sectionStates.outline ? 'display:block;' : 'display:none;'; outlineContent.appendChild(div()); outlineContent.appendChild(makeSlider("THICKNESS", "outline_thickness", 0, 50, 1, false)); outlineContent.appendChild(createColorPickerRow(this, "COLOR", "outline_color", this.textParams.outline_color)); outlineContent.appendChild(div()); outlineContent.appendChild(makeSlider("OPACITY", "outline_opacity", 0, 1, 0.05, true)); outlineContent.appendChild(div()); this.sidePanel.appendChild(outlineContent); const glowHeader = document.createElement('div'); glowHeader.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 0;cursor:pointer;user-select:none;'; const glowChevron = document.createElement('span'); glowChevron.textContent = this._sectionStates.glow ? '▼' : '▶'; glowChevron.style.cssText = 'color:#666;font-size:12px;transition:transform 200ms;flex-shrink:0;'; const glowLabel = document.createElement('label'); glowLabel.textContent = 'GLOW SETTINGS'; glowLabel.style.cssText = `flex:1;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;transition:color 200ms;`; glowLabel.style.color = this.textParams.enable_glow ? '#00FF00' : '#666'; glowHeader.appendChild(glowChevron); glowHeader.appendChild(glowLabel); const glowToggle = document.createElement('input'); glowToggle.type = 'checkbox'; glowToggle.checked = this.textParams.enable_glow; glowToggle.style.cssText = 'width:40px;height:20px;cursor:pointer;accent-color:#4CAF50;'; glowToggle.addEventListener('click', (e) => e.stopPropagation()); glowToggle.addEventListener('change', (e) => { e.stopPropagation(); const enabled = e.target.checked; this.textParams.enable_glow = enabled; if (!enabled) { this.textParams.glow_color = DEFAULT_EFFECTS.glow.glow_color; this.textParams.glow_size = DEFAULT_EFFECTS.glow.glow_size; this.textParams.glow_spread = DEFAULT_EFFECTS.glow.glow_spread; this.textParams.glow_opacity = DEFAULT_EFFECTS.glow.glow_opacity; } glowLabel.style.color = enabled ? '#00FF00' : '#666'; this.syncAllWidgets(); scheduleRender('enable_glow'); }); glowHeader.appendChild(glowToggle); glowHeader.addEventListener('click', (e) => { if (e.target.type !== 'checkbox') { this._sectionStates.glow = !this._sectionStates.glow; glowChevron.textContent = this._sectionStates.glow ? '▼' : '▶'; glowContent.style.display = this._sectionStates.glow ? 'block' : 'none'; } }); this.sidePanel.appendChild(glowHeader); const glowContent = document.createElement('div'); glowContent.style.cssText = this._sectionStates.glow ? 'display:block;' : 'display:none;'; glowContent.appendChild(div()); glowContent.appendChild(createColorPickerRow(this, "GLOW COLOR", "glow_color", this.textParams.glow_color)); glowContent.appendChild(div()); glowContent.appendChild(makeSlider("SIZE", "glow_size", 0, 200, 1, false)); glowContent.appendChild(makeSlider("SPREAD", "glow_spread", 0, 300, 1, false)); glowContent.appendChild(makeSlider("OPACITY", "glow_opacity", 0, 1, 0.02, true)); glowContent.appendChild(div()); this.sidePanel.appendChild(glowContent); const shadowHeader = document.createElement('div'); shadowHeader.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 0;cursor:pointer;user-select:none;'; const shadowChevron = document.createElement('span'); shadowChevron.textContent = this._sectionStates.shadow ? '▼' : '▶'; shadowChevron.style.cssText = 'color:#666;font-size:12px;transition:transform 200ms;flex-shrink:0;'; const shadowLabel = document.createElement('label'); shadowLabel.textContent = 'SHADOW SETTINGS'; shadowLabel.style.cssText = `flex:1;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;transition:color 200ms;`; shadowLabel.style.color = this.textParams.enable_shadow ? '#00FF00' : '#666'; shadowHeader.appendChild(shadowChevron); shadowHeader.appendChild(shadowLabel); const shadowToggle = document.createElement('input'); shadowToggle.type = 'checkbox'; shadowToggle.checked = this.textParams.enable_shadow; shadowToggle.style.cssText = 'width:40px;height:20px;cursor:pointer;accent-color:#4CAF50;'; shadowToggle.addEventListener('click', (e) => e.stopPropagation()); shadowToggle.addEventListener('change', (e) => { e.stopPropagation(); const enabled = e.target.checked; this.textParams.enable_shadow = enabled; if (!enabled) { this.textParams.shadow_color = DEFAULT_EFFECTS.shadow.shadow_color; this.textParams.shadow_offset_x = DEFAULT_EFFECTS.shadow.shadow_offset_x; this.textParams.shadow_offset_y = DEFAULT_EFFECTS.shadow.shadow_offset_y; this.textParams.shadow_opacity = DEFAULT_EFFECTS.shadow.shadow_opacity; this.textParams.shadow_blur = DEFAULT_EFFECTS.shadow.shadow_blur; } shadowLabel.style.color = enabled ? '#00FF00' : '#666'; this.syncAllWidgets(); scheduleRender('enable_shadow'); }); shadowHeader.appendChild(shadowToggle); shadowHeader.addEventListener('click', (e) => { if (e.target.type !== 'checkbox') { this._sectionStates.shadow = !this._sectionStates.shadow; shadowChevron.textContent = this._sectionStates.shadow ? '▼' : '▶'; shadowContent.style.display = this._sectionStates.shadow ? 'block' : 'none'; } }); this.sidePanel.appendChild(shadowHeader); const shadowContent = document.createElement('div'); shadowContent.style.cssText = this._sectionStates.shadow ? 'display:block;' : 'display:none;'; shadowContent.appendChild(div()); shadowContent.appendChild(createColorPickerRow(this, "COLOR", "shadow_color", this.textParams. shadow_color)); shadowContent.appendChild(div()); shadowContent.appendChild(makeSlider("OFFSET X", "shadow_offset_x", -30, 30, 1, false)); shadowContent.appendChild(makeSlider("OFFSET Y", "shadow_offset_y", -30, 30, 1, false)); shadowContent.appendChild(makeSlider("BLUR", "shadow_blur", 0, 100, 1, false)); shadowContent.appendChild(makeSlider("OPACITY", "shadow_opacity", 0, 1, 0.05, true)); shadowContent.appendChild(div()); this.sidePanel.appendChild(shadowContent); this.overlayContainer.appendChild(this.overlayCanvasWrapper); this.overlayContainer.appendChild(this.sidePanel); document.body.appendChild(this.overlayContainer); this.syncAllWidgets(); } api.addEventListener("rs-overlay-pro-start", (event) => { if (event.detail.id != this.id) return; this.pendingEditorData = event.detail; this.openDeferredEditor(); }); api.addEventListener("interrupted", () => { if (this.currentRenderAbortController) { this.currentRenderAbortController.abort(); this.currentRenderAbortController = null; } if (this._glowMaskTimeout) { clearTimeout(this._glowMaskTimeout); this._glowMaskTimeout = null; } if (this._glowMaskAbortController) { this._glowMaskAbortController.abort(); this._glowMaskAbortController = null; } if (this._shadowMaskTimeout) { clearTimeout(this._shadowMaskTimeout); this._shadowMaskTimeout = null; } if (this._shadowMaskAbortController) { this._shadowMaskAbortController.abort(); this._shadowMaskAbortController = null; } this.pendingEditorData = null; this.isLoading = false; this.isEditing = false; this.dragType = null; this.dragState = null; this.awaitingServerRender = false; this._glowMask = null; this._textMaskFile = null; this._glowKey = null; this._shadowMask = null; this._shadowKey = null; this._sectionStates = { text: true, outline: false, glow: false, shadow: false }; this.setDirtyCanvas(true); this.closeTextarea(); }); }; nodeType.prototype.requestGlowMask = function() { if (!this.textParams.enable_glow) { this._glowMask = null; this._glowExtraPadding = 0; this.glowPaddingPreview = 0; this._glowKey = null; this.setDirtyCanvas(true); return; } if (!this.textMask || !this._textMaskFile) return; const glowSize = this.textParams.glow_size; const glowSpread = this.textParams.glow_spread; if (glowSize <= 0) { this._glowMask = null; this._glowExtraPadding = 0; this.glowPaddingPreview = 0; this._glowKey = null; this.setDirtyCanvas(true); return; } const glowKey = `${this._textMaskFile}_${glowSize}_${glowSpread}_${this.textParams.glow_opacity}_${this.textParams.glow_color}`; if (this._glowKey === glowKey && this._glowMask) return; this._glowKey = glowKey; const glowPadding = glowSize * (this.displayWidth / this.canvasRealWidth); this.glowPaddingPreview = glowPadding; clearTimeout(this._glowMaskTimeout); this._glowMaskTimeout = setTimeout(() => { this.fetchGlowMaskFromServer(); }, 500); }; nodeType.prototype.fetchGlowMaskFromServer = async function() { try { if (this._glowMaskAbortController) this._glowMaskAbortController.abort(); this._glowMaskAbortController = new AbortController(); const payload = { text_mask_file: this._textMaskFile, glow_size: this.textParams.glow_size, glow_spread: this.textParams.glow_spread, glow_opacity: this.textParams.glow_opacity, node_id: String(this.id) }; const response = await api.fetchApi('/rayko/rs_overlay_pro/render_glow_mask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: this._glowMaskAbortController.signal }); const data = await response.json(); if (data.glow_file) { const img = new Image(); img.crossOrigin = "Anonymous"; const ts = data.timestamp; this._glowMaskTimestamp = ts; this._glowExtraPadding = data.extra_padding || 0; img.onload = () => { if (this._glowMaskTimestamp === ts) { this._glowMask = img; this._glowDrawLogged = false; this._glowFallbackLogged = false; this.setDirtyCanvas(true); } }; img.onerror = () => {}; img.src = `/view?filename=${data.glow_file}&type=temp&t=${ts}`; } } catch (e) { if (e.name === 'AbortError') return; } }; nodeType.prototype.requestShadowMask = function() { if (!this.textParams.enable_shadow) { this._shadowMask = null; this._shadowKey = null; this.setDirtyCanvas(true); return; } if (!this.textMask || !this._textMaskFile) return; const shadowBlur = this.textParams.shadow_blur || 0; if (shadowBlur <= 0) { this._shadowMask = null; this._shadowKey = null; this.setDirtyCanvas(true); return; } const shadowKey = `${this._textMaskFile}_${shadowBlur}`; if (this._shadowKey === shadowKey && this._shadowMask) return; this._shadowKey = shadowKey; clearTimeout(this._shadowMaskTimeout); this._shadowMaskTimeout = setTimeout(() => { this.fetchShadowMaskFromServer(); }, 500); }; nodeType.prototype.fetchShadowMaskFromServer = async function() { try { if (this._shadowMaskAbortController) this._shadowMaskAbortController.abort(); this._shadowMaskAbortController = new AbortController(); const payload = { text_mask_file: this._textMaskFile, shadow_blur: this.textParams.shadow_blur || 0, node_id: String(this.id) }; const response = await api.fetchApi('/rayko/rs_overlay_pro/render_shadow_mask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: this._shadowMaskAbortController.signal }); const data = await response.json(); if (data.shadow_file) { const img = new Image(); img.crossOrigin = "Anonymous"; const ts = data.timestamp; this._shadowMaskTimestamp = ts; img.onload = () => { if (this._shadowMaskTimestamp === ts) { this._shadowMask = img; this.setDirtyCanvas(true); } }; img.onerror = () => {}; img.src = `/view?filename=${data.shadow_file}&type=temp&t=${ts}`; } } catch (e) { if (e.name === 'AbortError') return; } }; nodeType.prototype.drawMasked = function(ctx, maskImg, x, y, w, h, color, opacity) { if (!maskImg) return; if (!this._offCanvas) { this._offCanvas = document.createElement('canvas'); this._offCtx = this._offCanvas.getContext('2d'); } const off = this._offCanvas; const offCtx = this._offCtx; const mw = maskImg.naturalWidth || maskImg.width || w; const mh = maskImg.naturalHeight || maskImg.height || h; if (off.width !== mw || off.height !== mh) { off.width = mw; off.height = mh; } offCtx.clearRect(0, 0, mw, mh); offCtx.drawImage(maskImg, 0, 0, mw, mh); offCtx.globalCompositeOperation = 'source-in'; offCtx.globalAlpha = Math.min(1, Math.max(0, opacity)); offCtx.fillStyle = color; offCtx.fillRect(0, 0, mw, mh); offCtx.globalCompositeOperation = 'source-over'; offCtx.globalAlpha = 1; ctx.drawImage(off, x, y, w, h); }; nodeType.prototype.drawGlowPreview = function(ctx, x, y, w, h) { if (!this.textParams.enable_glow || !this.textMask) return; const glowSize = this.textParams.glow_size; if (glowSize <= 0) return; const glowOpacity = this.textParams.glow_opacity; const glowColor = this.textParams.glow_color; if (this._glowMask) { const padding = this._glowExtraPadding || 0; if (!this._offCanvas) { this._offCanvas = document.createElement('canvas'); this._offCtx = this._offCanvas.getContext('2d'); } const off = this._offCanvas; const offCtx = this._offCtx; const mw = this._glowMask.naturalWidth; const mh = this._glowMask.naturalHeight; if (off.width !== mw || off.height !== mh) { off.width = mw; off.height = mh; } offCtx.clearRect(0, 0, mw, mh); offCtx.drawImage(this._glowMask, 0, 0, mw, mh); offCtx.globalCompositeOperation = 'source-in'; offCtx.globalAlpha = Math.min(1, Math.max(0, glowOpacity)); offCtx.fillStyle = glowColor; offCtx.fillRect(0, 0, mw, mh); offCtx.globalCompositeOperation = 'source-over'; offCtx.globalAlpha = 1; const textW = mw - 2 * padding; const textH = mh - 2 * padding; const scaleX = w / textW; const scaleY = h / textH; const glowW = mw * scaleX; const glowH = mh * scaleY; const glowX = x - padding * scaleX; const glowY = y - padding * scaleY; ctx.drawImage(off, glowX, glowY, glowW, glowH); return; } const maxBlur = Math.min(250, glowSize); ctx.save(); ctx.shadowColor = glowColor; ctx.shadowBlur = maxBlur; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; this.drawMasked(ctx, this.textMask, x, y, w, h, glowColor, glowOpacity); ctx.restore(); }; nodeType.prototype.drawShadow = function(ctx, x, y, w, h) { if (!this.textParams.enable_shadow || !this.textMask) return; const opacity = this.textParams.shadow_opacity; if (opacity === undefined || opacity === null || opacity <= 0) return; const offsetX = this.textParams.shadow_offset_x || 0; const offsetY = this.textParams.shadow_offset_y || 0; const color = this.textParams.shadow_color || '#333333'; const blur = this.textParams.shadow_blur || 0; if (this._shadowMask && blur > 0) { const mw = this._shadowMask.naturalWidth; const mh = this._shadowMask.naturalHeight; if (!this._offCanvas) { this._offCanvas = document.createElement('canvas'); this._offCtx = this._offCanvas.getContext('2d'); } const off = this._offCanvas; const offCtx = this._offCtx; if (off.width !== mw || off.height !== mh) { off.width = mw; off.height = mh; } offCtx.clearRect(0, 0, mw, mh); offCtx.drawImage(this._shadowMask, 0, 0, mw, mh); offCtx.globalCompositeOperation = 'source-in'; offCtx.globalAlpha = Math.min(1, Math.max(0, opacity)); offCtx.fillStyle = color; offCtx.fillRect(0, 0, mw, mh); offCtx.globalCompositeOperation = 'source-over'; offCtx.globalAlpha = 1; ctx.drawImage(off, x + offsetX, y + offsetY, w, h); return; } ctx.save(); ctx.shadowColor = color; ctx.shadowBlur = blur; ctx.shadowOffsetX = offsetX; ctx.shadowOffsetY = offsetY; ctx.globalAlpha = opacity; this.drawMasked(ctx, this.textMask, x, y, w, h, color, 1.0); ctx.restore(); }; nodeType.prototype.drawTextOverlay = function(ctx, x, y, w, h) { this.drawGlowPreview(ctx, x, y, w, h); this.drawShadow(ctx, x, y, w, h); if (this.textParams.effect_mode === "on" && this.outlineMask) { this.drawMasked(ctx, this.outlineMask, x, y, w, h, this.textParams.outline_color, this.textParams.outline_opacity); } if (this.textMask) { this.drawMasked(ctx, this.textMask, x, y, w, h, this.textParams.text_color, this.textParams.text_opacity); } }; nodeType.prototype._resizeOverlayCanvas = function() { if (!this.overlayCanvasWrapper || !this.overlayCanvas) return; const w = this.overlayCanvasWrapper.clientWidth; const h = this.overlayCanvasWrapper.clientHeight; if (w > 0 && h > 0) { this.overlayCanvas.width = w; this.overlayCanvas.height = h; if (this.isEditing && this.backgroundImage) { this.updateOverlayAbsolute(); this.computeAndApplyView(); } this.setDirtyCanvas(true); if (this.advancedMode && this.isEditing) { this.computeAndApplyView(); } } }; nodeType.prototype._handleOverlayEvent = function(e, type) { e.preventDefault(); e.stopPropagation(); const rect = this.overlayCanvas.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return; const scaleX = this.overlayCanvas.width / rect.width; const scaleY = this.overlayCanvas.height / rect.height; const x = (e.clientX - rect.left) * scaleX; const y = (e.clientY - rect.top) * scaleY; if (type === 'down') this.onMouseDown(null, [x, y]); else if (type === 'move') this.onMouseMove(null, [x, y]); else this.onMouseUp(); this.setDirtyCanvas(true); }; nodeType.prototype._handleOverlayWheel = function(e) { if (!this.advancedMode || !this.isEditing || activeNodeId !== this.id) return; e.preventDefault(); e.stopPropagation(); const canvasRect = this.overlayCanvas.getBoundingClientRect(); if (canvasRect.width === 0 || canvasRect.height === 0) return; const scaleX = this.overlayCanvas.width / canvasRect.width; const scaleY = this.overlayCanvas.height / canvasRect.height; const canvasX = (e.clientX - canvasRect.left) * scaleX; const canvasY = (e.clientY - canvasRect.top) * scaleY; const { rectX, rectY } = this.getCanvasMetrics(); const worldX = (canvasX - rectX - this.viewOffsetX) / this.viewScale; const worldY = (canvasY - rectY - this.viewOffsetY) / this.viewScale; const zoomFactor = e.deltaY < 0 ? 1.15 : 0.85; this.viewScale = Math.max(0.05, Math.min(10.0, this.viewScale * zoomFactor)); this.viewOffsetX = canvasX - rectX - (worldX * this.viewScale); this.viewOffsetY = canvasY - rectY - (worldY * this.viewScale); this.setDirtyCanvas(true); }; nodeType.prototype._toggleAdvancedMode = function(forceClose = false) { if (this.currentRenderAbortController) { this.currentRenderAbortController.abort(); this.currentRenderAbortController = null; } if (forceClose || this.advancedMode) { this.updateRelativeFromAbsolute(); this.advancedMode = false; if (activeNodeId === this.id) activeNodeId = null; this.overlayContainer.style.display = 'none'; if (this._overlayWheelHandler) { this.overlayCanvas.removeEventListener('wheel', this._overlayWheelHandler); delete this._overlayWheelHandler; } if (this._overlayMouseHandler) { this.overlayCanvas.removeEventListener('mousedown', this._overlayMouseHandler); this.overlayCanvas.removeEventListener('mousemove', this._overlayMouseHandler); this.overlayCanvas.removeEventListener('mouseup', this._overlayMouseHandler); this.overlayCanvas.removeEventListener('mouseleave', this._overlayMouseLeaveHandler); delete this._overlayMouseHandler; delete this._overlayMouseLeaveHandler; } if (this._globalKeyHandler) window.removeEventListener('keydown', this._globalKeyHandler); if (this.overlayRenderLoop) cancelAnimationFrame(this.overlayRenderLoop); this.setDirtyCanvas(true); return; } if (activeNodeId && activeNodeId !== this.id) { const otherNode = app.graph?.nodes.find(n => n.id == activeNodeId); if (otherNode && typeof otherNode._toggleAdvancedMode === 'function') { otherNode._toggleAdvancedMode(true); } } this.advancedMode = true; activeNodeId = this.id; this.overlayContainer.style.display = 'flex'; requestAnimationFrame(() => { this._resizeOverlayCanvas(); this.computeAndApplyView(); }); this._overlayMouseHandler = (e) => this._handleOverlayEvent(e, e.type === 'mousedown' ? 'down' : e.type === 'mousemove' ? 'move' : 'up'); this._overlayMouseLeaveHandler = () => this.onMouseUp(); this.overlayCanvas.addEventListener('mousedown', this._overlayMouseHandler); this.overlayCanvas.addEventListener('mousemove', this._overlayMouseHandler); this.overlayCanvas.addEventListener('mouseup', this._overlayMouseHandler); this.overlayCanvas.addEventListener('mouseleave', this._overlayMouseLeaveHandler); this._overlayWheelHandler = (e) => this._handleOverlayWheel(e); this.overlayCanvas.addEventListener('wheel', this._overlayWheelHandler, { passive: false }); this._globalKeyHandler = (e) => { if (e.key === 'Escape' && activeNodeId === this.id) { this._toggleAdvancedMode(); } }; window.addEventListener('keydown', this._globalKeyHandler); let lastDrawTime = 0; this.overlayRenderLoop = () => { if (!this.advancedMode || !this.overlayCtx) return; const now = performance.now(); if (now - lastDrawTime < 50) { requestAnimationFrame(this.overlayRenderLoop); return; } lastDrawTime = now; this.overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height); this.drawOverlayCanvas(this.overlayCtx); requestAnimationFrame(this.overlayRenderLoop); }; requestAnimationFrame(this.overlayRenderLoop); }; nodeType.prototype.drawOverlayCanvas = function(ctx) { if (!this.isEditing || !this.backgroundImage) { ctx.fillStyle = "#888"; ctx.font = "14px Arial"; ctx.textAlign = "center"; ctx.fillText("Loading...", this.overlayCanvas.width / 2, this.overlayCanvas.height / 2); return; } const { rectX, rectY } = this.getCanvasMetrics(); const useScale = this.viewScale; const useOffsetX = this.viewOffsetX; const useOffsetY = this.viewOffsetY; ctx.save(); ctx.translate(rectX + useOffsetX, rectY + useOffsetY); ctx.scale(useScale, useScale); ctx.drawImage(this.backgroundImage, -this.displayWidth / 2, -this.displayHeight / 2, this.displayWidth, this.displayHeight); if (this.textMask) { ctx.save(); ctx.translate(this.overlay.x, this.overlay.y); ctx.rotate(this.overlay.rotation * Math.PI / 180); const rectW = this.overlay.width; const rectH = this.overlay.height; this.drawTextOverlay(ctx, -rectW / 2, -rectH / 2, rectW, rectH); if (this.advancedMode) { ctx.shadowColor = "rgba(0,0,0,0.8)"; ctx.shadowBlur = 4 / useScale; ctx.strokeStyle = "#00FF00"; ctx.lineWidth = 1 / useScale; ctx.strokeRect(-rectW / 2, -rectH / 2, rectW, rectH); ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; const hw = rectW / 2, hh = rectH / 2; const hs = 6 / useScale; ctx.fillStyle = "#FF0000"; [[hw, hh], [-hw, hh], [hw, -hh], [-hw, -hh]].forEach(([x, y]) => ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs)); [[hw, 0], [-hw, 0], [0, hh], [0, -hh]].forEach(([x, y]) => ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs)); const rotHandleY = -hh - 40; ctx.beginPath(); ctx.arc(0, rotHandleY, 7 / useScale, 0, Math.PI * 2); ctx.fillStyle = "#ff9800"; ctx.fill(); ctx.strokeStyle = "#fff"; ctx.lineWidth = 1 / useScale; ctx.stroke(); } ctx.restore(); } ctx.restore(); if (this.advancedMode) { ctx.fillStyle = "#ff9800"; ctx.font = "12px Arial"; ctx.textAlign = "left"; ctx.fillText(`EDITING (Scale: ${(useScale * 100).toFixed(0)}%)`, this.overlayCanvas.width - 200, this.overlayCanvas.height - 20); } }; nodeType.prototype.openDeferredEditor = function() { if (!this.pendingEditorData) return; const data = this.pendingEditorData; this.textMask = null; this.outlineMask = null; this._glowMask = null; this._textMaskFile = null; this._glowMaskTimestamp = null; this._glowExtraPadding = 0; this._glowKey = null; this._shadowMask = null; this._shadowKey = null; this.backgroundImage = null; this.isLoading = true; this.currentSessionTimestamp = data.timestamp; this.textParams.text_scale_x = 1.0; this.textParams.text_scale_y = 1.0; this.awaitingServerRender = false; if (this.fontList.length === 0 && data.font_list && data.font_list.length > 0) { this.fontList = data.font_list; if (!this.textParams.font_name) { this.textParams.font_name = data.default_font || this.fontList[0]; } } this.realBackground = { width: data.bg_width, height: data.bg_height }; this.canvasRealWidth = this.realBackground.width; this.canvasRealHeight = this.realBackground.height; this.updateDisplaySize(this.canvasPixelSize); const bgFile = data.bg_file, ts = data.timestamp; const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = () => { this.backgroundImage = img; this.isLoading = false; const initialWidth = this.displayWidth; const initialHeight = this.displayWidth * (3 / 5); this.overlayRelative = { width: initialWidth / this.displayWidth, height: initialHeight / this.displayHeight, x: 0.5, y: 0.5, rotation: 0 }; this.updateOverlayAbsolute(); this.computeAndApplyView(); this.isEditing = true; this.baseWidth = this.overlay.width; this.baseHeight = this.overlay.height; this.setDirtyCanvas(true); this.renderMasks(); this.syncAllWidgets(); }; img.onerror = () => { this.isLoading = false; }; img.src = `/view?filename=${bgFile}&type=temp&t=${ts}`; }; nodeType.prototype.renderMasks = async function(signal = null) { if (!this.isEditing || !this.backgroundImage) return; const scale = this.realBackground.width / this.displayWidth; const containerWidth = Math.round(this.baseWidth * scale); const containerHeight = Math.round(this.baseHeight * scale); if (containerWidth < 10 || containerHeight < 10) return; try { const response = await api.fetchApi("/rayko/rs_overlay_pro/render_masks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ node_id: String(this.id), text_params: this.textParams, container_width: containerWidth, container_height: containerHeight }), signal: signal }); const data = await response.json(); if (data.error) { this.awaitingServerRender = false; return; } let loadedCount = 0; const totalMasks = 2; const checkDone = () => { if (loadedCount === totalMasks) { this.awaitingServerRender = false; this.setDirtyCanvas(true); this.requestGlowMask(); this.requestShadowMask(); } }; const textImg = new Image(); textImg.crossOrigin = "Anonymous"; textImg.onload = () => { this.textMask = textImg; this._textMaskFile = data.text_mask_file; loadedCount++; checkDone(); }; textImg.onerror = () => { loadedCount++; checkDone(); }; textImg.src = `/view?filename=${data.text_mask_file}&type=temp&t=${data.timestamp}`; const outlineImg = new Image(); outlineImg.crossOrigin = "Anonymous"; outlineImg.onload = () => { this.outlineMask = outlineImg; loadedCount++; checkDone(); }; outlineImg.onerror = () => { loadedCount++; checkDone(); }; outlineImg.src = `/view?filename=${data.outline_mask_file}&type=temp&t=${data.timestamp}`; } catch (e) { if (e.name === 'AbortError') return; this.awaitingServerRender = false; } }; nodeType.prototype.updateOverlayAbsolute = function() { this.overlay.x = (this.overlayRelative.x - 0.5) * this.displayWidth; this.overlay.y = (this.overlayRelative.y - 0.5) * this.displayHeight; this.overlay.width = this.overlayRelative.width * this.displayWidth; this.overlay.height = this.overlayRelative.height * this.displayHeight; this.overlay.rotation = this.overlayRelative.rotation; }; nodeType.prototype.updateRelativeFromAbsolute = function() { this.overlayRelative.x = (this.overlay.x / this.displayWidth) + 0.5; this.overlayRelative.y = (this.overlay.y / this.displayHeight) + 0.5; this.overlayRelative.width = this.overlay.width / this.displayWidth; this.overlayRelative.height = this.overlay.height / this.displayHeight; this.overlayRelative.rotation = this.overlay.rotation; }; nodeType.prototype.computeAndApplyView = function() { const bgW = this.displayWidth, bgH = this.displayHeight; const ovW = this.overlay.width, ovH = this.overlay.height; const ovL = this.overlay.x - ovW/2, ovT = this.overlay.y - ovH/2; const ovR = this.overlay.x + ovW/2, ovB = this.overlay.y + ovH/2; const bgL = -bgW/2, bgT = -bgH/2, bgR = bgW/2, bgB = bgH/2; const minX = Math.min(ovL, bgL), minY = Math.min(ovT, bgT), maxX = Math.max(ovR, bgR), maxY = Math.max(ovB, bgB); const contentW = Math.max(1, maxX - minX), contentH = Math.max(1, maxY - minY); const contentCX = (minX + maxX)/2, contentCY = (minY + maxY)/2; let glowPadding = this.glowPaddingPreview || 0; if (this.advancedMode) { const cw = this.overlayCanvas.width || (this.overlayCanvasWrapper ? this.overlayCanvasWrapper.clientWidth : 1000); const ch = this.overlayCanvas.height || (this.overlayCanvasWrapper ? this.overlayCanvasWrapper.clientHeight : 800); const availableW = cw * 0.8, availableH = ch * 0.8; const totalW = contentW + glowPadding*2; const totalH = contentH + glowPadding*2; this.viewScale = Math.max(0.1, Math.min(3.0, Math.min(availableW/totalW, availableH/totalH))); this.viewOffsetX = cw/2 - (contentCX * this.viewScale); this.viewOffsetY = ch/2 - (contentCY * this.viewScale); } else { const availableW = this.canvasPixelSize * 0.95, availableH = this.canvasPixelSize * 0.95; const totalW = contentW + glowPadding*2; const totalH = contentH + glowPadding*2; this.viewScale = Math.max(0.1, Math.min(3.0, Math.min(availableW/totalW, availableH/totalH))); this.viewOffsetX = this.canvasPixelSize/2 - (contentCX * this.viewScale); this.viewOffsetY = this.canvasPixelSize/2 - (contentCY * this.viewScale); } }; nodeType.prototype.updateDisplaySize = function(cS) { this.canvasPixelSize = cS; const safeHeight = this.realBackground.height || 1; const bgAR = this.realBackground.width / safeHeight; if (bgAR >= 1) { this.displayWidth = cS; this.displayHeight = cS / bgAR; } else { this.displayHeight = cS; this.displayWidth = cS * bgAR; } }; nodeType.prototype.getRealTransform = function() { const dS = this.canvasRealWidth / (this.displayWidth || 1); const absX = (this.overlay.x * dS) + (this.canvasRealWidth / 2); const absY = (this.overlay.y * dS) + (this.canvasRealHeight / 2); const scaleX = this.overlay.width / this.displayWidth; const scaleY = this.overlay.height / this.displayHeight; const baseWidthPx = this.baseWidth * dS; const baseHeightPx = this.baseHeight * dS; return { x: absX, y: absY, scale_x: scaleX, scale_y: scaleY, rotation: this.overlay.rotation, base_width_px: baseWidthPx, base_height_px: baseHeightPx }; }; nodeType.prototype.computeScreenHandles = function(rectX, rectY, useScale, useOffsetX, useOffsetY) { const hw = this.overlay.width / 2; const hh = this.overlay.height / 2; const rot = this.overlay.rotation * Math.PI / 180; const cos = Math.cos(rot), sin = Math.sin(rot); const handles = { 'scale-tl': [-hw, -hh], 'scale-tr': [hw, -hh], 'scale-bl': [-hw, hh], 'scale-br': [hw, hh], 'scale-t': [0, -hh], 'scale-b': [0, hh], 'scale-l': [-hw, 0], 'scale-r': [hw, 0], 'rotate': [0, -hh - 40] }; const screenHandles = {}; for (const [name, loc] of Object.entries(handles)) { const rx = loc[0]*cos - loc[1]*sin, ry = loc[0]*sin + loc[1]*cos; screenHandles[name] = { x: rectX + useOffsetX + (this.overlay.x + rx)*useScale, y: rectY + useOffsetY + (this.overlay.y + ry)*useScale }; } return screenHandles; }; nodeType.prototype.sendTransforms = async function() { if (this.currentRenderAbortController) { this.currentRenderAbortController.abort(); this.currentRenderAbortController = null; } const transform = this.getRealTransform(); const scale = this.realBackground.width / this.displayWidth; const textParamsWithBase = { ...this.textParams, base_width_px: transform.base_width_px, base_height_px: transform.base_height_px, shadow_offset_x: Math.round(this.textParams.shadow_offset_x * scale), shadow_offset_y: Math.round(this.textParams.shadow_offset_y * scale), shadow_blur: Math.round((this.textParams.shadow_blur || 0) * scale) }; const payload = { id: String(this.id), transforms: transform, text_params: textParamsWithBase }; try { await api.fetchApi("/rayko/rs_overlay_pro", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); this.isEditing = false; this.setDirtyCanvas(true); } catch(e) {} }; nodeType.prototype.cancelEditing = async function() { if (this.currentRenderAbortController) { this.currentRenderAbortController.abort(); this.currentRenderAbortController = null; } if (this._glowMaskTimeout) { clearTimeout(this._glowMaskTimeout); this._glowMaskTimeout = null; } if (this._glowMaskAbortController) { this._glowMaskAbortController.abort(); this._glowMaskAbortController = null; } if (this._shadowMaskTimeout) { clearTimeout(this._shadowMaskTimeout); this._shadowMaskTimeout = null; } if (this._shadowMaskAbortController) { this._shadowMaskAbortController.abort(); this._shadowMaskAbortController = null; } try { await api.interrupt(); } catch(e) {} await fetch("/rayko/rs_overlay_pro/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ node_id: String(this.id) }) }); this.isEditing = false; this.isLoading = false; this.dragType = null; this.dragState = null; this.awaitingServerRender = false; this._glowMask = null; this._textMaskFile = null; this._glowExtraPadding = 0; this._glowKey = null; this._shadowMask = null; this._shadowKey = null; this.setDirtyCanvas(true); this.closeTextarea(); }; nodeType.prototype.roundRect = function(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); }; nodeType.prototype.getNodeScreenCoords = function(localX, localY) { try { const canvas = LGraphCanvas.active_canvas; if (!canvas) return { x: 0, y: 0 }; const rect = canvas.canvas.getBoundingClientRect(); const ds = canvas.ds; return { x: rect.left + (this.pos[0] + localX) * ds.scale + ds.offset[0], y: rect.top + (this.pos[1] + localY) * ds.scale + ds.offset[1] }; } catch(e) { return { x: 0, y: 0 }; } }; nodeType.prototype.openTextarea = function(clickEvent) { if (this.textareaState.isOpen && this.textareaState.element) { this.textareaState.element.remove(); } const currentValue = this.textParams.text || ''; const popup = document.createElement('div'); popup.style.cssText = 'position:fixed;z-index:10002;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:10px;box-shadow:0 4px 20px rgba(0,0,0,0.5);'; const input = document.createElement('textarea'); input.value = currentValue; input.style.cssText = 'width:300px;height:150px;background:#222;color:#fff;border:1px solid #444;border-radius:4px;padding:10px;font-size:12px;resize:none;display:block;margin-bottom:10px;'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'SAVE'; saveBtn.style.cssText = 'background:#4CAF50;color:#fff;border:none;border-radius:4px;padding:8px 16px;font-size:14px;cursor:pointer;float:right;'; saveBtn.onmouseover = () => saveBtn.style.background = "#45a049"; saveBtn.onmouseout = () => saveBtn.style.background = "#4CAF50"; popup.appendChild(input); popup.appendChild(saveBtn); if (clickEvent) { popup.style.left = (clickEvent.clientX + 8) + 'px'; popup.style.top = clickEvent.clientY + 'px'; } document.body.appendChild(popup); setTimeout(() => { input.focus(); setTimeout(() => { if (currentValue && currentValue.length > 0) input.select(); }, 10); }, 50); saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); this.textParams.text = input.value; popup.remove(); this.textareaState.isOpen = false; this.textareaState.element = null; this.setDirtyCanvas(true); if (this.scheduleRender) this.scheduleRender('text'); this.syncAllWidgets(); }; input.onkeydown = (ev) => { if (ev.key === 'Enter' && ev.ctrlKey) { ev.preventDefault(); this.textParams.text = input.value; popup.remove(); this.textareaState.isOpen = false; this.textareaState.element = null; this.setDirtyCanvas(true); if (this.scheduleRender) this.scheduleRender('text'); this.syncAllWidgets(); } }; this.textareaState.isOpen = true; this.textareaState.element = popup; }; nodeType.prototype.closeTextarea = function() { if (this.textareaState.element) { this.textareaState.element.remove(); this.textareaState.isOpen = false; this.textareaState.element = null; } }; nodeType.prototype.showFontSelector = function(clickEvent) { if (this.fontMenu) { this.fontMenu.remove(); this.fontMenu = null; } const list = this.fontList || []; if (!list.length) return; const buttonBottomLeft = this.getNodeScreenCoords( this.fontSelectRect.x, this.fontSelectRect.y + this.fontSelectRect.h ); const buttonTopLeft = this.getNodeScreenCoords( this.fontSelectRect.x, this.fontSelectRect.y ); const menu = document.createElement("div"); menu.style.cssText = 'position:fixed;background:#1a1a1a;border:1px solid #444;border-radius:6px;max-height:300px;overflow:hidden;z-index:10001;box-shadow:0 4px 20px rgba(0,0,0,0.5);min-width:200px;display:flex;flex-direction:column;'; this.fontMenu = menu; const searchInput = document.createElement("input"); searchInput.type = "text"; searchInput.placeholder = "Search fonts..."; searchInput.value = ""; searchInput.style.cssText = 'width:100%;background:#222;color:#eee;border:none;border-bottom:1px solid #333;border-radius:6px 6px 0 0;padding:8px 12px;font-size:12px;outline:none;box-sizing:border-box;'; const listContainer = document.createElement("div"); listContainer.style.cssText = 'overflow-y:auto;flex:1;'; const renderList = (filter) => { listContainer.innerHTML = ''; const lowerFilter = filter.toLowerCase(); const filtered = list.filter(f => f.toLowerCase().includes(lowerFilter)); if (filtered.length === 0) { const empty = document.createElement("div"); empty.textContent = "No matches"; empty.style.cssText = 'padding:10px 15px;color:#666;font-size:12px;font-style:italic;'; listContainer.appendChild(empty); return; } filtered.forEach(opt => { const item = document.createElement("div"); item.textContent = opt; item.style.cssText = 'padding:10px 15px;cursor:pointer;color:#ddd;font-size:12px;border-bottom:1px solid #333;'; item.onmouseover = () => item.style.background = "#333"; item.onmouseout = () => item.style.background = "#1a1a1a"; item.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); this.textParams.font_name = opt; menu.remove(); this.fontMenu = null; this.setDirtyCanvas(true); if (this.scheduleRender) this.scheduleRender('font_name'); this.syncAllWidgets(); }; listContainer.appendChild(item); }); }; searchInput.addEventListener('input', (e) => { renderList(e.target.value); }); searchInput.addEventListener('mousedown', (e) => { e.stopPropagation(); }); searchInput.addEventListener('keydown', (e) => { e.stopPropagation(); if (e.key === 'Escape') { menu.remove(); this.fontMenu = null; } }); menu.appendChild(searchInput); menu.appendChild(listContainer); menu.style.left = buttonBottomLeft.x + 'px'; menu.style.top = buttonBottomLeft.y + 'px'; document.body.appendChild(menu); renderList(''); const menuHeight = menu.offsetHeight; const menuWidth = menu.offsetWidth; let finalLeft = buttonBottomLeft.x; let finalTop = buttonBottomLeft.y; if (finalTop + menuHeight > window.innerHeight - 10) { finalTop = buttonTopLeft.y - menuHeight; if (finalTop < 10) finalTop = 10; } if (finalLeft + menuWidth > window.innerWidth - 10) { finalLeft = window.innerWidth - menuWidth - 10; } menu.style.left = finalLeft + 'px'; menu.style.top = finalTop + 'px'; setTimeout(() => { searchInput.focus(); }, 50); setTimeout(() => { const closeHandler = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); this.fontMenu = null; document.removeEventListener("mousedown", closeHandler); } }; document.addEventListener("mousedown", closeHandler); }, 100); }; nodeType.prototype.showHexEditor = function() { if (this.hexEditor) { this.hexEditor.remove(); this.hexEditor = null; } const currentValue = this.textParams.text_color || '#FFFFFF'; const pos = this.getNodeScreenCoords(this.hexRect.x, this.hexRect.y); const container = document.createElement('div'); container.style.cssText = 'position:fixed;z-index:10002;background:#1a1a1a;border:1px solid #4CAF50;border-radius:4px;padding:6px 8px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:6px;'; const input = document.createElement('input'); input.type = 'text'; input.value = currentValue; input.style.cssText = 'width:80px;background:#222;color:#fff;border:1px solid #444;border-radius:3px;padding:4px 8px;font-size:11px;font-family:monospace;outline:none;text-transform:uppercase;'; const okBtn = document.createElement('button'); okBtn.textContent = 'OK'; okBtn.style.cssText = 'background:#2196F3;color:#fff;border:none;border-radius:3px;padding:4px 10px;font-size:11px;cursor:pointer;'; okBtn.onmouseover = () => okBtn.style.background = "#1976D2"; okBtn.onmouseout = () => okBtn.style.background = "#2196F3"; container.appendChild(input); container.appendChild(okBtn); document.body.appendChild(container); const containerHeight = container.offsetHeight; container.style.left = pos.x + 'px'; container.style.top = (pos.y - containerHeight * 1.5) + 'px'; this.hexEditor = container; const isValidHex = (val) => { const cleaned = val.replace('#', ''); return /^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(cleaned); }; const normalizeHex = (val) => { let cleaned = val.replace('#', '').toUpperCase(); if (cleaned.length === 3) { cleaned = cleaned.split('').map(c => c + c).join(''); } return '#' + cleaned; }; input.addEventListener('input', (e) => { const val = e.target.value; if (isValidHex(val)) { input.style.borderColor = '#4CAF50'; const normalized = normalizeHex(val); this.textParams.text_color = normalized; this.setDirtyCanvas(true); } else { input.style.borderColor = '#dc3545'; } }); const applyAndClose = () => { const val = input.value; if (isValidHex(val)) { this.textParams.text_color = normalizeHex(val); this.setDirtyCanvas(true); } container.remove(); this.hexEditor = null; }; okBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); applyAndClose(); }; input.addEventListener('keydown', (e) => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); applyAndClose(); } else if (e.key === 'Escape') { e.preventDefault(); container.remove(); this.hexEditor = null; } }); input.addEventListener('blur', () => { setTimeout(() => { if (this.hexEditor === container) { applyAndClose(); } }, 100); }); setTimeout(() => { input.focus(); input.select(); }, 50); }; nodeType.prototype.openColorPicker = function(clickEvent) { const currentColor = this.textParams.text_color || '#FFFFFF'; const colorInput = document.createElement('input'); colorInput.type = 'color'; colorInput.value = currentColor; colorInput.style.display = 'none'; document.body.appendChild(colorInput); colorInput.addEventListener('change', (e) => { const newColor = e.target.value.toUpperCase(); this.textParams.text_color = newColor; this.setDirtyCanvas(true); if (this.scheduleRender) this.scheduleRender('text_color'); this.syncAllWidgets(); colorInput.remove(); }, { once: true }); setTimeout(() => { if (colorInput.showPicker) { colorInput.showPicker(); } else { colorInput.click(); } }, 10); }; nodeType.prototype.updateLineSpacingFromMouse = function(localX) { if (!this.lineSpacingTrackRect) return; const { x: trackX, w: trackW } = this.lineSpacingTrackRect; let ratio = (localX - trackX) / trackW; ratio = Math.max(0, Math.min(1, ratio)); const newValue = 0.5 + ratio * 2.5; this.textParams.line_spacing = Math.round(newValue * 20) / 20; this.setDirtyCanvas(true); if (this.scheduleRender) this.scheduleRender('line_spacing'); this.syncAllWidgets(); }; nodeType.prototype.onResize = function(size) { if (size[0] < this.minWidth) size[0] = this.minWidth; const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30; const canvasTopPadding = 30; const cSize = Math.max(200, size[0] - 40); this.canvasPixelSize = cSize; const textareaH = 60; const alignH = 27; const fontColorH = 27; const btnH = 30; const gaps = 10 + 8 + 8 + 20 + 15; const neededHeight = titleH + canvasTopPadding + cSize + textareaH + alignH + fontColorH + btnH + gaps; if (size[1] < neededHeight) size[1] = neededHeight; this.setDirtyCanvas(true); if (this.isEditing && this.backgroundImage) { this.updateDisplaySize(cSize); this.updateOverlayAbsolute(); this.computeAndApplyView(); } }; nodeType.prototype.getCanvasMetrics = function() { const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30; const canvasTopPadding = 30; let cSize, rectX, rectY; if (this.advancedMode) { const w = this.overlayCanvas.width || (this.overlayCanvasWrapper ? this.overlayCanvasWrapper.clientWidth : 1000); const h = this.overlayCanvas.height || (this.overlayCanvasWrapper ? this.overlayCanvasWrapper.clientHeight : 800); cSize = Math.min(w, h); rectX = 0; rectY = 0; } else { cSize = Math.max(200, this.size[0] - 40); rectX = (this.size[0] - cSize) / 2; rectY = titleH + canvasTopPadding; } return { cSize, rectX, rectY }; }; nodeType.prototype.drawAlignIcon = function(ctx, type, x, y, w, h, isActive) { const lineHeight = 2; const gap = 3; const totalH = lineHeight*3 + gap*2; const startY = y + (h - totalH)/2; ctx.fillStyle = isActive ? "#4CAF50" : "#aaa"; if (type === "left") { ctx.fillRect(x, startY, w*0.9, lineHeight); ctx.fillRect(x, startY + lineHeight + gap, w*0.7, lineHeight); ctx.fillRect(x, startY + (lineHeight+gap)*2, w*0.5, lineHeight); } else if (type === "center") { const w1 = w*0.9, w2 = w*0.7, w3 = w*0.5; ctx.fillRect(x + (w-w1)/2, startY, w1, lineHeight); ctx.fillRect(x + (w-w2)/2, startY + lineHeight + gap, w2, lineHeight); ctx.fillRect(x + (w-w3)/2, startY + (lineHeight+gap)*2, w3, lineHeight); } else if (type === "right") { ctx.fillRect(x + w - w*0.9, startY, w*0.9, lineHeight); ctx.fillRect(x + w - w*0.7, startY + lineHeight + gap, w*0.7, lineHeight); ctx.fillRect(x + w - w*0.5, startY + (lineHeight+gap)*2, w*0.5, lineHeight); } }; nodeType.prototype.drawLineSpacingIcon = function(ctx, x, y, w, h) { const lineY1 = y + h*0.3; const lineY2 = y + h*0.7; const arrowX = x + w/2; ctx.strokeStyle = "#aaa"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(x+2, lineY1); ctx.lineTo(x+w-2, lineY1); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x+2, lineY2); ctx.lineTo(x+w-2, lineY2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(arrowX, lineY1+2); ctx.lineTo(arrowX, lineY2-2); ctx.stroke(); ctx.fillStyle = "#aaa"; ctx.beginPath(); ctx.moveTo(arrowX, lineY1+1); ctx.lineTo(arrowX-2, lineY1+4); ctx.lineTo(arrowX+2, lineY1+4); ctx.fill(); ctx.beginPath(); ctx.moveTo(arrowX, lineY2-1); ctx.lineTo(arrowX-2, lineY2-4); ctx.lineTo(arrowX+2, lineY2-4); ctx.fill(); }; nodeType.prototype.onDrawForeground = function(ctx) { if (this.advancedMode) { ctx.clearRect(0, 0, this.size[0], this.size[1]); return; } const { cSize, rectX, rectY } = this.getCanvasMetrics(); ctx.fillStyle = "#1e1e1e"; ctx.fillRect(rectX, rectY, cSize, cSize); ctx.strokeStyle = "#555"; ctx.strokeRect(rectX, rectY, cSize, cSize); this.updateDisplaySize(cSize); if (this.isEditing && this.backgroundImage) { this.updateOverlayAbsolute(); this.computeAndApplyView(); } if (!this.dragState) { this.updateOverlayAbsolute(); this.computeAndApplyView(); } if (this.isLoading) { ctx.fillStyle = "#888"; ctx.font = "12px Arial"; ctx.fillText("Loading...", rectX + cSize/2 - 35, rectY + cSize/2); } else if (this.isEditing && this.backgroundImage) { const useScale = this.dragState ? this.dragState.viewScale : this.viewScale; const useOffsetX = this.dragState ? this.dragState.viewOffsetX : this.viewOffsetX; const useOffsetY = this.dragState ? this.dragState.viewOffsetY : this.viewOffsetY; ctx.save(); ctx.translate(rectX + useOffsetX, rectY + useOffsetY); ctx.scale(useScale, useScale); ctx.drawImage(this.backgroundImage, -this.displayWidth/2, -this.displayHeight/2, this.displayWidth, this.displayHeight); if (this.textMask) { ctx.save(); ctx.translate(this.overlay.x, this.overlay.y); ctx.rotate(this.overlay.rotation * Math.PI / 180); const rectW = this.overlay.width; const rectH = this.overlay.height; this.drawTextOverlay(ctx, -rectW / 2, -rectH / 2, rectW, rectH); if (this.advancedMode) { ctx.shadowColor = "rgba(0,0,0,0.8)"; ctx.shadowBlur = 4/useScale; ctx.strokeStyle = "#00FF00"; ctx.lineWidth = 1/useScale; ctx.strokeRect(-rectW/2, -rectH/2, rectW, rectH); ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; const hw = rectW/2, hh = rectH/2, hs = 6/useScale; ctx.fillStyle = "#FF0000"; [[hw, hh], [-hw, hh], [hw, -hh], [-hw, -hh]].forEach(([x, y]) => ctx.fillRect(x - hs/2, y - hs/2, hs, hs)); [[hw, 0], [-hw, 0], [0, hh], [0, -hh]].forEach(([x, y]) => ctx.fillRect(x - hs/2, y - hs/2, hs, hs)); const rotHandleY = -hh - 40; ctx.beginPath(); ctx.arc(0, rotHandleY, 7/useScale, 0, Math.PI*2); ctx.fillStyle = "#ff9800"; ctx.fill(); ctx.strokeStyle = "#fff"; ctx.lineWidth = 1/useScale; ctx.stroke(); } ctx.restore(); } ctx.restore(); if (this.advancedMode) { ctx.fillStyle = "#ff9800"; ctx.font = "12px Arial"; ctx.fillText(`EDITING (Scale: ${(useScale*100).toFixed(0)}%)`, rectX + cSize - 160, rectY + cSize - 10); } } else { ctx.fillStyle = "#888"; ctx.font = "12px Arial"; ctx.fillText("▶ Run queue to start", rectX + cSize/2 - 65, rectY + cSize/2); } const toggleBtnW = 150, toggleBtnH = 24, toggleBtnX = (this.size[0] - toggleBtnW)/2, toggleBtnY = 20; ctx.fillStyle = "#2a2a2a"; ctx.strokeStyle = "#2196F3"; ctx.lineWidth = 2; this.roundRect(ctx, toggleBtnX, toggleBtnY, toggleBtnW, toggleBtnH, 6); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#2196F3"; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; ctx.fillText("🔍 ADVANCED MODE", toggleBtnX + toggleBtnW/2, toggleBtnY + toggleBtnH/2 + 4); const widgetX = 15; const widgetW = this.size[0] - 30; const textareaY = rectY + cSize + 10; const textareaH = 60; this.textareaRect = { x: widgetX, y: textareaY, w: widgetW, h: textareaH }; ctx.fillStyle = this.textareaHover ? "#2a2a2a" : "#252525"; ctx.strokeStyle = this.textareaHover ? "#4CAF50" : "#444"; ctx.lineWidth = 1; this.roundRect(ctx, widgetX, textareaY, widgetW, textareaH, 4); ctx.fill(); ctx.stroke(); ctx.save(); ctx.beginPath(); ctx.rect(widgetX+1, textareaY+1, widgetW-2, textareaH-2); ctx.clip(); ctx.font = "12px monospace"; ctx.textAlign = "left"; ctx.textBaseline = "top"; const text = this.textParams.text || ""; const lines = text.split('\n'); const maxLines = 3; const lineHeight = 16; const paddingX = 8; const paddingY = 6; if (!text) { ctx.fillStyle = "#666"; ctx.fillText("Your text…", widgetX+paddingX, textareaY+paddingY); } else { for (let i = 0; i < Math.min(lines.length, maxLines); i++) { let line = lines[i]; const maxWidth = widgetW - paddingX*2; while (ctx.measureText(line).width > maxWidth && line.length > 0) { line = line.slice(0, -1); } if (line !== lines[i]) line += "…"; ctx.fillStyle = "#aaa"; ctx.fillText(line, widgetX+paddingX, textareaY+paddingY+i*lineHeight); } if (lines.length > maxLines) { ctx.fillStyle = "#666"; ctx.fillText("…", widgetX+paddingX, textareaY+paddingY+maxLines*lineHeight); } } ctx.restore(); const alignY = textareaY + textareaH + 8; const alignH = 27; const padding = 5; const availableW = widgetW - padding*2; const sliderW = availableW * 0.7; const sliderX = widgetX + padding; this.lineSpacingRect = { x: sliderX, y: alignY, w: sliderW, h: alignH }; const btnW = availableW * 0.1; const btnGap = 4; const btnStartX = sliderX + sliderW + btnGap; const alignTypes = ["left", "center", "right"]; this.alignButtonsRects = []; const isMultiline = text.includes('\n'); ctx.fillStyle = this.lineSpacingHover ? "#2a2a2a" : "#252525"; ctx.strokeStyle = this.lineSpacingHover ? "#4CAF50" : "#444"; ctx.lineWidth = 1; this.roundRect(ctx, sliderX, alignY, sliderW, alignH, 4); ctx.fill(); ctx.stroke(); this.drawLineSpacingIcon(ctx, sliderX+6, alignY+6, 14, 15); const trackX = sliderX + 24; const trackW = sliderW - 70; const trackY = alignY + alignH/2; const trackH = 3; this.lineSpacingTrackRect = { x: trackX, y: alignY, w: trackW, h: alignH }; ctx.fillStyle = "#444"; ctx.fillRect(trackX, trackY - trackH/2, trackW, trackH); const lineSpacingVal = parseFloat(this.textParams.line_spacing) || 1.0; const fillRatio = Math.max(0, Math.min(1, (lineSpacingVal - 0.5)/2.5)); const fillW = trackW * fillRatio; ctx.fillStyle = "#4CAF50"; ctx.fillRect(trackX, trackY - trackH/2, fillW, trackH); const handleX = trackX + fillW; const handleSize = 8; ctx.fillStyle = "#fff"; ctx.beginPath(); ctx.arc(handleX, trackY, handleSize/2, 0, Math.PI*2); ctx.fill(); const valueW = 42; const valueX = sliderX + sliderW - valueW - 4; this.lineSpacingValueRect = { x: valueX, y: alignY, w: valueW, h: alignH }; ctx.fillStyle = this.lineSpacingValueHover ? "#2a2a2a" : "#222"; ctx.strokeStyle = this.lineSpacingValueHover ? "#4CAF50" : "#444"; ctx.lineWidth = 1; this.roundRect(ctx, valueX, alignY+3, valueW, alignH-6, 3); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#4CAF50"; ctx.font = "10px monospace"; ctx.textAlign = "center"; ctx.fillText(lineSpacingVal.toFixed(2), valueX+valueW/2, alignY+alignH/2+3); for (let i = 0; i < 3; i++) { const bx = btnStartX + i*(btnW+btnGap); const isActive = this.textParams.text_align === alignTypes[i]; const isHover = this.alignButtonsHover[i]; const isEnabled = isMultiline; this.alignButtonsRects.push({ x: bx, y: alignY, w: btnW, h: alignH, type: alignTypes[i], enabled: isEnabled }); ctx.fillStyle = isHover && isEnabled ? "#2a2a2a" : "#252525"; ctx.strokeStyle = isActive && isEnabled ? "#4CAF50" : (isHover && isEnabled ? "#4CAF50" : "#444"); ctx.lineWidth = isActive && isEnabled ? 2 : 1; ctx.globalAlpha = isEnabled ? 1.0 : 0.4; this.roundRect(ctx, bx, alignY, btnW, alignH, 4); ctx.fill(); ctx.stroke(); ctx.globalAlpha = 1.0; if (isEnabled) { this.drawAlignIcon(ctx, alignTypes[i], bx + (btnW-14)/2, alignY + (alignH-10)/2, 14, 10, isActive); } } const fontColorY = alignY + alignH + 8; const fontColorH = 27; const gap = 8; const fontSelectW = Math.floor((widgetW - gap) * 0.6); const colorPickerW = widgetW - fontSelectW - gap; const fontSelectX = widgetX; const colorPickerX = widgetX + fontSelectW + gap; this.fontColorY = fontColorY; this.fontSelectRect = { x: fontSelectX, y: fontColorY, w: fontSelectW, h: fontColorH }; ctx.fillStyle = this.fontSelectHover ? "#2a2a2a" : "#252525"; ctx.strokeStyle = this.fontSelectHover ? "#4CAF50" : "#444"; ctx.lineWidth = 1; this.roundRect(ctx, fontSelectX, fontColorY, fontSelectW, fontColorH, 4); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#fff"; ctx.font = "11px sans-serif"; ctx.textAlign = "center"; const fontName = this.textParams.font_name || "Fonts not found"; ctx.fillText(fontName, fontSelectX+fontSelectW/2, fontColorY+fontColorH/2+4); ctx.fillStyle = "#666"; ctx.beginPath(); ctx.moveTo(fontSelectX+fontSelectW-12, fontColorY+fontColorH/2-3); ctx.lineTo(fontSelectX+fontSelectW-6, fontColorY+fontColorH/2-3); ctx.lineTo(fontSelectX+fontSelectW-9, fontColorY+fontColorH/2+3); ctx.fill(); const colorBoxSize = 24; const colorBoxY = fontColorY + Math.round((fontColorH - colorBoxSize)/2); const hexW = colorPickerW - colorBoxSize - 5; const hexX = colorPickerX; const colorBoxX = colorPickerX + hexW + 5; this.hexRect = { x: hexX, y: fontColorY, w: hexW, h: fontColorH }; this.colorBoxRect = { x: colorBoxX, y: colorBoxY, w: colorBoxSize, h: colorBoxSize }; ctx.fillStyle = this.hexHover ? "#2a2a2a" : "#252525"; ctx.fillRect(hexX, fontColorY, hexW, fontColorH); ctx.strokeStyle = this.hexHover ? "#4CAF50" : "#444"; ctx.lineWidth = 1; ctx.strokeRect(hexX, fontColorY, hexW, fontColorH); ctx.fillStyle = "#fff"; ctx.font = "11px sans-serif"; ctx.textAlign = "center"; ctx.fillText(this.textParams.text_color || "#FFFFFF", hexX+hexW/2, fontColorY+fontColorH/2+4); ctx.fillStyle = this.textParams.text_color || "#FFFFFF"; this.roundRect(ctx, colorBoxX, colorBoxY, colorBoxSize, colorBoxSize, 4); ctx.fill(); ctx.strokeStyle = this.colorBoxHover ? "#4CAF50" : "#fff"; ctx.lineWidth = 1.5; this.roundRect(ctx, colorBoxX, colorBoxY, colorBoxSize, colorBoxSize, 4); ctx.stroke(); const buttonsY = fontColorY + fontColorH + 20; this.buttonsY = buttonsY; const btnH = 30; const btnGap2 = 10; const btnW2 = (this.size[0] - 35) / 2; [[15, buttonsY, "✔️ APPLY", this.btnApplyHover, "#4CAF50"], [15+btnW2+btnGap2, buttonsY, " CANCEL", this.btnCancelHover, "#dc3545"]].forEach(([bx, by, txt, hov, col]) => { ctx.fillStyle = hov ? "#444" : "#2a2a2a"; this.roundRect(ctx, bx, by, btnW2, btnH, 6); ctx.fill(); ctx.strokeStyle = col; ctx.stroke(); ctx.fillStyle = col; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; ctx.fillText(txt, bx+btnW2/2, by+btnH/2+4); }); }; nodeType.prototype.onMouseDown = function(event, pos) { if (!this.advancedMode && pos) { const toggleBtnW = 150, toggleBtnH = 24, toggleBtnX = (this.size[0] - toggleBtnW)/2, toggleBtnY = 20; if (pos[0] >= toggleBtnX && pos[0] <= toggleBtnX + toggleBtnW && pos[1] >= toggleBtnY && pos[1] <= toggleBtnY + toggleBtnH) { this._toggleAdvancedMode(); return true; } } if (this.textareaRect) { const { x, y, w, h } = this.textareaRect; if (pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h) { const canvas = LGraphCanvas.active_canvas; if (canvas) { const rect = canvas.canvas.getBoundingClientRect(); const ds = canvas.ds; this.lastClickCoords = { clientX: rect.left + (this.pos[0] + pos[0]) * ds.scale + ds.offset[0], clientY: rect.top + (this.pos[1] + pos[1]) * ds.scale + ds.offset[1] }; } this.openTextarea(this.lastClickCoords); return true; } } if (this.lineSpacingValueRect) { const { x, y, w, h } = this.lineSpacingValueRect; if (pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h) { if (this.lineSpacingPopup) { this.lineSpacingPopup.remove(); this.lineSpacingPopup = null; } const currentValue = this.textParams.line_spacing; const popup = document.createElement('div'); popup.style.cssText = 'position:fixed;z-index:10003;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:8px 12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:8px;'; const input = document.createElement('input'); input.type = 'number'; input.value = currentValue; input.min = 0.5; input.max = 3.0; input.step = 0.05; input.style.cssText = 'width:100px;background:#222;color:#fff;border:1px solid #444;border-radius:4px;padding:6px 10px;font-size:12px;outline:none;'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'OK'; saveBtn.style.cssText = 'background:#2196F3;color:#fff;border:none;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer;'; const doSave = () => { let num = parseFloat(input.value); if (isNaN(num)) num = currentValue; num = Math.max(0.5, Math.min(3.0, num)); this.textParams.line_spacing = num; popup.remove(); this.lineSpacingPopup = null; if (this.scheduleRender) this.scheduleRender('line_spacing'); this.syncAllWidgets(); }; saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); }; input.onkeydown = (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); doSave(); } }; popup.appendChild(input); popup.appendChild(saveBtn); document.body.appendChild(popup); const screenPos = this.getNodeScreenCoords(x, y); const popupWidth = popup.offsetWidth; const popupHeight = popup.offsetHeight; let leftPos = screenPos.x; let topPos = screenPos.y - popupHeight - 8; if (topPos < 8) topPos = screenPos.y + 8; if (leftPos + popupWidth > window.innerWidth - 8) leftPos = window.innerWidth - popupWidth - 8; popup.style.left = leftPos + 'px'; popup.style.top = topPos + 'px'; this.lineSpacingPopup = popup; setTimeout(() => { input.focus(); input.select(); }, 50); setTimeout(() => { const closeHandler = (ev) => { if (!popup.contains(ev.target)) { popup.remove(); this.lineSpacingPopup = null; document.removeEventListener('mousedown', closeHandler); } }; document.addEventListener('mousedown', closeHandler); }, 100); return true; } } if (this.lineSpacingTrackRect) { const { x, y, w, h } = this.lineSpacingTrackRect; if (pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h) { this.lineSpacingDragging = true; this.updateLineSpacingFromMouse(pos[0]); return true; } } if (this.alignButtonsRects && this.alignButtonsRects.length === 3) { for (let i = 0; i < 3; i++) { const { x, y, w, h, type, enabled } = this.alignButtonsRects[i]; if (enabled && pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h) { this.textParams.text_align = type; this.setDirtyCanvas(true); if (this.scheduleRender) this.scheduleRender('text_align'); this.syncAllWidgets(); return true; } } } if (this.fontSelectRect) { const { x, y, w, h } = this.fontSelectRect; if (pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h) { this.showFontSelector(); return true; } } if (this.hexRect) { const { x, y, w, h } = this.hexRect; if (pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h) { this.showHexEditor(); return true; } } if (this.colorBoxRect) { const { x, y, w, h } = this.colorBoxRect; if (pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h) { this.openColorPicker(); return true; } } if (pos && this.buttonsY !== undefined) { const btnH = 30; const btnGap = 10; const btnW = (this.size[0] - 35) / 2; const y1 = this.buttonsY; if (pos[0] >= 15 && pos[0] <= 15 + btnW && pos[1] >= y1 && pos[1] <= y1 + btnH) { this.sendTransforms(); return true; } if (pos[0] >= 15 + btnW + btnGap && pos[0] <= 15 + btnW + btnGap + btnW && pos[1] >= y1 && pos[1] <= y1 + btnH) { this.cancelEditing(); return true; } } if (!this.advancedMode) return false; if (!this.isEditing || this.isLoading || !this.textMask) return; const { cSize, rectX, rectY } = this.getCanvasMetrics(); const mx = pos[0], my = pos[1]; const frozenScale = this.viewScale, frozenOffsetX = this.viewOffsetX, frozenOffsetY = this.viewOffsetY; const worldMx = (mx - rectX - frozenOffsetX) / frozenScale, worldMy = (my - rectY - frozenOffsetY) / frozenScale; const screenHandles = this.computeScreenHandles(rectX, rectY, frozenScale, frozenOffsetX, frozenOffsetY); const cornerSize = 14, edgeSize = 18, rotateSize = 26; let detectedType = null, minDist = Infinity; const checkHandle = (name, h, threshold) => { const dist = Math.hypot(mx - h.x, my - h.y); if (dist < threshold && dist < minDist) { detectedType = name; minDist = dist; } }; for (const [name, hPos] of Object.entries(screenHandles)) { const isEdge = ['scale-t', 'scale-b', 'scale-l', 'scale-r'].includes(name); const threshold = name === 'rotate' ? rotateSize : (isEdge ? edgeSize : cornerSize); checkHandle(name, hPos, threshold); } this.dragType = detectedType; if (this.dragType) { this.isDragging = true; this.awaitingServerRender = false; const startW = this.overlay.width; const startH = this.overlay.height; const startRot = this.overlay.rotation * Math.PI / 180; const startCX = this.overlay.x; const startCY = this.overlay.y; const localPoints = { 'scale-br': { active: [startW/2, startH/2], fixed: [-startW/2, -startH/2] }, 'scale-bl': { active: [-startW/2, startH/2], fixed: [startW/2, -startH/2] }, 'scale-tr': { active: [startW/2, -startH/2], fixed: [-startW/2, startH/2] }, 'scale-tl': { active: [-startW/2, -startH/2], fixed: [startW/2, startH/2] }, 'scale-r': { active: [startW/2, 0], fixed: [-startW/2, 0] }, 'scale-l': { active: [-startW/2, 0], fixed: [startW/2, 0] }, 'scale-b': { active: [0, startH/2], fixed: [0, -startH/2] }, 'scale-t': { active: [0, -startH/2], fixed: [0, startH/2] } }; const pts = localPoints[this.dragType]; const fixedLocal = pts ? { x: pts.fixed[0], y: pts.fixed[1] } : { x: 0, y: 0 }; const activeLocal = pts ? { x: pts.active[0], y: pts.active[1] } : { x: 0, y: 0 }; this.dragState = { startMouseX: worldMx, startMouseY: worldMy, startX: this.overlay.x, startY: this.overlay.y, startW: startW, startH: startH, startRotation: this.overlay.rotation, startRot: startRot, startCX: startCX, startCY: startCY, viewScale: frozenScale, viewOffsetX: frozenOffsetX, viewOffsetY: frozenOffsetY, fixedLocal: fixedLocal, activeLocal: activeLocal }; return true; } const halfW = this.overlay.width / 2; const halfH = this.overlay.height / 2; const dx = worldMx - this.overlay.x, dy = worldMy - this.overlay.y; const rotRad = -this.overlay.rotation * Math.PI / 180; const localX = dx * Math.cos(rotRad) - dy * Math.sin(rotRad), localY = dx * Math.sin(rotRad) + dy * Math.cos(rotRad); if (Math.abs(localX) < halfW && Math.abs(localY) < halfH) { this.dragType = 'move'; this.dragState = { startMouseX: worldMx, startMouseY: worldMy, startX: this.overlay.x, startY: this.overlay.y, startW: this.overlay.width, startH: this.overlay.height, startRotation: this.overlay.rotation, startRot: this.overlay.rotation * Math.PI / 180, viewScale: frozenScale, viewOffsetX: frozenOffsetX, viewOffsetY: frozenOffsetY }; return true; } return false; }; nodeType.prototype.onMouseMove = function(event, pos) { if (this.lineSpacingDragging && pos) { this.updateLineSpacingFromMouse(pos[0]); return; } const prevTextareaHover = this.textareaHover; if (this.textareaRect && !this.textareaState.isOpen) { const { x, y, w, h } = this.textareaRect; this.textareaHover = pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h; } else { this.textareaHover = false; } if (prevTextareaHover !== this.textareaHover) this.setDirtyCanvas(true); const prevLineSpacingHover = this.lineSpacingHover; if (this.lineSpacingRect) { const { x, y, w, h } = this.lineSpacingRect; this.lineSpacingHover = pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h; } else { this.lineSpacingHover = false; } if (prevLineSpacingHover !== this.lineSpacingHover) this.setDirtyCanvas(true); const prevLineSpacingValueHover = this.lineSpacingValueHover || false; if (this.lineSpacingValueRect) { const { x, y, w, h } = this.lineSpacingValueRect; this.lineSpacingValueHover = pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h; } else { this.lineSpacingValueHover = false; } if (prevLineSpacingValueHover !== this.lineSpacingValueHover) this.setDirtyCanvas(true); const prevAlignHover = [...this.alignButtonsHover]; if (this.alignButtonsRects && this.alignButtonsRects.length === 3) { for (let i = 0; i < 3; i++) { const { x, y, w, h, enabled } = this.alignButtonsRects[i]; this.alignButtonsHover[i] = enabled && pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h; } } else { this.alignButtonsHover = [false, false, false]; } if (prevAlignHover.some((v, i) => v !== this.alignButtonsHover[i])) this.setDirtyCanvas(true); const prevFontHover = this.fontSelectHover; if (this.fontSelectRect) { const { x, y, w, h } = this.fontSelectRect; this.fontSelectHover = pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h; } else { this.fontSelectHover = false; } if (prevFontHover !== this.fontSelectHover) this.setDirtyCanvas(true); const prevHexHover = this.hexHover; if (this.hexRect) { const { x, y, w, h } = this.hexRect; this.hexHover = pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h; } else { this.hexHover = false; } if (prevHexHover !== this.hexHover) this.setDirtyCanvas(true); const prevColorBoxHover = this.colorBoxHover; if (this.colorBoxRect) { const { x, y, w, h } = this.colorBoxRect; this.colorBoxHover = pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h; } else { this.colorBoxHover = false; } if (prevColorBoxHover !== this.colorBoxHover) this.setDirtyCanvas(true); const prevBtnApply = this.btnApplyHover; const prevBtnCancel = this.btnCancelHover; if (this.buttonsY !== undefined) { const btnH = 30; const btnGap = 10; const btnW = (this.size[0] - 35) / 2; const y1 = this.buttonsY; this.btnApplyHover = pos[0] >= 15 && pos[0] <= 15 + btnW && pos[1] >= y1 && pos[1] <= y1 + btnH; this.btnCancelHover = pos[0] >= 15 + btnW + btnGap && pos[0] <= 15 + btnW + btnGap + btnW && pos[1] >= y1 && pos[1] <= y1 + btnH; } if (prevBtnApply !== this.btnApplyHover || prevBtnCancel !== this.btnCancelHover) this.setDirtyCanvas(true); if (!this.advancedMode) return; if (!this.dragType || !this.isEditing || this.isLoading || !this.dragState) return; const { rectX, rectY } = this.getCanvasMetrics(); const mx = pos[0], my = pos[1]; const worldMx = (mx - rectX - this.dragState.viewOffsetX) / this.dragState.viewScale; const worldMy = (my - rectY - this.dragState.viewOffsetY) / this.dragState.viewScale; if (this.dragType === 'move') { this.overlay.x = this.dragState.startX + (worldMx - this.dragState.startMouseX); this.overlay.y = this.dragState.startY + (worldMy - this.dragState.startMouseY); } else if (this.dragType === 'rotate') { const cx = this.overlay.x, cy = this.overlay.y; const sA = Math.atan2(this.dragState.startMouseY - cy, this.dragState.startMouseX - cx); const cA = Math.atan2(worldMy - cy, worldMx - cx); this.overlay.rotation = this.dragState.startRotation + (cA - sA) * 180 / Math.PI; } else if (this.dragType.startsWith('scale')) { const { startCX, startCY, startW, startH, startRot, fixedLocal, activeLocal } = this.dragState; const { x: fx, y: fy } = fixedLocal; const { x: ax0, y: ay0 } = activeLocal; const cosR = Math.cos(startRot), sinR = Math.sin(startRot); const fixedWorldX = startCX + fx*cosR - fy*sinR; const fixedWorldY = startCY + fx*sinR + fy*cosR; const dxw = worldMx - fixedWorldX; const dyw = worldMy - fixedWorldY; const ldx = dxw*cosR + dyw*sinR; const ldy = -dxw*sinR + dyw*cosR; let newW = startW, newH = startH; const isCorner = (ax0 !== 0 && ay0 !== 0); if (ax0 !== 0) { const signX = Math.sign(ax0); newW = 2 * (ldx + fx) / signX; newW = Math.max(40, newW); } if (ay0 !== 0) { const signY = Math.sign(ay0); newH = 2 * (ldy + fy) / signY; newH = Math.max(40, newH); } if (isCorner) { const scaleX = newW / startW; const scaleY = newH / startH; const scale = Math.max(scaleX, scaleY); newW = startW * scale; newH = startH * scale; } const newCX = fixedWorldX - (fx*cosR - fy*sinR); const newCY = fixedWorldY - (fx*sinR + fy*cosR); this.overlay.x = newCX; this.overlay.y = newCY; this.overlay.width = newW; this.overlay.height = newH; this.textParams.text_scale_x = this.overlay.width / this.baseWidth; this.textParams.text_scale_y = this.overlay.height / this.baseHeight; } this.setDirtyCanvas(true); }; nodeType.prototype.onMouseUp = function() { if (this.lineSpacingDragging) { this.lineSpacingDragging = false; this.setDirtyCanvas(true); } if (!this.advancedMode) { this.dragType = null; this.dragState = null; this.setDirtyCanvas(true); return; } if (this.dragType) { const wasScale = this.dragType.startsWith('scale'); const wasRotate = this.dragType === 'rotate'; if (wasScale) { this.awaitingServerRender = true; } this.updateRelativeFromAbsolute(); if (!this.advancedMode) { this.computeAndApplyView(); } if (wasScale || wasRotate) { if (this.currentRenderAbortController) { this.currentRenderAbortController.abort(); this.currentRenderAbortController = null; } if (this.renderTimeout) clearTimeout(this.renderTimeout); this.currentRenderAbortController = new AbortController(); const signal = this.currentRenderAbortController.signal; this.renderMasks(signal); } } this.isDragging = false; this.dragType = null; this.dragState = null; }; } });