import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; import { fabric } from "./lib/fabric-slim.min.js"; // 使用精简版fabric.js以提高性能 import { queueManager } from "./queue_shortcut.js"; // 定义常量 const CANVAS_SIZE = { WIDTH: 420, HEIGHT: 200, BOTTOM_MARGIN: 110, // 节点下沿扩展值 RIGHT_MARGIN: 20, // 节点右沿扩展值 CONTROL_PANEL_HEIGHT: 30 // 控制面板高度 }; // 模块级变量 const instances = []; let eventHandlersInitialized = false; let globalUpdateHandler, globalGetStateHandler; // 模块级函数 - 清理指定节点ID的实例 function cleanup(nodeId) { const index = instances.findIndex(inst => inst.node?.id === nodeId); if (index !== -1) { const instance = instances[index]; // 从数组中移除 instances.splice(index, 1); // 清理核心资源 if (instance.canvas) { instance.canvas.off(); instance.canvas.dispose(); } if (instance.canvasContainer?.parentNode) { instance.canvasContainer.parentNode.removeChild(instance.canvasContainer); } return true; } return false; } // 模块级函数 - 设置全局事件处理 function setupGlobalEventHandling() { // 如果已经设置了全局处理,则不重复设置 if (eventHandlersInitialized) return; // 设置全局事件处理函数 globalUpdateHandler = async (event) => { const data = event.detail; if (!data || !data.node_id) return; const nodeId = data.node_id; const instance = instances.find(inst => inst.node?.id.toString() === nodeId); if (!instance) return; if (data.canvas_data) { await instance.updateCanvas(data.canvas_data, data.transform_data); } else { await instance.sendCanvasState(); } }; globalGetStateHandler = async (event) => { const data = event.detail; if (!data || !data.node_id) return; const nodeId = data.node_id; const instance = instances.find(inst => inst.node?.id.toString() === nodeId); if (instance) { await instance.sendCanvasState(); } }; // 添加全局事件监听器 api.addEventListener("fast_canvas_update", globalUpdateHandler); api.addEventListener("fast_canvas_get_state", globalGetStateHandler); eventHandlersInitialized = true; } class FastCanvas { constructor(node, initialSize = null) { this.node = node; this.lastCanvasState = null; // 使用传入的初始尺寸或默认尺寸 this.originalSize = initialSize || { width: CANVAS_SIZE.WIDTH, height: CANVAS_SIZE.HEIGHT }; // 添加到实例数组 instances.push(this); // 确保全局事件处理已设置 setupGlobalEventHandling(); // 其他初始化代码... this.currentSize = { ...this.originalSize }; this.maxDisplaySize = 768; // 创建缩放值显示元素 this.scaleText = document.createElement('div'); this.scaleText.style.cssText = ` position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.5); color: white; padding: 2px 5px; border-radius: 3px; font-size: 12px; pointer-events: none; z-index: 1000; display: none; `; this.initCanvas(); this.setupDragAndDrop(); this.setupPaste(); } cleanup() { try { if (this.node?.id) { cleanup(this.node.id); } else { const index = instances.indexOf(this); if (index !== -1) { instances.splice(index, 1); } } } catch (e) { console.error('[清理错误]', e); } } initCanvas() { this.canvasContainer = document.createElement('div'); this.canvasContainer.className = 'fast-canvas-container'; // 创建 canvas 元素 const canvasElement = document.createElement('canvas'); // 使用原始尺寸创建画布 this.canvas = new fabric.Canvas(canvasElement, { width: this.originalSize.width, height: this.originalSize.height, preserveObjectStacking: true, selection: true }); // 设置选中框样式 this.canvas.selectionColor = 'rgba(0, 123, 255, 0.3)'; // 选中区域填充色 this.canvas.selectionBorderColor = '#007bff'; // 选中框边框颜色 this.canvas.selectionLineWidth = 2; // 选中框边框宽度 // 设置控制点样式 fabric.Object.prototype.set({ transparentCorners: false, // 控制点不透明 cornerColor: '#007bff', // 控制点颜色 cornerSize: 20, // 控制点大小 cornerStyle: 'circle', // 控制点形状为圆形 cornerStrokeColor: '#ffffff', // 控制点边框颜色 cornerStrokeWidth: 2, // 控制点边框宽度 padding: 5, // 选中框内边距 borderColor: '#007bff', // 边框颜色 borderScaleFactor: 2, // 边框宽度 hasRotatingPoint: true, // 显示旋转控制点 rotatingPointOffset: 30 // 旋转控制点偏移距离 }); // 创建默认背景图像 const defaultBackground = new fabric.Rect({ width: this.originalSize.width, height: this.originalSize.height, left: 0, top: 0, fill: '#000000', selectable: false, evented: false, excludeFromExport: false }); // 设置为背景图像 this.canvas.setBackgroundImage(defaultBackground, () => { this.canvas.renderAll(); }); this.canvasContainer.style.cssText = ` position: relative; width: ${this.originalSize.width}px; height: ${this.originalSize.height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px; background: transparent; // 改为透明背景 `; // 创建画布包装容器 const canvasWrapper = document.createElement('div'); canvasWrapper.style.cssText = ` width: 100%; height: ${this.originalSize.height}px; `; canvasWrapper.appendChild(this.canvas.wrapperEl); this.canvasContainer.appendChild(canvasWrapper); this.canvasContainer.appendChild(this.scaleText); // 创建控制面板 this.controlPanel = new ControlPanel(this.canvas, this.node); this.canvasContainer.appendChild(this.controlPanel.getContainer()); // 初始化状态 this.layers = new Map(); this.background = defaultBackground; // 保存背景引用 this.isDragging = false; this.isLocked = false; this.initContextMenu(); // 设置事件监听 this.setupEventListeners(); // 设置节点的初始尺寸 this.node.size = [ this.originalSize.width + CANVAS_SIZE.RIGHT_MARGIN, this.originalSize.height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT + CANVAS_SIZE.BOTTOM_MARGIN ]; } initContextMenu() { // 创建菜单元素 const menu = document.createElement('div'); menu.className = 'fast-canvas-context-menu'; menu.style.cssText = ` position: fixed; display: none; background: #2a2a2a; border: 1px solid #3f3f3f; border-radius: 6px; padding: 3px; z-index: 1000; color: white; box-shadow: 0 4px 12px rgba(0,0,0,0.3); transition: background-color 0.2s ease; min-width: 120px; `; const menuItems = [ { text: '设为背景', action: () => this.setLayerAsBackground() }, { text: '变换', submenu: [ { text: '水平翻转', action: () => this.flipObject('horizontal') }, { text: '垂直翻转', action: () => this.flipObject('vertical') }, { text: '居中', action: () => this.centerObject('both') }, { text: '水平居中', action: () => this.centerObject('horizontal') }, { text: '垂直居中', action: () => this.centerObject('vertical') } ] }, { text: '调整图层', submenu: [ { text: '上移一层', action: () => { const obj = this.canvas.getActiveObject(); if (obj) { obj.bringForward(); this.canvas.renderAll(); } }}, { text: '下移一层', action: () => { const obj = this.canvas.getActiveObject(); if (obj) { obj.sendBackwards(); this.canvas.renderAll(); } }}, { text: '置于顶层', action: () => { const obj = this.canvas.getActiveObject(); if (obj) { obj.bringToFront(); this.canvas.renderAll(); } }}, { text: '置于底层', action: () => { const obj = this.canvas.getActiveObject(); if (obj) { obj.sendToBack(); this.canvas.renderAll(); } }} ] }, { text: '混合模式', submenu: [ { text: '正常 (source-over)', action: () => this.setObjectCompositeMode('source-over') }, { text: '正片叠底 (multiply)', action: () => this.setObjectCompositeMode('multiply') }, { text: '滤色 (screen)', action: () => this.setObjectCompositeMode('screen') }, { text: '叠加 (overlay)', action: () => this.setObjectCompositeMode('overlay') }, { text: '变亮 (lighten)', action: () => this.setObjectCompositeMode('lighten') }, { text: '变暗 (darken)', action: () => this.setObjectCompositeMode('darken') }, { text: '强光 (hard-light)', action: () => this.setObjectCompositeMode('hard-light') }, { text: '柔光 (soft-light)', action: () => this.setObjectCompositeMode('soft-light') }, { text: '差值 (difference)', action: () => this.setObjectCompositeMode('difference') }, { text: '排除 (exclusion)', action: () => this.setObjectCompositeMode('exclusion') }, ] }, { text: '调整透明度', action: () => this.showOpacityDialog() }, { text: '恢复原尺寸', action: () => this.resetObjectSize() } ]; // 创建菜单项 menuItems.forEach(item => { const menuItem = document.createElement('div'); menuItem.textContent = item.text; menuItem.style.cssText = ` padding: 6px 15px; margin: 1px 3px; cursor: pointer; white-space: nowrap; transition: background-color 0.15s ease; font-size: 13px; border-radius: 4px; position: relative; `; if (item.submenu) { // 添加子菜单指示器 menuItem.style.paddingRight = '25px'; const arrow = document.createElement('span'); arrow.textContent = '▶'; arrow.style.cssText = ` position: absolute; right: 8px; top: 50%; transform: translateY(-50%); font-size: 10px; `; menuItem.appendChild(arrow); // 创建子菜单 const submenu = document.createElement('div'); submenu.style.cssText = ` position: absolute; left: 100%; top: 0; display: none; background: #2a2a2a; border: 1px solid #3f3f3f; border-radius: 6px; padding: 3px; z-index: 1001; box-shadow: 0 4px 12px rgba(0,0,0,0.3); `; // 创建子菜单项 item.submenu.forEach(subItem => { const subMenuItem = document.createElement('div'); subMenuItem.textContent = subItem.text; subMenuItem.style.cssText = ` padding: 6px 15px; margin: 1px 3px; cursor: pointer; white-space: nowrap; transition: background-color 0.15s ease; font-size: 13px; border-radius: 4px; `; subMenuItem.onmouseover = () => subMenuItem.style.backgroundColor = '#3f3f3f'; subMenuItem.onmouseout = () => subMenuItem.style.backgroundColor = ''; subMenuItem.onclick = (e) => { e.stopPropagation(); subItem.action(); menu.style.display = 'none'; }; submenu.appendChild(subMenuItem); }); // 显示/隐藏子菜单 menuItem.onmouseover = () => { submenu.style.display = 'block'; menuItem.style.backgroundColor = '#3f3f3f'; }; menuItem.onmouseout = (e) => { if (!e.relatedTarget || !submenu.contains(e.relatedTarget)) { submenu.style.display = 'none'; menuItem.style.backgroundColor = ''; } }; submenu.onmouseover = () => { submenu.style.display = 'block'; menuItem.style.backgroundColor = '#3f3f3f'; }; submenu.onmouseout = (e) => { if (!menuItem.contains(e.relatedTarget)) { submenu.style.display = 'none'; menuItem.style.backgroundColor = ''; } }; menuItem.appendChild(submenu); } else { // 普通菜单项的鼠标悬停效果 menuItem.onmouseover = () => menuItem.style.backgroundColor = '#3f3f3f'; menuItem.onmouseout = () => menuItem.style.backgroundColor = ''; menuItem.onclick = (e) => { e.stopPropagation(); item.action(); menu.style.display = 'none'; }; } menu.appendChild(menuItem); }); // 在画布容器上监听右键事件 this.canvasContainer.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); const activeObject = this.canvas.getActiveObject(); if (activeObject) { menu.style.left = `${e.pageX}px`; menu.style.top = `${e.pageY}px`; menu.style.display = 'block'; // 添加一次性点击事件监听器 const closeMenu = (e) => { if (!menu.contains(e.target)) { menu.style.display = 'none'; document.removeEventListener('click', closeMenu); document.removeEventListener('contextmenu', closeMenu); } }; // 延迟添加事件监听器,避免立即触发 setTimeout(() => { document.addEventListener('click', closeMenu); document.addEventListener('contextmenu', closeMenu); }, 0); } return false; }, true); // 阻止菜单内右键点击 menu.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); return false; }); document.body.appendChild(menu); } // 菜单操作方法 flipObject(direction) { const activeObject = this.canvas.getActiveObject(); if (activeObject) { if (direction === 'horizontal') { activeObject.flipX = !activeObject.flipX; } else { activeObject.flipY = !activeObject.flipY; } this.canvas.renderAll(); } } centerObject(direction) { const activeObject = this.canvas.getActiveObject(); if (activeObject) { if (direction === 'both') { activeObject.center(); } else if (direction === 'horizontal') { activeObject.centerH(); } else { activeObject.centerV(); } activeObject.setCoords(); this.canvas.renderAll(); } } resetObjectSize() { const activeObject = this.canvas.getActiveObject(); if (activeObject && activeObject.originalWidth && activeObject.originalHeight) { // 重置对象到原始尺寸 const originalWidth = activeObject.originalWidth; const originalHeight = activeObject.originalHeight; // 计算比例 const scaleX = originalWidth / activeObject.width; const scaleY = originalHeight / activeObject.height; // 应用变换 activeObject.set({ scaleX: 1, scaleY: 1, width: originalWidth, height: originalHeight }); this.canvas.renderAll(); } } // 设置图层混合模式 setObjectCompositeMode(mode) { const activeObject = this.canvas.getActiveObject(); if (activeObject) { activeObject.set({ globalCompositeOperation: mode }); this.canvas.renderAll(); } } // 显示透明度调整对话框 showOpacityDialog() { const activeObject = this.canvas.getActiveObject(); if (!activeObject) return; // 创建对话框元素 const dialog = document.createElement('div'); dialog.className = 'opacity-dialog'; dialog.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #2a2a2a; border: 1px solid #3f3f3f; border-radius: 6px; padding: 15px; z-index: 1000; color: white; box-shadow: 0 4px 12px rgba(0,0,0,0.3); min-width: 250px; `; // 创建滑动条 const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0'; slider.max = '100'; slider.value = Math.round(activeObject.opacity * 100); slider.style.cssText = ` width: 100%; margin: 5px 0; `; // 当前透明度值显示 const valueDisplay = document.createElement('div'); valueDisplay.style.cssText = ` margin: 5px 0; text-align: center; font-size: 14px; `; valueDisplay.textContent = `${Math.round(activeObject.opacity * 100)}%`; // 滑动时实时更新透明度 slider.addEventListener('input', (e) => { const opacity = parseInt(e.target.value) / 100; activeObject.set({ opacity }); valueDisplay.textContent = `${Math.round(opacity * 100)}%`; this.canvas.renderAll(); }); // 按钮容器 const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; justify-content: space-between; margin-top: 10px; `; // 关闭对话框函数 const closeDialog = () => { activeObject.set({ opacity: activeObject._originalOpacity }); try { document.body.removeChild(dialog); // 移除ESC键监听器 document.removeEventListener('keydown', handleKeyDown); } catch (error) { console.log("对话框已被移除"); } this.canvas.renderAll(); }; // 应用更改并关闭对话框 const applyChanges = () => { try { document.body.removeChild(dialog); // 移除所有事件监听器 document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('mousedown', handleClickOutside); } catch (error) { console.log("对话框已被移除"); } }; // ESC键监听器 const handleKeyDown = (e) => { if (e.key === 'Escape') { closeDialog(); } }; // 添加ESC键监听 document.addEventListener('keydown', handleKeyDown); // 取消按钮 const cancelButton = document.createElement('button'); cancelButton.textContent = '取消'; cancelButton.style.cssText = ` background: #444; border: none; color: white; padding: 5px 10px; border-radius: 4px; cursor: pointer; `; cancelButton.onclick = closeDialog; // 确认按钮 const confirmButton = document.createElement('button'); confirmButton.textContent = '确认'; confirmButton.style.cssText = ` background: #007bff; border: none; color: white; padding: 5px 10px; border-radius: 4px; cursor: pointer; `; confirmButton.onclick = applyChanges; // 添加所有元素 dialog.appendChild(valueDisplay); dialog.appendChild(slider); buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(confirmButton); dialog.appendChild(buttonContainer); // 保存原始透明度以便取消时恢复 activeObject._originalOpacity = activeObject.opacity; // 添加到文档 document.body.appendChild(dialog); // 在函数内部创建并存储mousedown监听器的引用 let handleClickOutside; // 定义并存储监听器引用 handleClickOutside = function(e) { if (!dialog.contains(e.target)) { closeDialog(); document.removeEventListener('mousedown', handleClickOutside); } }; // 添加监听器 document.addEventListener('mousedown', handleClickOutside); } updateContainerSize(width, height) { // 更新画布容器尺寸为缩放后的显示尺寸 this.canvasContainer.style.width = `${width}px`; this.canvasContainer.style.height = `${height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px`; // 确保控制面板始终在最上层 this.canvasContainer.style.position = 'relative'; // 添加这行 this.canvasContainer.style.zIndex = '1'; // 添加这行 // 更新控制面板尺寸 this.controlPanel.updateSize(width); // 更新节点的 DOM 元素尺寸 if (this.node.canvasElement) { this.node.canvasElement.style.width = `${width}px`; this.node.canvasElement.style.height = `${height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px`; } // 更新节点尺寸 this.node.size = [ width + CANVAS_SIZE.RIGHT_MARGIN, height + CANVAS_SIZE.BOTTOM_MARGIN + CANVAS_SIZE.CONTROL_PANEL_HEIGHT ]; // 强制更新节点 this.node.setDirtyCanvas(true, true); } updateCanvasSize(displayWidth, displayHeight) { // 1. 计算缩放比例 const scaleX = displayWidth / this.originalSize.width; const scaleY = displayHeight / this.originalSize.height; const scale = Math.min(scaleX, scaleY); // 使用统一的缩放比例 // 2. 设置画布为原始尺寸 this.canvas.setDimensions({ width: this.originalSize.width, height: this.originalSize.height }); // 3. 更新画布包装器的CSS transform this.canvas.wrapperEl.style.transform = `scale(${scale})`; this.canvas.wrapperEl.style.transformOrigin = 'top left'; this.canvas.wrapperEl.style.width = `${displayWidth}px`; this.canvas.wrapperEl.style.height = `${displayHeight}px`; // 4. 同步Fabric.js的缩放 this.canvas.setZoom(1); // 重置zoom,因为我们使用CSS缩放 // 5. 如果有背景图片,确保它使用原始尺寸 if (this.canvas.backgroundImage) { this.canvas.backgroundImage.set({ width: this.originalSize.width, height: this.originalSize.height, scaleX: 1, scaleY: 1, left: 0, top: 0, originX: 'left', originY: 'top' }); // 强制更新背景坐标 this.canvas.backgroundImage.setCoords(); } // 6. 更新容器尺寸 this.updateContainerSize(displayWidth, displayHeight); // 7. 重新渲染 this.canvas.renderAll(); // 8. 更新事件监听区域 this.canvas.calcOffset(); } // 修改 updateDisplayScale 方法 updateDisplayScale(maxSize) { this.maxDisplaySize = maxSize; if (this.originalSize?.width && this.originalSize?.height) { const scaledSize = this.calculateScaledSize( this.originalSize.width, this.originalSize.height, maxSize ); // 更新显示尺寸,但保持画布原始尺寸 this.updateCanvasSize(scaledSize.width, scaledSize.height); // 确保画布完全重置和更新 this.canvas.requestRenderAll(); } } // 计算缩放后的尺寸 calculateScaledSize(width, height, maxSize) { if (width <= maxSize && height <= maxSize) { return { width, height }; } const ratio = width / height; if (width > height) { return { width: maxSize, height: Math.round(maxSize / ratio) }; } else { return { width: Math.round(maxSize * ratio), height: maxSize }; } } setupPaste() { // 移除可能存在的旧事件监听器 if (this._pasteHandler) { document.removeEventListener('paste', this._pasteHandler); } let canvasActive = false; // 创建新的粘贴处理函数 this._pasteHandler = async (e) => { if (!canvasActive) return; // 如果画布不是活动状态,不处理粘贴 const items = (e.clipboardData || e.originalEvent.clipboardData).items; for (const item of items) { if (item.type.indexOf('image') === 0) { e.preventDefault(); e.stopPropagation(); const blob = item.getAsFile(); await this.handleImageUpload(blob, { center: true, autoScale: true }); break; } } }; // 添加内部复制功能 document.addEventListener('keydown', (e) => { // 检查是否在画布区域内且画布处于活动状态 const activeElement = document.activeElement; const isInputActive = activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA'; if (isInputActive || !canvasActive) return; // 复制 (Ctrl+C) if (e.ctrlKey && e.key === 'c') { const activeObjects = this.canvas.getActiveObjects(); if (activeObjects.length > 0) { activeObjects.forEach(obj => { // 创建图层的副本 obj.clone((cloned) => { // 生成新的图层ID const newId = this.controlPanel.maxLayerId + 1; this.controlPanel.maxLayerId = newId; cloned.set({ left: obj.left + 20, top: obj.top + 20, id: newId }); // 添加到画布和图层管理 this.canvas.add(cloned); this.layers.set(newId, cloned); }); }); this.canvas.renderAll(); this.controlPanel.updateLayerSelector(); } } }); // 添加新的事件监听器到 document document.addEventListener('paste', this._pasteHandler, true); // 点击画布时设置活动状态 this.canvasContainer.addEventListener('mousedown', () => { canvasActive = true; }); // 点击其他地方时取消活动状态 document.addEventListener('mousedown', (e) => { if (!this.canvasContainer.contains(e.target)) { canvasActive = false; } }); // 清理函数 this.cleanup = () => { document.removeEventListener('paste', this._pasteHandler, true); }; } setupDragAndDrop() { // 防止浏览器默认的拖放行为 this.canvasContainer.addEventListener('dragenter', (e) => { e.preventDefault(); e.stopPropagation(); this.canvasContainer.classList.add('drag-over'); }); this.canvasContainer.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); }); this.canvasContainer.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); if (e.target === this.canvasContainer) { this.canvasContainer.classList.remove('drag-over'); } }); this.canvasContainer.addEventListener('drop', async (e) => { e.preventDefault(); e.stopPropagation(); this.canvasContainer.classList.remove('drag-over'); const files = e.dataTransfer.files; if (files.length > 0) { const file = files[0]; if (file.type.startsWith('image/')) { await this.handleImageUpload(file, { center: true, autoScale: true }); } } }); // 添加样式 const style = document.createElement('style'); style.textContent = ` .fast-canvas-container.drag-over::after { content: '拖放图片到这里'; position: absolute; top: 0; left: 0; right: 0; bottom: ${CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px; background: rgba(0, 0, 0, 0.7); color: white; display: flex; align-items: center; justify-content: center; font-size: 18px; pointer-events: none; z-index: 1000; } `; document.head.appendChild(style); } async handleImageUpload(file, options = { center: true, autoScale: true }) { try { const reader = new FileReader(); const imageLoadPromise = new Promise((resolve, reject) => { reader.onload = (e) => { const dataUrl = e.target.result; resolve(dataUrl); }; reader.onerror = reject; }); reader.readAsDataURL(file); const imageData = await imageLoadPromise; // 统一按图层方式加载图片 fabric.Image.fromURL(imageData, (img) => { if (options.center && options.autoScale) { // 计算适合画布的缩放比例 const scale = Math.min( this.canvas.width / img.width, this.canvas.height / img.height ); // 计算居中位置 const canvasCenter = this.canvas.getCenter(); img.set({ scaleX: scale, scaleY: scale, left: canvasCenter.left, top: canvasCenter.top, originX: 'center', originY: 'center' }); } else { // 不进行居中和缩放,使用原始尺寸和位置 img.set({ left: 0, top: 0, originX: 'left', originY: 'top' }); } // 添加到画布并更新图层ID this.canvas.add(img); this.controlPanel.updateLayerIds(); this.canvas.setActiveObject(img); this.canvas.renderAll(); }); } catch (error) { console.error('图片上传失败:', error); alert('图片上传失败,请重试'); } } setupEventListeners() { // 添加一个计算相对缩放值的辅助方法 const getRelativeScale = (obj) => { const backgroundImage = this.canvas.backgroundImage; if (!backgroundImage) return obj.scaleX; // 计算背景图片的缩放比例 const bgScaleRatio = backgroundImage.width / backgroundImage.getScaledWidth(); // 计算相对于背景图片的实际缩放比例 return Number((obj.scaleX * bgScaleRatio).toFixed(2)); }; // 创建一个绑定到当前实例的事件处理函数 this._handleKeyDown = (e) => { // 首先检查画布和节点是否还存在 if (!this.canvas || !this.node) { // 如果画布或节点不存在,移除事件监听器 document.removeEventListener('keydown', this._handleKeyDown); return; } // 检查是否有选中的对象且当前焦点不在输入框上 if (this.canvas.getActiveObject() && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') { // Delete 键或 Backspace 键 if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); const activeObject = this.canvas.getActiveObject(); // 确保不是背景图片 if (activeObject !== this.canvas.backgroundImage) { // 如果是图层,从图层映射中移除 if (activeObject.id) { this.layers.delete(activeObject.id); } // 从画布中移除 this.canvas.remove(activeObject); this.canvas.renderAll(); } } } }; // 添加滚轮缩放 this.canvas.on('mouse:wheel', (opt) => { const delta = opt.e.deltaY; const activeObject = this.canvas.getActiveObject(); if (activeObject) { let scale = activeObject.scaleX; if (delta < 0) { scale *= 1.1; // 放大 } else { scale *= 0.9; // 缩小 } // 限制缩放范围 scale = Math.min(Math.max(scale, 0.01), 10); activeObject.scale(scale); this.canvas.renderAll(); // 更新缩放值显示 const relativeScale = getRelativeScale(activeObject); this.scaleText.innerHTML = `scale: ${relativeScale}×`; this.scaleText.style.display = 'block'; opt.e.preventDefault(); opt.e.stopPropagation(); } }); } async updateCanvas(canvasData, transformData = null) { if (!canvasData) return; try { // 先获取当前所有图层的状态(只在没有传入transformData时保留状态) const currentLayers = !transformData ? this.canvas.getObjects().map(obj => ({ object: obj, state: { left: obj.left, top: obj.top, scaleX: obj.scaleX, scaleY: obj.scaleY, angle: obj.angle, flipX: obj.flipX, flipY: obj.flipY } })) : []; // 先设置背景 if (canvasData.background) { await this.setBackground(canvasData.background); } // 再更新图层 if (canvasData.layers) { await this.updateLayers(canvasData.layers); // 如果有transformData,应用变换数据 if (transformData) { this.applyTransformData(transformData); } else { // 否则恢复图层状态 currentLayers.forEach(({object, state}) => { const existingObj = this.canvas.getObjects().find(obj => obj.id === object.id); if (existingObj) { existingObj.set(state); } }); } } // 确保画布完全渲染 await new Promise(resolve => { requestAnimationFrame(() => { this.canvas.renderAll(); resolve(); }); }); await this.sendCanvasState(); } catch (error) { console.error('[FastCanvas] updateCanvas error:', error); } } // 应用变换数据到图层 applyTransformData(transformData) { if (!transformData) return; try { // 遍历transform_data中的每个图层 for (const [layerId, trans] of Object.entries(transformData)) { // 跳过background if (layerId === 'background') continue; // 查找对应的图层对象 const layerObj = this.layers.get(parseInt(layerId)); if (!layerObj) { console.warn(`[FastCanvas] Layer ${layerId} not found`); continue; } // 应用变换参数 const updates = { originX: 'center', originY: 'center' }; // 设置中心点位置 if (trans.centerX !== undefined) updates.left = trans.centerX; if (trans.centerY !== undefined) updates.top = trans.centerY; // 缩放 if (trans.scaleX !== undefined) updates.scaleX = trans.scaleX; if (trans.scaleY !== undefined) updates.scaleY = trans.scaleY; // 旋转(注意:fabric.js使用正角度,transform_data可能需要调整) if (trans.angle !== undefined) updates.angle = trans.angle; // 翻转 if (trans.flipX !== undefined) updates.flipX = trans.flipX; if (trans.flipY !== undefined) updates.flipY = trans.flipY; // 应用所有变换 layerObj.set(updates); layerObj.setCoords(); // 更新对象坐标 } // 渲染画布 this.canvas.renderAll(); console.log('[FastCanvas] Applied transform data to layers'); } catch (error) { console.error('[FastCanvas] Error applying transform data:', error); } } // 添加一个通用的图像加载函数 loadImage(imageUrl, timeout = 5000) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error('Image loading timeout')); }, timeout); fabric.Image.fromURL(imageUrl, (img) => { clearTimeout(timeoutId); if (!img) { reject(new Error('Failed to create image')); return; } resolve(img); }, (error) => { clearTimeout(timeoutId); reject(error); }, { crossOrigin: 'anonymous' } // 添加跨域支持 ); }); } setLayerAsBackground() { const activeObject = this.canvas.getActiveObject(); if (!activeObject) return; // 移除当前图层 this.canvas.remove(activeObject); // 移除图层ID this.controlPanel.removeLayerId(activeObject); // 准备背景数据 const backgroundData = { image: activeObject.getSrc() }; // 使用现有的 setBackground 方法设置背景 this.setBackground(backgroundData); } async setBackground(backgroundData) { if (!backgroundData?.image) { return; } try { const img = await this.loadImage(backgroundData.image); // 保存当前所有图层的状态 const layerStates = this.canvas.getObjects().map(obj => ({ object: obj, state: { left: obj.left, top: obj.top, scaleX: obj.scaleX, scaleY: obj.scaleY, angle: obj.angle } })); // 保存原始尺寸 this.originalSize = { width: img.width, height: img.height }; // 计算显示尺寸 const scaledSize = this.calculateScaledSize( img.width, img.height, this.maxDisplaySize ); // 设置画布尺寸为原始尺寸 this.canvas.setDimensions({ width: this.originalSize.width, height: this.originalSize.height }); // 设置背景图片属性 img.set({ scaleX: 1, scaleY: 1, left: 0, top: 0, selectable: false, evented: false, originX: 'left', originY: 'top' }); // 设置CSS缩放 const scale = scaledSize.width / this.originalSize.width; this.canvas.wrapperEl.style.transform = `scale(${scale})`; this.canvas.wrapperEl.style.transformOrigin = 'top left'; // 更新容器尺寸 this.updateContainerSize(scaledSize.width, scaledSize.height); // 更新尺寸输入框 if (this.node?.sizeOverlay?.updateSizeInputs) { this.node.sizeOverlay.updateSizeInputs( this.originalSize.width, this.originalSize.height ); } // 直接设置背景图片 return new Promise((resolve) => { this.canvas.setBackgroundImage(img, () => { // 恢复所有图层的状态 layerStates.forEach(({object, state}) => { object.set(state); }); this.canvas.renderAll(); resolve(); }); }); } catch (error) { this.sendErrorState("Background loading failed"); } } addLayer(layerData) { return new Promise((resolve) => { fabric.Image.fromURL(layerData.image, (img) => { if (!img) { resolve(); return; } // 计算适合画布的缩放比例 const scale = Math.min( this.canvas.width / img.width, this.canvas.height / img.height ); // 计算居中位置 const centerX = (this.canvas.width - img.width * scale) / 2; const centerY = (this.canvas.height - img.height * scale) / 2; img.set({ id: layerData.id, scaleX: scale, scaleY: scale, left: centerX, top: centerY, selectable: true, hasControls: true, hasBorders: true, originalWidth: img.width, originalHeight: img.height }); this.canvas.add(img); this.layers.set(layerData.id, img); // 添加以下代码来保持选中状态 this.canvas.setActiveObject(img); this.canvas.renderAll(); resolve(); }); }); } updateLayer(layerData) { return new Promise((resolve) => { const existingObj = this.layers.get(layerData.id); if (existingObj) { // 保存当前的变换状态(补充透明度与混合模式) const currentState = { scaleX: existingObj.scaleX, scaleY: existingObj.scaleY, left: existingObj.left, top: existingObj.top, angle: existingObj.angle, flipX: existingObj.flipX, flipY: existingObj.flipY, opacity: existingObj.opacity, globalCompositeOperation: existingObj.globalCompositeOperation }; // 检查当前对象是否被选中 const wasActive = this.canvas.getActiveObject() === existingObj; fabric.Image.fromURL(layerData.image, (newImg) => { if (!newImg) { resolve(); return; } // 直接应用所有保存的变换状态 newImg.set({ ...currentState, id: layerData.id, selectable: true, hasControls: true, hasBorders: true, originalWidth: newImg.width, originalHeight: newImg.height }); // 替换旧图层 const index = this.canvas.getObjects().indexOf(existingObj); this.canvas.remove(existingObj); this.canvas.insertAt(newImg, index); this.layers.set(layerData.id, newImg); // 如果原对象是选中状态,则保持新对象的选中状态 if (wasActive) { this.canvas.setActiveObject(newImg); } this.canvas.renderAll(); resolve(); }); } else { // 如果图层不存在,作为新图层添加 this.addLayer(layerData).then(resolve); } }); } async updateLayers(layersData) { try { // 异步移除不再需要的图层 const currentIds = new Set(layersData.map(layer => layer.id)); const removePromises = []; for (const [id, fabricObj] of this.layers.entries()) { if (!currentIds.has(id)) { removePromises.push( new Promise(resolve => { this.canvas.remove(fabricObj); this.layers.delete(id); requestAnimationFrame(resolve); }) ); } } if (removePromises.length > 0) { await Promise.all(removePromises); this.canvas.renderAll(); } for (const layerData of layersData) { if (this.layers.has(layerData.id)) { await this.updateLayer(layerData); } else { await this.addLayer(layerData); } // 更新最大图层ID this.controlPanel.maxLayerId = Math.max( this.controlPanel.maxLayerId, layerData.id ); } this.controlPanel.updateLayerSelector(); // 确保最终渲染 return new Promise(resolve => { requestAnimationFrame(() => { this.canvas.renderAll(); resolve(); }); }); } catch (error) { throw error; } } async getLayerMask(selectedLayer, ignoreOcclusion = false) { if (!selectedLayer || !this.canvas.backgroundImage) { return null; } // 创建与原始画布相同尺寸的遮罩画布 const maskCanvas = document.createElement('canvas'); maskCanvas.width = this.originalSize.width; maskCanvas.height = this.originalSize.height; const ctx = maskCanvas.getContext('2d'); // 清除画布,保持透明背景 ctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); // 渲染选中图层 if (selectedLayer.visible !== false) { ctx.save(); // 直接使用图层的变换矩阵 const matrix = selectedLayer.calcTransformMatrix(); ctx.transform( matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5] ); // 绘制图层 - 图像对象 if (selectedLayer._element) { ctx.drawImage( selectedLayer._element, -selectedLayer.width / 2, -selectedLayer.height / 2, selectedLayer.width, selectedLayer.height ); } else if (selectedLayer.type === 'text' || selectedLayer.type === 'i-text') { // 设置文本样式 ctx.fillStyle = 'white'; ctx.font = `${selectedLayer.fontSize}px ${selectedLayer.fontFamily}`; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; // 处理行间距和多行文本 const lines = selectedLayer.text.split('\n'); const lineHeight = selectedLayer.lineHeight || 1.16; // Fabric默认值 for (let i = 0; i < lines.length; i++) { // 计算每行Y位置,考虑行高 const y = -selectedLayer.height / 2 + i * lineHeight * selectedLayer.fontSize + lineHeight*1.8; // 绘制每行文本,保留字间距处理 ctx.fillText(lines[i], -selectedLayer.width / 2, y); } } ctx.restore(); } // 上层遮挡处理部分也需要修改 if (!ignoreOcclusion) { const allLayers = this.canvas.getObjects() .filter(obj => obj !== this.canvas.backgroundImage); const selectedLayerIndex = allLayers.indexOf(selectedLayer); const upperLayers = allLayers.slice(selectedLayerIndex + 1); if (upperLayers.length > 0) { ctx.globalCompositeOperation = 'destination-out'; upperLayers.forEach(layer => { if (layer.visible !== false) { ctx.save(); const matrix = layer.calcTransformMatrix(); ctx.transform( matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5] ); if (layer._element) { ctx.drawImage( layer._element, -layer.width / 2, -layer.height / 2, layer.width, layer.height ); } else if (layer.type === 'text' || layer.type === 'i-text') { // 设置文本样式 ctx.fillStyle = 'white'; ctx.font = `${layer.fontSize}px ${layer.fontFamily}`; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; // 处理行间距和多行文本 const lines = layer.text.split('\n'); const lineHeight = layer.lineHeight || 1.16; // Fabric默认值 for (let i = 0; i < lines.length; i++) { // 计算每行Y位置,考虑行高 const y = -selectedLayer.height / 2 + i * lineHeight * selectedLayer.fontSize + lineHeight*1.8; // 绘制每行文本,保留字间距处理 ctx.fillText(lines[i], -layer.width / 2, y); } } ctx.restore(); } }); } } return maskCanvas; } async sendCanvasState() { if (!this.canvas || !this.canvas.contextContainer) { console.log('[FastCanvas] 画布或上下文不存在,跳过状态发送'); return; } try { const timestamp = Date.now(); // 创建临时画布来处理尺寸 const tempCanvas = document.createElement('canvas'); tempCanvas.width = this.originalSize.width; tempCanvas.height = this.originalSize.height; const tempCtx = tempCanvas.getContext('2d'); const activeObjects = this.canvas.getActiveObjects(); const activeObject = this.canvas.getActiveObject(); // 主图像处理 this.canvas.discardActiveObject(); this.canvas.renderAll(); // 将高分辨率画布内容绘制到正确尺寸的临时画布 tempCtx.drawImage( this.canvas.lowerCanvasEl, 0, 0, this.canvas.lowerCanvasEl.width, this.canvas.lowerCanvasEl.height, // 源尺寸(高分辨率) 0, 0, this.originalSize.width, this.originalSize.height // 目标尺寸 ); if (activeObject) { this.canvas.setActiveObject(activeObject); this.canvas.renderAll(); } // 从临时画布获取正确尺寸的Blob const imageBlob = await new Promise(resolve => { tempCanvas.toBlob(async (blob) => { const tempImg = await createImageBitmap(blob); resolve(blob); }, 'image/png', 1.0); }); // 将Blob转换为ArrayBuffer const imageBuffer = await imageBlob.arrayBuffer(); // 遮罩处理 const maskCanvas = document.createElement('canvas'); maskCanvas.width = this.originalSize.width; maskCanvas.height = this.originalSize.height; const ctx = maskCanvas.getContext('2d'); // 处理选中对象的遮罩 if (activeObjects && activeObjects.length > 1) { // 设置混合模式为"lighter",确保重叠的白色区域保持白色 ctx.globalCompositeOperation = 'lighter'; // 按照画布中的顺序获取选中的图层 const allLayers = this.canvas.getObjects().filter(obj => obj !== this.canvas.backgroundImage); const orderedSelectedObjects = allLayers.filter(obj => activeObjects.includes(obj)); // 按顺序处理每个选中的对象 for (const obj of orderedSelectedObjects) { // 获取带遮挡关系的遮罩(注意这里传入 false 表示不忽略遮挡) const layerMask = await this.getLayerMask(obj, false); if (layerMask) { ctx.drawImage(layerMask, 0, 0); } } } else if (activeObject) { const layerMask = await this.getLayerMask(activeObject, false); if (layerMask) { ctx.drawImage(layerMask, 0, 0); } } else { ctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); } // 主遮罩转换为二进制 const maskBlob = await new Promise(resolve => { maskCanvas.toBlob(resolve, 'image/png', 1.0); }); const maskBuffer = await maskBlob.arrayBuffer(); // 收集变换信息(使用之前获取的activeObjects) const layerTransforms = {}; if (activeObjects && activeObjects.length > 0) { activeObjects.forEach(obj => { if (obj.id) { // 获取对象的中心点坐标 const centerPoint = obj.getCenterPoint(); layerTransforms[obj.id] = { centerX: centerPoint.x, // 使用中心点 X 坐标 centerY: centerPoint.y, // 使用中心点 Y 坐标 scaleX: obj.scaleX, scaleY: obj.scaleY, angle: obj.angle, width: obj.width, height: obj.height, flipX: obj.flipX, flipY: obj.flipY }; } }); } // 构建发送的数据对象 const data = { node_id: this.node.id.toString(), timestamp: timestamp.toString(), type: 'temp', subfolder: 'fast_canvas', overwrite: 'true', main_image: Array.from(new Uint8Array(imageBuffer)), main_mask: Array.from(new Uint8Array(maskBuffer)), layer_transforms: layerTransforms // 替换原来的 layer_masks }; // 发送数据 const response = await api.fetchApi('/fast_canvas_all', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }); console.log('[FastCanvas] 数据发送成功,服务器响应:', response); await new Promise(resolve => setTimeout(resolve, 200)); } catch (error) { console.error('[FastCanvas] 更新画布状态失败:', error); throw error; } } // 使用fabric.js内置方法序列化画布(不包含图像数据) serializeCanvasJSON() { if (!this.canvas) { return null; } try { // 保存当前选中状态 const activeObjectIds = []; const activeObjects = this.canvas.getActiveObjects(); if (activeObjects) { activeObjects.forEach(obj => { if (obj.id) activeObjectIds.push(obj.id); }); } // 序列化画布,包含自定义属性(混合模式/透明度/文字相关) const json = this.canvas.toJSON([ 'id', 'originalWidth', 'originalHeight', 'globalCompositeOperation', 'opacity', 'text', 'fontFamily', 'fontSize', 'fill', 'backgroundColor', 'fontWeight', 'fontStyle', 'underline', 'charSpacing', 'lineHeight', 'stroke', 'strokeWidth', 'textAlign' ]); // 添加元数据 json.metadata = { version: '1.0', timestamp: Date.now(), canvasSize: this.originalSize, activeObjectIds: activeObjectIds }; // 移除图像数据,仅保留引用 if (json.objects) { json.objects.forEach(obj => { if (obj.type === 'image') { obj.imageRef = `layer_${obj.id}`; delete obj.src; } }); } if (json.backgroundImage) { json.backgroundImage.imageRef = 'background'; delete json.backgroundImage.src; } return JSON.stringify(json); } catch (error) { return null; } } // 从JSON恢复画布状态(需要提供图像数据) async deserializeCanvasJSON(jsonString, imageDataProvider) { if (!this.canvas || !jsonString) { return false; } try { // 解析JSON const json = JSON.parse(jsonString); // 处理对象中的图像引用 if (json.objects) { for (let i = 0; i < json.objects.length; i++) { const obj = json.objects[i]; if (obj.type === 'image' && obj.imageRef && imageDataProvider) { // 获取图像数据 const imageData = await imageDataProvider(obj.imageRef); if (imageData) { obj.src = imageData; } } } } // 处理背景图像引用 if (json.backgroundImage && json.backgroundImage.imageRef && imageDataProvider) { const bgImageData = await imageDataProvider(json.backgroundImage.imageRef); if (bgImageData) { json.backgroundImage.src = bgImageData; } } // 使用fabric.js的loadFromJSON方法恢复画布 return new Promise(resolve => { this.canvas.loadFromJSON(json, () => { // 恢复选中状态 if (json.metadata && json.metadata.activeObjectIds && json.metadata.activeObjectIds.length > 0) { const objectsToSelect = []; json.metadata.activeObjectIds.forEach(id => { const obj = this.canvas.getObjects().find(o => o.id === id); if (obj) objectsToSelect.push(obj); }); if (objectsToSelect.length === 1) { this.canvas.setActiveObject(objectsToSelect[0]); } else if (objectsToSelect.length > 1) { const selection = new fabric.ActiveSelection(objectsToSelect, { canvas: this.canvas }); this.canvas.setActiveObject(selection); } } // 设置画布尺寸 if (json.metadata && json.metadata.canvasSize) { this.originalSize = json.metadata.canvasSize; } // 更新图层引用 this.layers = new Map(); this.canvas.getObjects().forEach(obj => { if (obj.id) { this.layers.set(obj.id, obj); } }); this.canvas.renderAll(); resolve(true); }); }); } catch (error) { return false; } } } // 控制面板类 class ControlPanel { constructor(canvas, node) { this.canvas = canvas; // fabric.js canvas 实例 this.node = node; // ComfyUI 节点实例 this.container = this.createContainer(); this.currentLayerId = null; // 当前选中的图层ID this.maxLayerId = 0; // 最大图层ID this.textPanelVisible = false; // 文字面板可见性状态 this.initializeControls(); } // 创建控制面板容器 createContainer() { const container = document.createElement('div'); container.className = 'fast-canvas-control-panel'; container.style.cssText = ` position: absolute; bottom: -5px; // 将bottom设为负值,使控制面板向下移动 left: 0; width: 100%; height: ${CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px; background-color: #353535; padding: 10px; display: flex; align-items: center; gap: 10px; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; box-sizing: border-box; `; return container; } // 创建按钮 createButton(text, onClick) { const button = document.createElement('button'); button.textContent = text; button.style.cssText = ` padding: 5px 10px; height: 28px; line-height: 18px; background: #444; border: 1px solid #666; color: #fff; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-sizing: border-box; font-size: 14px; `; button.onclick = onClick; return button; } // 创建数字输入框 createNumberInput(placeholder, defaultValue, onInput) { const input = document.createElement('input'); input.type = 'number'; input.placeholder = placeholder; input.style.cssText = ` padding: 5px 10px; height: 28px; line-height: 18px; background: #333; border: 1px solid #666; color: #fff; border-radius: 4px; width: 80px; box-sizing: border-box; display: flex; align-items: center; font-size: 14px; text-align: center; `; const style = document.createElement('style'); style.textContent = ` input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } input[type="number"] { -moz-appearance: textfield; } input[type="number"]::placeholder { color: #888; opacity: 1; text-align: center; } `; document.head.appendChild(style); input.dataset.defaultValue = defaultValue; input.oninput = (e) => { const value = parseInt(e.target.value); if (value > 0) { onInput(e); } else { e.target.value = input.dataset.defaultValue; onInput({ target: { value: input.dataset.defaultValue } }); } }; return input; } // 创建图层选择器 createLayerSelector() { const container = document.createElement('div'); container.className = 'layer-selector'; container.style.cssText = ` position: relative; display: inline-block; `; const button = document.createElement('button'); button.className = 'layer-selector-btn'; button.style.cssText = ` padding: 5px 8px; height: 28px; background: #444; border: 1px solid #666; color: #fff; border-radius: 4px; cursor: pointer; display: flex; align-items: center; min-width: 80px; justify-content: space-between; font-size: 14px; `; const buttonText = document.createElement('span'); buttonText.textContent = '图层'; button.appendChild(buttonText); const arrow = document.createElement('span'); arrow.textContent = '▼'; arrow.style.marginLeft = '5px'; button.appendChild(arrow); const dropdown = document.createElement('div'); dropdown.className = 'layer-dropdown'; dropdown.style.cssText = ` position: absolute; top: 100%; left: 0; background: #2a2a2a; border: 1px solid #666; border-radius: 4px; display: none; z-index: 1000; min-width: 100%; max-height: 200px; overflow-y: auto; `; // 点击按钮显示/隐藏下拉菜单 button.onclick = (e) => { e.stopPropagation(); dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; }; // 点击其他地方关闭下拉菜单 document.addEventListener('click', () => { dropdown.style.display = 'none'; }); container.appendChild(button); container.appendChild(dropdown); // 保存引用以便更新 this.layerButton = button; this.layerButtonText = buttonText; this.layerDropdown = dropdown; return container; } // 更新图层选择器 updateLayerSelector() { const dropdown = this.layerDropdown; dropdown.innerHTML = ''; // 获取所有图层,保持原有顺序 const layers = Array.from(this.canvas.getObjects()) .filter(obj => obj !== this.canvas.backgroundImage && obj.id); // 获取当前选中的图层 const activeObject = this.canvas.getActiveObject(); if (activeObject && activeObject !== this.canvas.backgroundImage) { this.currentLayerId = activeObject.id; } // 更新按钮文本 this.layerButtonText.textContent = this.currentLayerId ? `图层 ${this.currentLayerId}` : '图层'; // 创建下拉选项 layers.forEach(layer => { if (layer.id !== this.currentLayerId) { const option = document.createElement('div'); option.className = 'layer-option'; option.textContent = `图层 ${layer.id}`; option.style.cssText = ` padding: 5px 10px; cursor: pointer; white-space: nowrap; &:hover { background: #444; } `; option.onclick = (e) => { e.stopPropagation(); // 只选中图层,不改变叠加顺序 this.canvas.setActiveObject(layer); this.currentLayerId = layer.id; this.updateLayerSelector(); // 触发画布更新,但不改变图层顺序 this.canvas.requestRenderAll(); this.layerDropdown.style.display = 'none'; }; dropdown.appendChild(option); } }); } // 创建设置弹窗 createSettingsDialog() { const dialog = document.createElement('dialog'); dialog.style.cssText = ` background: #353535; border: 1px solid #666; border-radius: 4px; color: #fff; padding: 20px; min-width: 300px; `; const title = document.createElement('h3'); title.textContent = '设置'; title.style.cssText = ` margin: 0 0 20px 0; font-size: 16px; `; // 创建设置项容器 const settingsContainer = document.createElement('div'); settingsContainer.style.cssText = ` display: flex; flex-direction: column; gap: 15px; `; // 窗口最大尺寸设置项 const maxSizeInput = this.createNumberInput('最大尺寸', 1024, (e) => { const maxSize = parseInt(e.target.value); if (maxSize > 0) { this.node.canvasInstance.updateDisplayScale(maxSize); } }); const maxSizeItem = this.createSettingItem( '窗口最大尺寸:', '限制画布显示尺寸', maxSizeInput ); const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; `; const saveButton = this.createButton('保存', () => { dialog.close(); }); const cancelButton = this.createButton('取消', () => { dialog.close(); }); settingsContainer.appendChild(maxSizeItem); buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(saveButton); dialog.appendChild(title); dialog.appendChild(settingsContainer); dialog.appendChild(buttonContainer); return dialog; } // 创建单个设置项 createSettingItem(labelText, tooltipText, inputElement) { const container = document.createElement('div'); container.style.cssText = ` display: flex; align-items: center; gap: 10px; position: relative; `; const label = document.createElement('label'); label.style.cssText = ` flex: 1; font-size: 14px; display: flex; align-items: center; gap: 5px; `; label.textContent = labelText; // 添加问号图标 const helpIcon = document.createElement('span'); helpIcon.textContent = '?'; helpIcon.style.cssText = ` display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: #666; font-size: 12px; cursor: help; `; // 创建tooltip const tooltip = document.createElement('div'); tooltip.textContent = tooltipText; tooltip.style.cssText = ` position: absolute; background: #1a1a1a; border: 1px solid #666; border-radius: 4px; padding: 8px 12px; font-size: 12px; color: #fff; width: max-content; max-width: 200px; left: 0; top: -45px; opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; z-index: 1000; line-height: 1.4; pointer-events: none; `; // 添加悬停事件 helpIcon.addEventListener('mouseenter', () => { tooltip.style.opacity = '1'; tooltip.style.visibility = 'visible'; }); helpIcon.addEventListener('mouseleave', () => { tooltip.style.opacity = '0'; tooltip.style.visibility = 'hidden'; }); label.appendChild(helpIcon); container.appendChild(label); container.appendChild(inputElement); container.appendChild(tooltip); return container; } // 创建文字功能面板 createTextPanel() { const panel = document.createElement('div'); panel.className = 'text-function-panel'; panel.style.cssText = ` position: absolute; top: calc(100% + 5px); left: 0; width: 100%; background-color: #353535; padding: 10px; display: none; flex-wrap: wrap; gap: 10px; border-top: 1px solid #666; border-radius: 8px; z-index: 100; box-sizing: border-box; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); `; // 添加文字按钮 const addTextBtn = this.createButton('添加文字', () => { const text = new fabric.IText('双击编辑文字', { fontSize: 48, fill: '#ffffff', fontFamily: 'Arial', originX: 'center', originY: 'center' }); // 为新添加的文本对象分配ID this.maxLayerId++; text.id = this.maxLayerId; // 获取画布中心点 const canvasCenter = this.canvas.getCenter(); text.set({ left: canvasCenter.left, top: canvasCenter.top }); this.canvas.add(text); this.canvas.setActiveObject(text); this.currentLayerId = text.id; this.updateLayerSelector(); this.canvas.renderAll(); }); // 字体选择器 const fontSelector = document.createElement('select'); fontSelector.style.cssText = ` padding: 5px 10px; height: 28px; background: #444; border: 1px solid #666; color: #fff; border-radius: 4px; min-width: 100px; `; const fonts = ['Arial', 'Times New Roman', 'Courier New', '宋体', '黑体', '微软雅黑']; fonts.forEach(font => { const option = document.createElement('option'); option.value = font; option.textContent = font; fontSelector.appendChild(option); }); fontSelector.oninput = () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('fontFamily', fontSelector.value); this.canvas.renderAll(); } }; // 字号选择器 const sizeSelector = document.createElement('select'); sizeSelector.style.cssText = ` padding: 5px 10px; height: 28px; background: #444; border: 1px solid #666; color: #fff; border-radius: 4px; min-width: 60px; `; const sizes = [12, 14, 16, 18, 20, 24, 28, 32, 36, 42, 48, 56, 64, 72]; sizes.forEach(size => { const option = document.createElement('option'); option.value = size; option.textContent = size; sizeSelector.appendChild(option); }); sizeSelector.oninput = () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('fontSize', parseInt(sizeSelector.value)); this.canvas.renderAll(); } }; // 文字颜色选择器 const colorPicker = document.createElement('input'); colorPicker.type = 'color'; colorPicker.value = '#ffffff'; colorPicker.style.cssText = ` width: 28px; height: 28px; padding: 0; border: 1px solid #666; border-radius: 4px; background: #444; cursor: pointer; `; colorPicker.oninput = () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('fill', colorPicker.value); this.canvas.renderAll(); } }; // 背景色选择器 const bgColorPicker = document.createElement('input'); bgColorPicker.type = 'color'; bgColorPicker.value = '#000000'; bgColorPicker.style.cssText = ` width: 28px; height: 28px; padding: 0; border: 1px solid #666; border-radius: 4px; background: #444; cursor: pointer; `; const bgColorLabel = document.createElement('span'); bgColorLabel.textContent = '背景'; bgColorLabel.style.cssText = ` color: #fff; font-size: 12px; margin-right: 4px; `; const bgColorContainer = document.createElement('div'); bgColorContainer.style.cssText = ` display: flex; align-items: center; `; bgColorContainer.appendChild(bgColorLabel); bgColorContainer.appendChild(bgColorPicker); bgColorPicker.oninput = () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('backgroundColor', bgColorPicker.value); this.canvas.renderAll(); } }; // 文字样式按钮组 const styleContainer = document.createElement('div'); styleContainer.style.cssText = ` display: flex; gap: 5px; `; // 加粗按钮 const boldBtn = this.createButton('B', () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('fontWeight', activeObj.fontWeight === 'bold' ? 'normal' : 'bold'); this.canvas.renderAll(); boldBtn.style.background = activeObj.fontWeight === 'bold' ? '#666' : '#444'; } }); boldBtn.style.fontWeight = 'bold'; boldBtn.style.padding = '5px 8px'; // 斜体按钮 const italicBtn = this.createButton('I', () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('fontStyle', activeObj.fontStyle === 'italic' ? 'normal' : 'italic'); this.canvas.renderAll(); italicBtn.style.background = activeObj.fontStyle === 'italic' ? '#666' : '#444'; } }); italicBtn.style.fontStyle = 'italic'; italicBtn.style.padding = '5px 8px'; // 下划线按钮 const underlineBtn = this.createButton('U', () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('underline', !activeObj.underline); this.canvas.renderAll(); underlineBtn.style.background = activeObj.underline ? '#666' : '#444'; } }); underlineBtn.style.textDecoration = 'underline'; underlineBtn.style.padding = '5px 8px'; styleContainer.appendChild(boldBtn); styleContainer.appendChild(italicBtn); styleContainer.appendChild(underlineBtn); // 字间距控制 const charSpacingLabel = document.createElement('span'); charSpacingLabel.textContent = '字间距'; charSpacingLabel.style.cssText = ` color: #fff; font-size: 12px; margin-right: 4px; `; const charSpacingInput = document.createElement('input'); charSpacingInput.type = 'number'; charSpacingInput.min = '0'; charSpacingInput.max = '1000'; charSpacingInput.value = '0'; charSpacingInput.style.cssText = ` width: 50px; padding: 3px; background: #444; border: 1px solid #666; color: #fff; border-radius: 4px; `; const charSpacingContainer = document.createElement('div'); charSpacingContainer.style.cssText = ` display: flex; align-items: center; `; charSpacingContainer.appendChild(charSpacingLabel); charSpacingContainer.appendChild(charSpacingInput); charSpacingInput.oninput = () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('charSpacing', parseInt(charSpacingInput.value)); this.canvas.renderAll(); } }; // 行间距控制 const lineHeightLabel = document.createElement('span'); lineHeightLabel.textContent = '行间距'; lineHeightLabel.style.cssText = ` color: #fff; font-size: 12px; margin-right: 4px; `; const lineHeightInput = document.createElement('input'); lineHeightInput.type = 'number'; lineHeightInput.min = '0'; lineHeightInput.max = '10'; lineHeightInput.step = '0.1'; lineHeightInput.value = '1'; lineHeightInput.style.cssText = ` width: 50px; padding: 3px; background: #444; border: 1px solid #666; color: #fff; border-radius: 4px; `; const lineHeightContainer = document.createElement('div'); lineHeightContainer.style.cssText = ` display: flex; align-items: center; `; lineHeightContainer.appendChild(lineHeightLabel); lineHeightContainer.appendChild(lineHeightInput); lineHeightInput.oninput = () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('lineHeight', parseFloat(lineHeightInput.value)); this.canvas.renderAll(); } }; // 描边控制 const strokeLabel = document.createElement('span'); strokeLabel.textContent = '描边'; strokeLabel.style.cssText = ` color: #fff; font-size: 12px; margin-right: 4px; `; const strokeColorPicker = document.createElement('input'); strokeColorPicker.type = 'color'; strokeColorPicker.value = '#000000'; strokeColorPicker.style.cssText = ` width: 28px; height: 28px; padding: 0; border: 1px solid #666; border-radius: 4px; background: #444; cursor: pointer; margin-right: 4px; `; const strokeWidthInput = document.createElement('input'); strokeWidthInput.type = 'number'; strokeWidthInput.min = '0'; strokeWidthInput.max = '10'; strokeWidthInput.step = '0.5'; strokeWidthInput.value = '0'; strokeWidthInput.style.cssText = ` width: 40px; padding: 3px; background: #444; border: 1px solid #666; color: #fff; border-radius: 4px; `; const strokeContainer = document.createElement('div'); strokeContainer.style.cssText = ` display: flex; align-items: center; `; strokeContainer.appendChild(strokeLabel); strokeContainer.appendChild(strokeColorPicker); strokeContainer.appendChild(strokeWidthInput); strokeColorPicker.oninput = () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('stroke', strokeColorPicker.value); this.canvas.renderAll(); } }; strokeWidthInput.oninput = () => { const activeObj = this.canvas.getActiveObject(); if (activeObj && activeObj.type === 'i-text') { activeObj.set('strokeWidth', parseFloat(strokeWidthInput.value)); this.canvas.renderAll(); } }; // 添加所有控件到面板 panel.appendChild(addTextBtn); panel.appendChild(fontSelector); panel.appendChild(sizeSelector); panel.appendChild(colorPicker); panel.appendChild(bgColorContainer); panel.appendChild(styleContainer); panel.appendChild(charSpacingContainer); panel.appendChild(lineHeightContainer); panel.appendChild(strokeContainer); // 监听文本对象选择事件,更新控件状态 this.canvas.on('selection:created', (e) => { const obj = e.selected[0]; if (obj && obj.type === 'i-text') { fontSelector.value = obj.fontFamily; sizeSelector.value = obj.fontSize; colorPicker.value = obj.fill; bgColorPicker.value = obj.backgroundColor || '#000000'; boldBtn.style.background = obj.fontWeight === 'bold' ? '#666' : '#444'; italicBtn.style.background = obj.fontStyle === 'italic' ? '#666' : '#444'; underlineBtn.style.background = obj.underline ? '#666' : '#444'; charSpacingInput.value = obj.charSpacing || 0; lineHeightInput.value = obj.lineHeight || 1.2; strokeWidthInput.value = obj.strokeWidth || 0; strokeColorPicker.value = obj.stroke || '#000000'; } }); this.canvas.on('selection:updated', (e) => { const obj = e.selected[0]; if (obj && obj.type === 'i-text') { fontSelector.value = obj.fontFamily; sizeSelector.value = obj.fontSize; colorPicker.value = obj.fill; bgColorPicker.value = obj.backgroundColor || '#000000'; boldBtn.style.background = obj.fontWeight === 'bold' ? '#666' : '#444'; italicBtn.style.background = obj.fontStyle === 'italic' ? '#666' : '#444'; underlineBtn.style.background = obj.underline ? '#666' : '#444'; charSpacingInput.value = obj.charSpacing || 0; lineHeightInput.value = obj.lineHeight || 1.2; strokeWidthInput.value = obj.strokeWidth || 0; strokeColorPicker.value = obj.stroke || '#000000'; } }); return panel; } // 切换文字功能面板显示状态 toggleTextPanel() { if (!this.textPanel) { this.textPanel = this.createTextPanel(); this.node.canvasElement.appendChild(this.textPanel); } this.textPanelVisible = !this.textPanelVisible; this.textPanel.style.display = this.textPanelVisible ? 'flex' : 'none'; } initializeControls() { const layerSelector = this.createLayerSelector(); this.container.appendChild(layerSelector); const loadButton = this.createButton('加载', () => { if (this.node.id) { queueManager.queueOutputNodes([this.node.id]); } }); // 添加文字功能按钮 const textButton = this.createButton('文字', () => { this.toggleTextPanel(); }); const resetButton = this.createButton('重置', () => { if (this.node.id) { // 保存当前节点的状态 const currentNode = this.node; const currentSize = [...currentNode.size]; const currentPos = [...currentNode.pos]; const currentOriginalSize = {...currentNode.canvasInstance.originalSize}; const currentMaxDisplaySize = currentNode.canvasInstance.maxDisplaySize; // 获取当前节点的存储键和尺寸 const STORAGE_KEY = 'canvas_size_'; const savedSize = localStorage.getItem(STORAGE_KEY + currentNode.id); // 创建新节点 let new_node = LiteGraph.createNode("FastCanvas"); new_node.pos = currentPos; app.graph.add(new_node, false); requestAnimationFrame(() => { // 复制节点信息(包括连接) node_info_copy(currentNode, new_node, true); if (new_node.canvasInstance) { // 设置画布实例的属性 new_node.canvasInstance.originalSize = currentOriginalSize; new_node.canvasInstance.maxDisplaySize = currentMaxDisplaySize; // 保持原有尺寸 new_node.size = currentSize; // 如果有保存的尺寸,转移到新节点 if (savedSize) { localStorage.setItem(STORAGE_KEY + new_node.id, savedSize); // 删除旧节点的存储 localStorage.removeItem(STORAGE_KEY + currentNode.id); } // 设置画布容器尺寸 if (new_node.canvasElement) { const width = currentOriginalSize.width; const height = currentOriginalSize.height; // 计算缩放后的尺寸 const scaledSize = new_node.canvasInstance.calculateScaledSize( width, height, currentMaxDisplaySize ); // 更新画布容器尺寸 new_node.canvasElement.style.width = `${scaledSize.width}px`; new_node.canvasElement.style.height = `${scaledSize.height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px`; // 更新画布尺寸 if (new_node.canvasInstance.canvas) { new_node.canvasInstance.updateCanvasSize( scaledSize.width, scaledSize.height ); } // 更新尺寸输入框显示 if (new_node.sizeOverlay?.updateSizeInputs) { new_node.sizeOverlay.updateSizeInputs(width, height); } } } // 更新画布 app.graph.setDirtyCanvas(true, true); // 移除旧节点 app.graph.remove(currentNode); }); } }); const settingsButton = this.createButton('⚙️', () => { const dialog = this.createSettingsDialog(); document.body.appendChild(dialog); dialog.showModal(); dialog.addEventListener('close', () => { document.body.removeChild(dialog); }); }); settingsButton.style.padding = '5px 8px'; // 添加控件到容器 this.container.appendChild(loadButton); this.container.appendChild(textButton); // 添加文字按钮 this.container.appendChild(resetButton); this.container.appendChild(settingsButton); // 监听画布选择变化 this.canvas.on('selection:created', (e) => { const obj = e.selected[0]; if (obj && obj !== this.canvas.backgroundImage) { this.currentLayerId = obj.id; this.updateLayerSelector(); } }); this.canvas.on('selection:updated', (e) => { const obj = e.selected[0]; if (obj && obj !== this.canvas.backgroundImage) { this.currentLayerId = obj.id; this.updateLayerSelector(); } }); this.canvas.on('selection:cleared', () => { this.currentLayerId = null; this.updateLayerSelector(); }); } // 获取控制面板容器 getContainer() { return this.container; } // 更新控制面板尺寸 updateSize(width) { this.container.style.width = `${width}px`; } // 更新图层ID updateLayerIds() { const objects = this.canvas.getObjects(); let maxId = this.maxLayerId; objects.forEach(obj => { if (obj !== this.canvas.backgroundImage) { if (!obj.id) { // 为新图层分配ID maxId++; obj.id = maxId; } } }); this.maxLayerId = maxId; // 更新当前选中的图层ID const activeObject = this.canvas.getActiveObject(); if (activeObject && activeObject !== this.canvas.backgroundImage) { this.currentLayerId = activeObject.id; } this.updateLayerSelector(); } // 设置背景时移除图层ID removeLayerId(obj) { if (obj.id) { delete obj.id; if (this.currentLayerId === obj.id) { this.currentLayerId = null; } this.updateLayerSelector(); } } } // 创建覆盖层扩展 class FastCanvasOverlay { static createOverlay(node) { // 创建覆盖层容器 const overlayContainer = document.createElement("div"); overlayContainer.style.cssText = ` position: absolute; left: 0; top: -35px; width: 100%; height: ${CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px; background: #353535; display: flex; gap: 10px; padding: 10px; z-index: 10; align-items: center; box-sizing: border-box; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; `; // 创建输入框样式 const inputStyle = ` padding: 5px 10px; height: 28px; line-height: 18px; background: #333; border: 1px solid #666; color: #fff; border-radius: 4px; width: 60px; box-sizing: border-box; display: flex; align-items: center; font-size: 14px; text-align: center; `; // 创建按钮样式 const buttonStyle = ` padding: 5px 10px; height: 28px; line-height: 18px; background: #444; border: 1px solid #666; color: #fff; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-sizing: border-box; font-size: 14px; `; const STORAGE_KEY = 'canvas_size_'; // 从本地存储中获取保存的尺寸 const savedSize = localStorage.getItem(STORAGE_KEY + node.id); const initialSize = savedSize ? JSON.parse(savedSize) : { width: node.canvasInstance.originalSize.width, height: node.canvasInstance.originalSize.height }; // 创建宽度输入框时使用保存的值 const widthInput = document.createElement("input"); widthInput.type = "number"; widthInput.id = "canvas-width"; widthInput.placeholder = "宽度"; widthInput.value = initialSize.width; // 使用保存的宽度 widthInput.style.cssText = inputStyle; // 创建高度输入框时使用保存的值 const heightInput = document.createElement("input"); heightInput.type = "number"; heightInput.id = "canvas-height"; heightInput.placeholder = "高度"; heightInput.value = initialSize.height; // 使用保存的高度 heightInput.style.cssText = inputStyle; // 更新尺寸显示函数(显示实际尺寸) const updateSizeInputs = (width, height) => { widthInput.value = width; heightInput.value = height; }; // 创建更新按钮 const updateButton = document.createElement("button"); updateButton.textContent = "更新尺寸"; updateButton.style.cssText = buttonStyle; updateButton.onclick = async () => { if (!node.canvasInstance?.canvas) return; let customWidth = parseInt(widthInput.value, 10); let customHeight = parseInt(heightInput.value, 10); if (isNaN(customWidth) && isNaN(customHeight)) { console.warn('[FastCanvas] 无效的尺寸输入'); return; } // 获取当前背景尺寸 const currentBg = node.canvasInstance.canvas.backgroundImage; const currentWidth = currentBg ? currentBg.width : node.canvasInstance.originalSize.width; const currentHeight = currentBg ? currentBg.height : node.canvasInstance.originalSize.height; // 处理按边等比例缩放 if (customWidth === 0 && customHeight > 0) { // 按高度等比例计算宽度 const ratio = customHeight / currentHeight; customWidth = Math.round(currentWidth * ratio); } else if (customHeight === 0 && customWidth > 0) { // 按宽度等比例计算高度 const ratio = customWidth / currentWidth; customHeight = Math.round(currentHeight * ratio); } else if (customWidth === 0 && customHeight === 0) { console.warn('[FastCanvas] 宽度和高度不能同时为0'); return; } try { // 1. 确保尺寸能被8整除 const adjustedWidth = Math.floor(customWidth / 8) * 8; const adjustedHeight = Math.floor(customHeight / 8) * 8; // 2. 更新原始尺寸 node.canvasInstance.originalSize = { width: adjustedWidth, height: adjustedHeight }; // 3. 计算显示尺寸(应用限制窗口大小的功能) const scaledSize = node.canvasInstance.calculateScaledSize( adjustedWidth, adjustedHeight, node.canvasInstance.maxDisplaySize ); // 4. 如果有现有背景,创建新的背景 const currentBg = node.canvasInstance.canvas.backgroundImage; let newBackground; if (currentBg) { // 创建临时画布 const tempCanvas = document.createElement('canvas'); tempCanvas.width = adjustedWidth; tempCanvas.height = adjustedHeight; const ctx = tempCanvas.getContext('2d'); // 获取当前背景的原始尺寸 const origWidth = currentBg.width; const origHeight = currentBg.height; // 计算填充比例 const scaleX = adjustedWidth / origWidth; const scaleY = adjustedHeight / origHeight; const scale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填充 // 计算居中位置 const scaledWidth = origWidth * scale; const scaledHeight = origHeight * scale; const left = (adjustedWidth - scaledWidth) / 2; const top = (adjustedHeight - scaledHeight) / 2; // 先填充黑色背景 ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, adjustedWidth, adjustedHeight); // 绘制并居中裁剪背景 if (currentBg instanceof fabric.Image) { // 如果是 Image 对象,使用 getElement() ctx.drawImage( currentBg.getElement(), left, top, scaledWidth, scaledHeight ); } else if (currentBg instanceof fabric.Rect) { // 如果是 Rect 对象,创建一个新的临时画布来渲染矩形 const tempRectCanvas = document.createElement('canvas'); tempRectCanvas.width = origWidth; tempRectCanvas.height = origHeight; const tempCtx = tempRectCanvas.getContext('2d'); // 绘制矩形 tempCtx.fillStyle = currentBg.fill || '#000000'; tempCtx.fillRect(0, 0, origWidth, origHeight); // 使用临时画布作为源 ctx.drawImage( tempRectCanvas, left, top, scaledWidth, scaledHeight ); } else if (currentBg._element) { // 如果有 _element 属性(某些 fabric 对象的情况) ctx.drawImage( currentBg._element, left, top, scaledWidth, scaledHeight ); } else { // 如果都不是,填充黑色 ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, adjustedWidth, adjustedHeight); } // 创建新的背景图像 newBackground = new fabric.Image(tempCanvas, { width: adjustedWidth, height: adjustedHeight, left: 0, top: 0, selectable: false, evented: false, excludeFromExport: false, originX: 'left', originY: 'top' }); } else { // 如果没有背景,创建黑色矩形 newBackground = new fabric.Rect({ width: adjustedWidth, height: adjustedHeight, left: 0, top: 0, fill: '#000000', selectable: false, evented: false, excludeFromExport: false }); } // 5. 设置新背景并等待完成 await new Promise(resolve => { node.canvasInstance.canvas.setBackgroundImage(newBackground, resolve); }); // 6. 更新背景引用 node.canvasInstance.background = newBackground; // 7. 更新画布尺寸和缩放 node.canvasInstance.updateCanvasSize(scaledSize.width, scaledSize.height); // 8. 更新输入框显示实际尺寸 widthInput.value = adjustedWidth; heightInput.value = adjustedHeight; localStorage.setItem(STORAGE_KEY + node.id, JSON.stringify({ width: adjustedWidth, height: adjustedHeight })); } catch (error) { console.error('[FastCanvas] 更新尺寸失败:', error); } }; // 创建颜色选择器 const colorInput = document.createElement("input"); colorInput.type = "color"; colorInput.value = "#000000"; colorInput.style.cssText = ` padding: 0; width: 28px; height: 28px; background: #333; border: 1px solid #666; border-radius: 4px; cursor: pointer; -webkit-appearance: none; -moz-appearance: none; appearance: none; `; colorInput.oninput = (e) => { const newColor = e.target.value; if (node.canvasInstance?.canvas) { // 创建新的纯色矩形背景 const newBackground = new fabric.Rect({ width: node.canvasInstance.originalSize.width, height: node.canvasInstance.originalSize.height, left: 0, top: 0, fill: newColor, selectable: false, evented: false, excludeFromExport: false, originX: 'left', originY: 'top' }); // 设置新背景 node.canvasInstance.canvas.setBackgroundImage(newBackground); node.canvasInstance.background = newBackground; node.canvasInstance.canvas.renderAll(); } }; // 统一的样式规则 if (!document.getElementById('fast-canvas-overlay-styles')) { const style = document.createElement('style'); style.id = 'fast-canvas-overlay-styles'; // 添加ID避免重复 style.textContent = ` input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } input[type="number"] { -moz-appearance: textfield; } input[type="number"]::placeholder { color: #888; opacity: 1; text-align: center; } input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; } input[type="color"]::-webkit-color-swatch { border: none; border-radius: 2px; } input[type="color"]::-moz-color-swatch { border: none; border-radius: 2px; } `; document.head.appendChild(style); } // 创建文件输入框(隐藏) const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.style.display = 'none'; // 创建上传按钮 const uploadButton = document.createElement("button"); uploadButton.textContent = "上传图片"; uploadButton.style.cssText = buttonStyle; // 使用已有的按钮样式 // 处理点击上传按钮 uploadButton.onclick = () => { fileInput.click(); }; // 处理文件选择 fileInput.onchange = async (e) => { const file = e.target.files[0]; if (file && node.canvasInstance) { await node.canvasInstance.handleImageUpload(file); } // 清除文件选择,允许选择相同文件 fileInput.value = ''; }; const lockButton = document.createElement("button"); lockButton.innerHTML = '🔓'; // 默认未锁定状态 lockButton.style.cssText = buttonStyle; lockButton.style.marginLeft = 'auto'; // 将按钮推到最右边 lockButton.dataset.locked = 'false'; const updateLockStyle = (isLocked) => { if (isLocked) { lockButton.innerHTML = '🔒'; lockButton.style.background = '#663c3c'; overlayContainer.classList.add('canvas-locked'); // 禁用控制按钮和输入 colorInput.disabled = true; widthInput.disabled = true; heightInput.disabled = true; updateButton.disabled = true; uploadButton.disabled = true; if (node.canvasInstance?.canvas) { const canvas = node.canvasInstance.canvas; // 创建透明覆盖层 if (!canvas.lockOverlay) { const overlayDiv = document.createElement('div'); overlayDiv.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; background: transparent; cursor: default; `; canvas.wrapperEl.appendChild(overlayDiv); canvas.lockOverlay = overlayDiv; } canvas.selection = false; canvas.skipTargetFind = true; canvas.renderAll(); } } else { lockButton.innerHTML = '🔓'; lockButton.style.background = '#444'; overlayContainer.classList.remove('canvas-locked'); // 启用控制按钮和输入 colorInput.disabled = false; widthInput.disabled = false; heightInput.disabled = false; updateButton.disabled = false; uploadButton.disabled = false; if (node.canvasInstance?.canvas) { const canvas = node.canvasInstance.canvas; // 移除覆盖层 if (canvas.lockOverlay) { canvas.wrapperEl.removeChild(canvas.lockOverlay); delete canvas.lockOverlay; } canvas.selection = true; canvas.skipTargetFind = false; canvas.renderAll(); } } }; // 添加点击事件 lockButton.onclick = () => { const isLocked = lockButton.dataset.locked === 'true'; const newLockState = !isLocked; lockButton.dataset.locked = newLockState.toString(); updateLockStyle(newLockState); // 保存锁定状态到节点实例 if (node.canvasInstance) { node.canvasInstance.isLocked = newLockState; } }; // 添加禁用状态的样式 const additionalStyles = ` .canvas-locked button:disabled, .canvas-locked input:disabled { opacity: 0.5; cursor: not-allowed; } .canvas-locked input[type="color"]:disabled { pointer-events: none; } `; // 添加样式到已有的样式表中 if (document.getElementById('fast-canvas-overlay-styles')) { const styleElement = document.getElementById('fast-canvas-overlay-styles'); styleElement.textContent += additionalStyles; } // 将元素添加到容器 overlayContainer.appendChild(colorInput); overlayContainer.appendChild(widthInput); overlayContainer.appendChild(heightInput); overlayContainer.appendChild(updateButton); overlayContainer.appendChild(uploadButton); overlayContainer.appendChild(fileInput); overlayContainer.appendChild(lockButton); // 保存更新函数引用 overlayContainer.updateSizeInputs = updateSizeInputs; if (node.canvasInstance?.isLocked) { lockButton.dataset.locked = 'false'; updateLockStyle(false); } return overlayContainer; } } function node_info_copy(src, dest, connect_both) { // copy input connections for(let i in src.inputs) { let input = src.inputs[i]; if (input.widget !== undefined) { const destWidget = dest.widgets.find(x => x.name === input.widget.name); dest.convertWidgetToInput(destWidget); } if(input.link) { let link = app.graph.links[input.link]; let src_node = app.graph.getNodeById(link.origin_id); src_node.connect(link.origin_slot, dest.id, input.name); } } // copy output connections if(connect_both) { let output_links = {}; for(let i in src.outputs) { let output = src.outputs[i]; if(output.links) { let links = []; for(let j in output.links) { links.push(app.graph.links[output.links[j]]); } output_links[output.name] = links; } } for(let i in dest.outputs) { let links = output_links[dest.outputs[i].name]; if(links) { for(let j in links) { let link = links[j]; let target_node = app.graph.getNodeById(link.target_id); dest.connect(parseInt(i), target_node, link.target_slot); } } } } dest.color = src.color; dest.bgcolor = src.bgcolor; dest.size = src.size; app.graph.afterChange(); } // 修改节点执行处理 app.registerExtension({ name: "Custom.FastCanvas", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name === "FastCanvas") { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { const result = onNodeCreated?.apply(this, arguments); // 添加随机种子输入框 const seedWidget = this.addWidget( "text", "seed", "initial_seed", // 给一个初始值 (v) => { this.seed = v; }, { multiline: false, readonly: true } ); // 生成随机种子的方法 this.generateSeed = () => { const timestamp = Date.now(); const random = Math.floor(Math.random() * 1000000); return `${timestamp}_${random}`; }; // 更新种子值的方法 this.updateSeed = () => { const newSeed = this.generateSeed(); const seedWidget = this.widgets?.find(w => w.name === "seed"); if (seedWidget) { seedWidget.value = newSeed; this.seed = newSeed; // 确保也更新节点的属性 if (!this.widgets_values) { this.widgets_values = []; } const widgetIndex = this.widgets.findIndex(w => w.name === "seed"); if (widgetIndex !== -1) { this.widgets_values[widgetIndex] = newSeed; } this.setDirtyCanvas(true, false); } }; // 添加队列执行前的钩子到种子widget上 seedWidget.beforeQueued = () => { // 如果画布未锁定,则更新种子 if (!this.canvasInstance?.isLocked) { this.updateSeed(); } }; return result; }; nodeType.prototype.onAdded = function() { if (this.id !== undefined && this.id !== -1) { // 从本地存储获取保存的尺寸 const STORAGE_KEY = 'canvas_size_'; const savedSize = localStorage.getItem(STORAGE_KEY + this.id); const size = savedSize ? JSON.parse(savedSize) : { width: CANVAS_SIZE.WIDTH, height: CANVAS_SIZE.HEIGHT }; // 创建画布容器 const element = document.createElement("div"); element.style.position = "relative"; element.style.width = "100%"; element.style.height = "100%"; // 存储 element 引用 this.canvasElement = element; // 先创建画布实例 this.canvasInstance = new FastCanvas(this, size); // 计算缩放后的尺寸 const scaledSize = this.canvasInstance.calculateScaledSize( size.width, size.height, this.canvasInstance.maxDisplaySize ); // 更新容器尺寸 element.style.minWidth = `${scaledSize.width}px`; element.style.minHeight = `${scaledSize.height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px`; // 更新画布尺寸 this.canvasInstance.updateCanvasSize(scaledSize.width, scaledSize.height); // 更新节点尺寸计算方法 this.computeSize = () => [ scaledSize.width + CANVAS_SIZE.RIGHT_MARGIN, scaledSize.height + CANVAS_SIZE.BOTTOM_MARGIN + CANVAS_SIZE.CONTROL_PANEL_HEIGHT ]; // 创建覆盖层 const overlay = FastCanvasOverlay.createOverlay(this); element.appendChild(overlay); this.sizeOverlay = overlay; // 添加画布组件 this.canvasWidget = this.addDOMWidget("canvas", "canvas", element); // 添加画布到容器 this.canvasElement.appendChild(this.canvasInstance.canvasContainer); // 强制更新节点尺寸 requestAnimationFrame(() => { this.size = this.computeSize(); this.setDirtyCanvas(true, true); // 如果有保存的尺寸,立即更新输入框显示 if (this.sizeOverlay?.updateSizeInputs) { this.sizeOverlay.updateSizeInputs(size.width, size.height); } }); } }; nodeType.prototype.onRemoved = function() { if (!this) return; const nodeId = this.id; // 清理实例 cleanup(nodeId); // 清除引用 if (this.canvasInstance) { this.canvasInstance = null; } }; // 在FastCanvas节点定义中添加 nodeType.prototype.onConfigure = function(o) { // 不要调用原始的onConfigure方法,因为会导致递归 // 发送need_update=true标记到后端 setTimeout(() => { api.fetchApi('/fast_canvas_refresh_signal', { method: 'POST', body: JSON.stringify({ node_id: this.id.toString(), need_update: true }) }).then(() => { console.log(`[FastCanvas] 已发送刷新信号给节点 ${this.id}`); }).catch(error => { console.error(`[FastCanvas] 发送刷新信号失败:`, error); }); }, 0); }; } } }); app.registerExtension({ name: "Custom.FastCanvasTool", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name === "FastCanvasTool") { // 处理连接变化 nodeType.prototype.onConnectionsChange = async function(type, index, connected, link_info) { if (!link_info || type == LiteGraph.OUTPUT) return; const stackTrace = new Error().stack; // 如果是背景图片端口,不进行删除操作 if (this.inputs[index]?.name === "bg_img") { return; } // 处理断开连接 if (!connected) { if (!stackTrace.includes('LGraphNode.prototype.connect') && !stackTrace.includes('LGraphNode.connect') && !stackTrace.includes('loadGraphData')) { // 只删除非背景图片的输入端口 if (this.inputs[index]?.name.startsWith("img_")) { this.removeInput(index); } } } // 处理新连接 if (connected) { await app.queuePromise; } // 重新编号图层输入(跳过背景图片端口) let imgIndex = 1; this.inputs.forEach(input => { if (input.name.startsWith("img_")) { const newName = `img_${imgIndex}`; if (input.name !== newName) { input.name = newName; } imgIndex++; } }); // 只有在最后一个非背景端口被连接时添加新端口 const nonBgInputs = this.inputs.filter(input => input.name.startsWith("img_")); const lastInput = nonBgInputs[nonBgInputs.length - 1]; if (lastInput?.link != null) { const newIndex = `img_${imgIndex}`; this.addInput(newIndex, "IMAGE"); } this.setDirtyCanvas(true, true); }; // 添加初始化方法 const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (onNodeCreated) { onNodeCreated.apply(this, arguments); } // 确保始终有一个背景图片端口和一个图层端口 if (!this.inputs.find(input => input.name === "bg_img")) { this.addInput("bg_img", "IMAGE"); } if (!this.inputs.find(input => input.name === "img_1")) { this.addInput("img_1", "IMAGE"); } }; } // AFL_PSD_Layer 动态输入处理 if (nodeData.name === "AFL2_PSD_Layer") { nodeType.prototype.onConnectionsChange = async function(type, index, connected, link_info) { if (!link_info || type == LiteGraph.OUTPUT) return; const stackTrace = new Error().stack; // 处理断开连接 if (!connected) { if (!stackTrace.includes('LGraphNode.prototype.connect') && !stackTrace.includes('LGraphNode.connect') && !stackTrace.includes('loadGraphData')) { if (this.inputs[index]?.name.startsWith("layer_")) { this.removeInput(index); } } } if (connected) { await app.queuePromise; } // 重新编号图层输入 let layerIndex = 1; this.inputs.forEach(input => { if (input.name.startsWith("layer_")) { const newName = `layer_${layerIndex}`; if (input.name !== newName) { input.name = newName; } layerIndex++; } }); // 最后一个端口被连接时添加新端口 const layerInputs = this.inputs.filter(input => input.name.startsWith("layer_")); const lastInput = layerInputs[layerInputs.length - 1]; if (lastInput?.link != null) { const newIndex = `layer_${layerIndex}`; this.addInput(newIndex, "IMAGE"); } this.setDirtyCanvas(true, true); }; const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (onNodeCreated) { onNodeCreated.apply(this, arguments); } if (!this.inputs.find(input => input.name === "layer_1")) { this.addInput("layer_1", "IMAGE"); } }; } } });