Files

3649 lines
132 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}
};
}
}
});