2560 lines
112 KiB
JavaScript
2560 lines
112 KiB
JavaScript
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 = [
|
|
'<svg width="20" height="20" viewBox="0 0 20 20"><line x1="2" y1="5" x2="18" y2="5" stroke="currentColor" stroke-width="2"/><line x1="2" y1="10" x2="12" y2="10" stroke="currentColor" stroke-width="2"/><line x1="2" y1="15" x2="15" y2="15" stroke="currentColor" stroke-width="2"/></svg>',
|
|
'<svg width="20" height="20" viewBox="0 0 20 20"><line x1="2" y1="5" x2="18" y2="5" stroke="currentColor" stroke-width="2"/><line x1="5" y1="10" x2="15" y2="10" stroke="currentColor" stroke-width="2"/><line x1="3" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="2"/></svg>',
|
|
'<svg width="20" height="20" viewBox="0 0 20 20"><line x1="2" y1="5" x2="18" y2="5" stroke="currentColor" stroke-width="2"/><line x1="8" y1="10" x2="18" y2="10" stroke="currentColor" stroke-width="2"/><line x1="5" y1="15" x2="18" y2="15" stroke="currentColor" stroke-width="2"/></svg>'
|
|
];
|
|
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;
|
|
};
|
|
}
|
|
}); |