diff --git a/js/Canvas.js b/js/Canvas.js index eae5305..c663468 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -28,8 +28,28 @@ export class Canvas { this.renderInterval = 1000 / 60; this.isDirty = false; + this.dataInitialized = false; + this.pendingDataCheck = null; + this.initCanvas(); this.setupEventListeners(); + this.initNodeData(); + + // 添加混合模式列表 + this.blendModes = [ + { name: 'normal', label: '正常' }, + { name: 'multiply', label: '正片叠底' }, + { name: 'screen', label: '滤色' }, + { name: 'overlay', label: '叠加' }, + { name: 'darken', label: '变暗' }, + { name: 'lighten', label: '变亮' }, + { name: 'color-dodge', label: '颜色减淡' }, + { name: 'color-burn', label: '颜色加深' }, + { name: 'hard-light', label: '强光' }, + { name: 'soft-light', label: '柔光' }, + { name: 'difference', label: '差值' }, + { name: 'exclusion', label: '排除' } + ]; } initCanvas() { @@ -147,7 +167,7 @@ export class Canvas { const rect = this.canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; - + if (isDragging && isAltPressed) { const dx = mouseX - dragStartX; const dy = mouseY - dragStartY; @@ -216,7 +236,7 @@ export class Canvas { const centerX = layer.x + layer.width/2; const centerY = layer.y + layer.height/2; - // 计算鼠标相对于图层中心的位置 + // 计算鼠标相对于图中心的位置 const relativeX = mouseX - centerX; const relativeY = mouseY - centerY; @@ -314,6 +334,24 @@ export class Canvas { this.render(); } }); + + this.canvas.addEventListener('mousedown', (e) => { + const rect = this.canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + if (e.shiftKey) { + const result = this.getLayerAtPosition(mouseX, mouseY); + if (result) { + this.selectedLayer = result.layer; + this.showBlendModeMenu(e.clientX, e.clientY); + e.preventDefault(); // 阻止默认行为 + return; + } + } + + // ... 其余现有的mousedown处理代码 ... + }); } isRotationHandle(x, y) { @@ -327,18 +365,28 @@ export class Canvas { } addLayer(image) { - const layer = { - image: image, - x: 0, - y: 0, - width: image.width, - height: image.height, - rotation: 0, - zIndex: this.layers.length - }; - this.layers.push(layer); - this.selectedLayer = layer; - this.render(); + try { + console.log("Adding layer with image:", image); + + const layer = { + image: image, + x: (this.width - image.width) / 2, + y: (this.height - image.height) / 2, + width: image.width, + height: image.height, + rotation: 0, + zIndex: this.layers.length + }; + + this.layers.push(layer); + this.selectedLayer = layer; + this.render(); + + console.log("Layer added successfully"); + } catch (error) { + console.error("Error adding layer:", error); + throw error; + } } removeLayer(index) { @@ -439,6 +487,9 @@ export class Canvas { ctx.save(); + // 应用混合模式 + ctx.globalCompositeOperation = layer.blendMode || 'normal'; + const centerX = layer.x + layer.width/2; const centerY = layer.y + layer.height/2; const rad = layer.rotation * Math.PI / 180; @@ -585,83 +636,72 @@ export class Canvas { tempCtx.fillStyle = '#ffffff'; tempCtx.fillRect(0, 0, this.width, this.height); - // 填充黑色背景作为遮罩的基础(表示完全透明) + // 填充黑色背景作为遮罩的基础 maskCtx.fillStyle = '#000000'; maskCtx.fillRect(0, 0, this.width, this.height); - // 绘制所有图层 + // 按照zIndex顺序绘制所有图层 this.layers.sort((a, b) => a.zIndex - b.zIndex).forEach(layer => { - // 绘制主图像 + // 绘制主图像,包含混合模式 tempCtx.save(); + tempCtx.globalCompositeOperation = layer.blendMode || 'normal'; tempCtx.translate(layer.x + layer.width/2, layer.y + layer.height/2); tempCtx.rotate(layer.rotation * Math.PI / 180); - - // 创建临时画布来处理透明度 - const layerCanvas = document.createElement('canvas'); - layerCanvas.width = layer.width; - layerCanvas.height = layer.height; - const layerCtx = layerCanvas.getContext('2d'); - - // 绘制图层到临时画布 - layerCtx.drawImage( + tempCtx.drawImage( layer.image, - 0, - 0, + -layer.width/2, + -layer.height/2, layer.width, layer.height ); + tempCtx.restore(); - // 获取图层的像素数据 - const imageData = layerCtx.getImageData(0, 0, layer.width, layer.height); - const data = imageData.data; - - // 创建遮罩数据 - const maskImageData = new ImageData(layer.width, layer.height); - const maskData = maskImageData.data; - - // 处理每个像素的透明度 - for (let i = 0; i < data.length; i += 4) { - const alpha = data[i + 3] / 255; // 获取原始alpha值 - - // 设置遮罩像素值(白色表示不透明区域) - maskData[i] = maskData[i + 1] = maskData[i + 2] = 255 * alpha; - maskData[i + 3] = 255; // 遮罩本身始终不透明 - } - - // 将处理后的图层绘制到主画布 - tempCtx.drawImage(layerCanvas, -layer.width/2, -layer.height/2); - - // 绘制遮罩 + // 处理遮罩 maskCtx.save(); maskCtx.translate(layer.x + layer.width/2, layer.y + layer.height/2); maskCtx.rotate(layer.rotation * Math.PI / 180); - - // 创建临时遮罩画布 - const tempMaskCanvas = document.createElement('canvas'); - tempMaskCanvas.width = layer.width; - tempMaskCanvas.height = layer.height; - const tempMaskCtx = tempMaskCanvas.getContext('2d'); - - // 将遮罩数据绘制到临时画布 - tempMaskCtx.putImageData(maskImageData, 0, 0); - - // 使用lighter混合模式来叠加透明度 maskCtx.globalCompositeOperation = 'lighter'; - maskCtx.drawImage(tempMaskCanvas, -layer.width/2, -layer.height/2); + // 如果图层有遮罩,使用它 + if (layer.mask) { + maskCtx.drawImage(layer.mask, -layer.width/2, -layer.height/2, layer.width, layer.height); + } else { + // 如果没有遮罩,使用图层的alpha通道 + const layerCanvas = document.createElement('canvas'); + layerCanvas.width = layer.width; + layerCanvas.height = layer.height; + const layerCtx = layerCanvas.getContext('2d'); + layerCtx.drawImage(layer.image, 0, 0, layer.width, layer.height); + const imageData = layerCtx.getImageData(0, 0, layer.width, layer.height); + + // 创建遮罩画布 + const alphaCanvas = document.createElement('canvas'); + alphaCanvas.width = layer.width; + alphaCanvas.height = layer.height; + const alphaCtx = alphaCanvas.getContext('2d'); + const alphaData = alphaCtx.createImageData(layer.width, layer.height); + + // 提取alpha通道 + for (let i = 0; i < imageData.data.length; i += 4) { + alphaData.data[i] = alphaData.data[i + 1] = alphaData.data[i + 2] = imageData.data[i + 3]; + alphaData.data[i + 3] = 255; + } + + alphaCtx.putImageData(alphaData, 0, 0); + maskCtx.drawImage(alphaCanvas, -layer.width/2, -layer.height/2, layer.width, layer.height); + } maskCtx.restore(); - tempCtx.restore(); }); - // 在保存遮罩之前反转遮罩数据 - const maskData = maskCtx.getImageData(0, 0, this.width, this.height); - const data = maskData.data; - for (let i = 0; i < data.length; i += 4) { - // 反转RGB值(255 - 原值) - data[i] = data[i + 1] = data[i + 2] = 255 - data[i]; - data[i + 3] = 255; // Alpha保持不变 + // 反转最终的遮罩 + const finalMaskData = maskCtx.getImageData(0, 0, this.width, this.height); + for (let i = 0; i < finalMaskData.data.length; i += 4) { + finalMaskData.data[i] = + finalMaskData.data[i + 1] = + finalMaskData.data[i + 2] = 255 - finalMaskData.data[i]; + finalMaskData.data[i + 3] = 255; } - maskCtx.putImageData(maskData, 0, 0); + maskCtx.putImageData(finalMaskData, 0, 0); // 保存主图像和遮罩 tempCanvas.toBlob(async (blob) => { @@ -939,4 +979,535 @@ export class Canvas { this.selectedLayer = layer; this.render(); } + + processInputData(nodeData) { + if (nodeData.input_image) { + this.addInputImage(nodeData.input_image); + } + if (nodeData.input_mask) { + this.addInputMask(nodeData.input_mask); + } + } + + addInputImage(imageData) { + const layer = new ImageLayer(imageData); + this.layers.push(layer); + this.updateCanvas(); + } + + addInputMask(maskData) { + if (this.inputImage) { + const mask = new MaskLayer(maskData); + mask.linkToLayer(this.inputImage); + this.masks.push(mask); + this.updateCanvas(); + } + } + + async addInputToCanvas(inputImage, inputMask) { + try { + console.log("Adding input to canvas:", { inputImage }); + + // 创建临时画布 + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = inputImage.width; + tempCanvas.height = inputImage.height; + + // 将数据绘制到临时画布 + const imgData = new ImageData( + inputImage.data, + inputImage.width, + inputImage.height + ); + tempCtx.putImageData(imgData, 0, 0); + + // 创建新图像 + const image = new Image(); + await new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + image.src = tempCanvas.toDataURL(); + }); + + // 计算缩放比例 + const scale = Math.min( + this.width / inputImage.width * 0.8, + this.height / inputImage.height * 0.8 + ); + + // 创建新图层 + const layer = { + image: image, + x: (this.width - inputImage.width * scale) / 2, + y: (this.height - inputImage.height * scale) / 2, + width: inputImage.width * scale, + height: inputImage.height * scale, + rotation: 0, + zIndex: this.layers.length + }; + + // 如果有遮罩数据,添加到图层 + if (inputMask) { + layer.mask = inputMask.data; + } + + // 添加图层并选中 + this.layers.push(layer); + this.selectedLayer = layer; + + // 渲染画布 + this.render(); + console.log("Layer added successfully"); + + return true; + + } catch (error) { + console.error("Error in addInputToCanvas:", error); + throw error; + } + } + + // 改进图像转换方法 + async convertTensorToImage(tensor) { + try { + console.log("Converting tensor to image:", tensor); + + if (!tensor || !tensor.data || !tensor.width || !tensor.height) { + throw new Error("Invalid tensor data"); + } + + // 创建临时画布 + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = tensor.width; + canvas.height = tensor.height; + + // 创建像数据 + const imageData = new ImageData( + new Uint8ClampedArray(tensor.data), + tensor.width, + tensor.height + ); + + // 将数据绘制到画布 + ctx.putImageData(imageData, 0, 0); + + // 创建新图像 + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = (e) => reject(new Error("Failed to load image: " + e)); + img.src = canvas.toDataURL(); + }); + } catch (error) { + console.error("Error converting tensor to image:", error); + throw error; + } + } + + // 改进遮罩转换方法 + async convertTensorToMask(tensor) { + if (!tensor || !tensor.data) { + throw new Error("Invalid mask tensor"); + } + + try { + // 确保数据是Float32Array + return new Float32Array(tensor.data); + } catch (error) { + throw new Error(`Mask conversion failed: ${error.message}`); + } + } + + // 改进数据初始化方法 + async initNodeData() { + try { + console.log("Starting node data initialization..."); + + // 检查节点和输入是否存在 + if (!this.node || !this.node.inputs) { + console.log("Node or inputs not ready"); + return this.scheduleDataCheck(); + } + + // 检查图像输入 + if (this.node.inputs[0] && this.node.inputs[0].link) { + const imageLinkId = this.node.inputs[0].link; + const imageData = app.nodeOutputs[imageLinkId]; + + if (imageData) { + console.log("Found image data:", imageData); + await this.processImageData(imageData); + this.dataInitialized = true; + } else { + console.log("Image data not available yet"); + return this.scheduleDataCheck(); + } + } + + // 检查遮罩输入 + if (this.node.inputs[1] && this.node.inputs[1].link) { + const maskLinkId = this.node.inputs[1].link; + const maskData = app.nodeOutputs[maskLinkId]; + + if (maskData) { + console.log("Found mask data:", maskData); + await this.processMaskData(maskData); + } + } + + } catch (error) { + console.error("Error in initNodeData:", error); + return this.scheduleDataCheck(); + } + } + + // 添加数据检查调度方法 + scheduleDataCheck() { + if (this.pendingDataCheck) { + clearTimeout(this.pendingDataCheck); + } + + this.pendingDataCheck = setTimeout(() => { + this.pendingDataCheck = null; + if (!this.dataInitialized) { + this.initNodeData(); + } + }, 1000); // 1秒后重试 + } + + // 修改图像数据处理方法 + async processImageData(imageData) { + try { + if (!imageData) return; + + console.log("Processing image data:", { + type: typeof imageData, + isArray: Array.isArray(imageData), + shape: imageData.shape, + hasData: !!imageData.data + }); + + // 处理数组格式 + if (Array.isArray(imageData)) { + imageData = imageData[0]; + } + + // 验证数据格式 + if (!imageData.shape || !imageData.data) { + throw new Error("Invalid image data format"); + } + + // 保持原始尺寸和比例 + const originalWidth = imageData.shape[2]; + const originalHeight = imageData.shape[1]; + + // 计算适当的缩放比例 + const scale = Math.min( + this.width / originalWidth * 0.8, + this.height / originalHeight * 0.8 + ); + + // 转换数据 + const convertedData = this.convertTensorToImageData(imageData); + if (convertedData) { + const image = await this.createImageFromData(convertedData); + + // 使用计算的缩放比例添加图层 + this.addScaledLayer(image, scale); + console.log("Image layer added successfully with scale:", scale); + } + } catch (error) { + console.error("Error processing image data:", error); + throw error; + } + } + + // 添加新的缩放图层方法 + addScaledLayer(image, scale) { + try { + const scaledWidth = image.width * scale; + const scaledHeight = image.height * scale; + + const layer = { + image: image, + x: (this.width - scaledWidth) / 2, + y: (this.height - scaledHeight) / 2, + width: scaledWidth, + height: scaledHeight, + rotation: 0, + zIndex: this.layers.length, + originalWidth: image.width, + originalHeight: image.height + }; + + this.layers.push(layer); + this.selectedLayer = layer; + this.render(); + + console.log("Scaled layer added:", { + originalSize: `${image.width}x${image.height}`, + scaledSize: `${scaledWidth}x${scaledHeight}`, + scale: scale + }); + } catch (error) { + console.error("Error adding scaled layer:", error); + throw error; + } + } + + // 改进张量转换方法 + convertTensorToImageData(tensor) { + try { + const shape = tensor.shape; + const height = shape[1]; + const width = shape[2]; + const channels = shape[3]; + + console.log("Converting tensor:", { + shape: shape, + dataRange: { + min: tensor.min_val, + max: tensor.max_val + } + }); + + // 创建图像数据 + const imageData = new ImageData(width, height); + const data = new Uint8ClampedArray(width * height * 4); + + // 重建数据结构 + const flatData = tensor.data; + const pixelCount = width * height; + + for (let i = 0; i < pixelCount; i++) { + const pixelIndex = i * 4; + const tensorIndex = i * channels; + + // 正确处理RGB通道 + for (let c = 0; c < channels; c++) { + const value = flatData[tensorIndex + c]; + // 根据实际值范围行映射 + const normalizedValue = (value - tensor.min_val) / (tensor.max_val - tensor.min_val); + data[pixelIndex + c] = Math.round(normalizedValue * 255); + } + + // Alpha通道 + data[pixelIndex + 3] = 255; + } + + imageData.data.set(data); + return imageData; + } catch (error) { + console.error("Error converting tensor:", error); + return null; + } + } + + // 添加图像创建方法 + async createImageFromData(imageData) { + return new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + canvas.width = imageData.width; + canvas.height = imageData.height; + const ctx = canvas.getContext('2d'); + ctx.putImageData(imageData, 0, 0); + + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = canvas.toDataURL(); + }); + } + + // 添加数据重试机制 + async retryDataLoad(maxRetries = 3, delay = 1000) { + for (let i = 0; i < maxRetries; i++) { + try { + await this.initNodeData(); + return; + } catch (error) { + console.warn(`Retry ${i + 1}/${maxRetries} failed:`, error); + if (i < maxRetries - 1) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + console.error("Failed to load data after", maxRetries, "retries"); + } + + async processMaskData(maskData) { + try { + if (!maskData) return; + + console.log("Processing mask data:", maskData); + + // 处理数组格式 + if (Array.isArray(maskData)) { + maskData = maskData[0]; + } + + // 检查数据格式 + if (!maskData.shape || !maskData.data) { + throw new Error("Invalid mask data format"); + } + + // 如果有选中的图层,应用遮罩 + if (this.selectedLayer) { + const maskTensor = await this.convertTensorToMask(maskData); + this.selectedLayer.mask = maskTensor; + this.render(); + console.log("Mask applied to selected layer"); + } + } catch (error) { + console.error("Error processing mask data:", error); + } + } + + async loadImageFromCache(base64Data) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = base64Data; + }); + } + + async importImage(cacheData) { + try { + console.log("Starting image import with cache data"); + const img = await this.loadImageFromCache(cacheData.image); + const mask = cacheData.mask ? await this.loadImageFromCache(cacheData.mask) : null; + + // 计算缩放比例 + const scale = Math.min( + this.width / img.width * 0.8, + this.height / img.height * 0.8 + ); + + // 创建临时画布来合并图像和遮罩 + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = img.width; + tempCanvas.height = img.height; + const tempCtx = tempCanvas.getContext('2d'); + + // 绘制图像 + tempCtx.drawImage(img, 0, 0); + + // 如果有遮罩,应用遮罩 + if (mask) { + const imageData = tempCtx.getImageData(0, 0, img.width, img.height); + const maskCanvas = document.createElement('canvas'); + maskCanvas.width = img.width; + maskCanvas.height = img.height; + const maskCtx = maskCanvas.getContext('2d'); + maskCtx.drawImage(mask, 0, 0); + const maskData = maskCtx.getImageData(0, 0, img.width, img.height); + + // 应用遮罩到alpha通道 + for (let i = 0; i < imageData.data.length; i += 4) { + imageData.data[i + 3] = maskData.data[i]; + } + + tempCtx.putImageData(imageData, 0, 0); + } + + // 创建最终图像 + const finalImage = new Image(); + await new Promise((resolve) => { + finalImage.onload = resolve; + finalImage.src = tempCanvas.toDataURL(); + }); + + // 创建新图层 + const layer = { + image: finalImage, + x: (this.width - img.width * scale) / 2, + y: (this.height - img.height * scale) / 2, + width: img.width * scale, + height: img.height * scale, + rotation: 0, + zIndex: this.layers.length + }; + + this.layers.push(layer); + this.selectedLayer = layer; + this.render(); + + } catch (error) { + console.error('Error importing image:', error); + } + } + + // 添加混合模式菜单方法 + showBlendModeMenu(x, y) { + // 移除已存在的菜单 + const existingMenu = document.getElementById('blend-mode-menu'); + if (existingMenu) { + document.body.removeChild(existingMenu); + } + + const menu = document.createElement('div'); + menu.id = 'blend-mode-menu'; + menu.style.cssText = ` + position: fixed; + left: ${x}px; + top: ${y}px; + background: #2a2a2a; + border: 1px solid #3a3a3a; + border-radius: 4px; + padding: 5px; + z-index: 1000; + box-shadow: 0 2px 10px rgba(0,0,0,0.3); + `; + + this.blendModes.forEach(mode => { + const option = document.createElement('div'); + option.style.cssText = ` + padding: 5px 10px; + color: white; + cursor: pointer; + transition: background-color 0.2s; + `; + option.textContent = `${mode.label} (${mode.name})`; + + // 高亮当前选中的混合模式 + if (this.selectedLayer && this.selectedLayer.blendMode === mode.name) { + option.style.backgroundColor = '#3a3a3a'; + } + + option.onmouseover = () => { + option.style.backgroundColor = '#3a3a3a'; + }; + option.onmouseout = () => { + if (!(this.selectedLayer && this.selectedLayer.blendMode === mode.name)) { + option.style.backgroundColor = ''; + } + }; + + option.onclick = () => { + if (this.selectedLayer) { + this.selectedLayer.blendMode = mode.name; + this.render(); + } + document.body.removeChild(menu); + }; + + menu.appendChild(option); + }); + + document.body.appendChild(menu); + + // 点击其他地方关闭菜单 + const closeMenu = (e) => { + if (!menu.contains(e.target)) { + document.body.removeChild(menu); + document.removeEventListener('mousedown', closeMenu); + } + }; + setTimeout(() => { + document.addEventListener('mousedown', closeMenu); + }, 0); + } }