diff --git a/js/Canvas.js b/js/Canvas.js new file mode 100644 index 0000000..6798f12 --- /dev/null +++ b/js/Canvas.js @@ -0,0 +1,865 @@ +export class Canvas { + constructor(node, widget) { + this.node = node; + this.widget = widget; + this.canvas = document.createElement('canvas'); + this.ctx = this.canvas.getContext('2d'); + this.width = 512; + this.height = 512; + this.layers = []; + this.selectedLayer = null; + this.isRotating = false; + this.rotationStartAngle = 0; + this.rotationCenter = { x: 0, y: 0 }; + this.selectedLayers = []; + this.isCtrlPressed = false; + + this.offscreenCanvas = document.createElement('canvas'); + this.offscreenCtx = this.offscreenCanvas.getContext('2d', { + alpha: false + }); + this.gridCache = document.createElement('canvas'); + this.gridCacheCtx = this.gridCache.getContext('2d', { + alpha: false + }); + + this.renderAnimationFrame = null; + this.lastRenderTime = 0; + this.renderInterval = 1000 / 60; + this.isDirty = false; + + this.initCanvas(); + this.setupEventListeners(); + } + + initCanvas() { + this.canvas.width = this.width; + this.canvas.height = this.height; + this.canvas.style.border = '1px solid black'; + this.canvas.style.maxWidth = '100%'; + this.canvas.style.backgroundColor = '#606060'; + } + + setupEventListeners() { + let isDragging = false; + let lastX = 0; + let lastY = 0; + let isRotating = false; + let isResizing = false; + let resizeHandle = null; + let lastClickTime = 0; + + document.addEventListener('keydown', (e) => { + if (e.key === 'Control') { + this.isCtrlPressed = true; + } + }); + + document.addEventListener('keyup', (e) => { + if (e.key === 'Control') { + this.isCtrlPressed = false; + } + }); + + this.canvas.addEventListener('mousedown', (e) => { + const currentTime = new Date().getTime(); + const rect = this.canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + if (currentTime - lastClickTime < 300) { + this.selectedLayers = []; + this.selectedLayer = null; + this.render(); + return; + } + lastClickTime = currentTime; + + const result = this.getLayerAtPosition(mouseX, mouseY); + + if (result) { + const clickedLayer = result.layer; + + if (this.isCtrlPressed) { + const index = this.selectedLayers.indexOf(clickedLayer); + if (index === -1) { + this.selectedLayers.push(clickedLayer); + this.selectedLayer = clickedLayer; + } else { + this.selectedLayers.splice(index, 1); + this.selectedLayer = this.selectedLayers[this.selectedLayers.length - 1] || null; + } + } else { + if (!this.selectedLayers.includes(clickedLayer)) { + this.selectedLayers = [clickedLayer]; + this.selectedLayer = clickedLayer; + } + } + + if (this.isRotationHandle(mouseX, mouseY)) { + isRotating = true; + this.rotationCenter.x = this.selectedLayer.x + this.selectedLayer.width/2; + this.rotationCenter.y = this.selectedLayer.y + this.selectedLayer.height/2; + this.rotationStartAngle = Math.atan2( + mouseY - this.rotationCenter.y, + mouseX - this.rotationCenter.x + ); + } else { + isDragging = true; + lastX = mouseX; + lastY = mouseY; + } + } else { + if (!this.isCtrlPressed) { + this.selectedLayers = []; + this.selectedLayer = null; + } + } + this.render(); + }); + + this.canvas.addEventListener('mousemove', (e) => { + if (!this.selectedLayer) return; + + const rect = this.canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + if (isResizing && resizeHandle) { + const dx = mouseX - lastX; + const dy = mouseY - lastY; + + this.selectedLayers.forEach(layer => { + const originalWidth = layer.width; + const originalHeight = layer.height; + const originalX = layer.x; + const originalY = layer.y; + + switch(resizeHandle) { + case 'nw': + layer.width = Math.max(20, originalWidth - dx); + layer.height = Math.max(20, originalHeight - dy); + layer.x = originalX + (originalWidth - layer.width); + layer.y = originalY + (originalHeight - layer.height); + break; + case 'ne': + layer.width = Math.max(20, originalWidth + dx); + layer.height = Math.max(20, originalHeight - dy); + layer.y = originalY + (originalHeight - layer.height); + break; + case 'se': + layer.width = Math.max(20, originalWidth + dx); + layer.height = Math.max(20, originalHeight + dy); + break; + case 'sw': + layer.width = Math.max(20, originalWidth - dx); + layer.height = Math.max(20, originalHeight + dy); + layer.x = originalX + (originalWidth - layer.width); + break; + } + }); + + lastX = mouseX; + lastY = mouseY; + this.render(); + } else if (isRotating) { + const currentAngle = Math.atan2( + mouseY - this.rotationCenter.y, + mouseX - this.rotationCenter.x + ); + let rotation = (currentAngle - this.rotationStartAngle) * (180/Math.PI); + const snap = 15; + rotation = Math.round(rotation / snap) * snap; + + this.selectedLayers.forEach(layer => { + layer.rotation = rotation; + }); + this.render(); + } else if (isDragging) { + const dx = mouseX - lastX; + const dy = mouseY - lastY; + + this.selectedLayers.forEach(layer => { + layer.x += dx; + layer.y += dy; + }); + + lastX = mouseX; + lastY = mouseY; + this.render(); + } + + const cursor = this.getResizeHandle(mouseX, mouseY) + ? 'nw-resize' + : this.isRotationHandle(mouseX, mouseY) + ? 'grab' + : isDragging ? 'move' : 'default'; + this.canvas.style.cursor = cursor; + }); + + this.canvas.addEventListener('mouseup', () => { + isDragging = false; + isRotating = false; + }); + + this.canvas.addEventListener('mouseleave', () => { + isDragging = false; + isRotating = false; + }); + + // 添加鼠标滚轮缩放功能 + this.canvas.addEventListener('wheel', (e) => { + if (!this.selectedLayer) return; + + e.preventDefault(); + const scaleFactor = e.deltaY > 0 ? 0.95 : 1.05; + + // 如果按住Shift键,则进行旋转而不是缩放 + if (e.shiftKey) { + const rotateAngle = e.deltaY > 0 ? -5 : 5; + this.selectedLayers.forEach(layer => { + layer.rotation = (layer.rotation + rotateAngle) % 360; + }); + } else { + // 从鼠标位置为中心进行缩放 + const rect = this.canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + this.selectedLayers.forEach(layer => { + const centerX = layer.x + layer.width/2; + const centerY = layer.y + layer.height/2; + + // 计算鼠标相对于图层中心的位置 + const relativeX = mouseX - centerX; + const relativeY = mouseY - centerY; + + // 更新尺寸 + const oldWidth = layer.width; + const oldHeight = layer.height; + layer.width *= scaleFactor; + layer.height *= scaleFactor; + + // 调整位置以保持鼠标指向的点不变 + layer.x += (oldWidth - layer.width) / 2; + layer.y += (oldHeight - layer.height) / 2; + }); + } + this.render(); + }); + + // 优化旋转控制逻辑 + let initialRotation = 0; + let initialAngle = 0; + + this.canvas.addEventListener('mousemove', (e) => { + // ... 其他代码保持不变 ... + + if (isRotating) { + const rect = this.canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const centerX = this.selectedLayer.x + this.selectedLayer.width/2; + const centerY = this.selectedLayer.y + this.selectedLayer.height/2; + + // 计算当前角度 + const angle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI; + + if (e.shiftKey) { + // 按住Shift键时启用15度角度吸附 + const snap = 15; + const rotation = Math.round((angle - initialAngle + initialRotation) / snap) * snap; + this.selectedLayers.forEach(layer => { + layer.rotation = rotation; + }); + } else { + // 正常旋转 + const rotation = angle - initialAngle + initialRotation; + this.selectedLayers.forEach(layer => { + layer.rotation = rotation; + }); + } + this.render(); + } + }); + + this.canvas.addEventListener('mousedown', (e) => { + // ... 其他代码保持不变 ... + + if (this.isRotationHandle(mouseX, mouseY)) { + isRotating = true; + const centerX = this.selectedLayer.x + this.selectedLayer.width/2; + const centerY = this.selectedLayer.y + this.selectedLayer.height/2; + initialRotation = this.selectedLayer.rotation; + initialAngle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI; + } + }); + + // 添加键盘快捷键 + document.addEventListener('keydown', (e) => { + if (!this.selectedLayer) return; + + const step = e.shiftKey ? 1 : 5; // Shift键按下时更精细的控制 + + switch(e.key) { + case 'ArrowLeft': + this.selectedLayers.forEach(layer => layer.x -= step); + break; + case 'ArrowRight': + this.selectedLayers.forEach(layer => layer.x += step); + break; + case 'ArrowUp': + this.selectedLayers.forEach(layer => layer.y -= step); + break; + case 'ArrowDown': + this.selectedLayers.forEach(layer => layer.y += step); + break; + case '[': + this.selectedLayers.forEach(layer => layer.rotation -= step); + break; + case ']': + this.selectedLayers.forEach(layer => layer.rotation += step); + break; + } + + if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', '[', ']'].includes(e.key)) { + e.preventDefault(); + this.render(); + } + }); + } + + isRotationHandle(x, y) { + if (!this.selectedLayer) return false; + + const handleX = this.selectedLayer.x + this.selectedLayer.width/2; + const handleY = this.selectedLayer.y - 20; + const handleRadius = 5; + + return Math.sqrt(Math.pow(x - handleX, 2) + Math.pow(y - handleY, 2)) <= handleRadius; + } + + 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(); + } + + removeLayer(index) { + if (index >= 0 && index < this.layers.length) { + this.layers.splice(index, 1); + this.selectedLayer = this.layers[this.layers.length - 1] || null; + this.render(); + } + } + + moveLayer(fromIndex, toIndex) { + if (fromIndex >= 0 && fromIndex < this.layers.length && + toIndex >= 0 && toIndex < this.layers.length) { + const layer = this.layers.splice(fromIndex, 1)[0]; + this.layers.splice(toIndex, 0, layer); + this.render(); + } + } + + resizeLayer(scale) { + this.selectedLayers.forEach(layer => { + layer.width *= scale; + layer.height *= scale; + }); + this.render(); + } + + rotateLayer(angle) { + this.selectedLayers.forEach(layer => { + layer.rotation += angle; + }); + this.render(); + } + + updateCanvasSize(width, height) { + this.width = width; + this.height = height; + + this.canvas.width = width; + this.canvas.height = height; + + // 调整所有图层的位置和大小 + this.layers.forEach(layer => { + const scale = Math.min( + width / layer.image.width * 0.8, + height / layer.image.height * 0.8 + ); + layer.width = layer.image.width * scale; + layer.height = layer.image.height * scale; + layer.x = (width - layer.width) / 2; + layer.y = (height - layer.height) / 2; + }); + + this.render(); + } + + render() { + if (this.renderAnimationFrame) { + this.isDirty = true; + return; + } + + this.renderAnimationFrame = requestAnimationFrame(() => { + const now = performance.now(); + if (now - this.lastRenderTime >= this.renderInterval) { + this.lastRenderTime = now; + this.actualRender(); + this.isDirty = false; + } + + if (this.isDirty) { + this.renderAnimationFrame = null; + this.render(); + } else { + this.renderAnimationFrame = null; + } + }); + } + + actualRender() { + if (this.offscreenCanvas.width !== this.width || + this.offscreenCanvas.height !== this.height) { + this.offscreenCanvas.width = this.width; + this.offscreenCanvas.height = this.height; + } + + const ctx = this.offscreenCtx; + + ctx.fillStyle = '#606060'; + ctx.fillRect(0, 0, this.width, this.height); + + this.drawCachedGrid(); + + const sortedLayers = [...this.layers].sort((a, b) => a.zIndex - b.zIndex); + + sortedLayers.forEach(layer => { + if (!layer.image || layer.width <= 0 || layer.height <= 0) return; + + ctx.save(); + + const centerX = layer.x + layer.width/2; + const centerY = layer.y + layer.height/2; + const rad = layer.rotation * Math.PI / 180; + + ctx.setTransform( + Math.cos(rad), Math.sin(rad), + -Math.sin(rad), Math.cos(rad), + centerX, centerY + ); + + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + + ctx.drawImage( + layer.image, + -layer.width/2, + -layer.height/2, + layer.width, + layer.height + ); + + if (this.selectedLayers.includes(layer)) { + this.drawSelectionFrame(layer); + } + + ctx.restore(); + }); + + this.ctx.drawImage(this.offscreenCanvas, 0, 0); + } + + drawCachedGrid() { + if (this.gridCache.width !== this.width || + this.gridCache.height !== this.height) { + this.gridCache.width = this.width; + this.gridCache.height = this.height; + + const ctx = this.gridCacheCtx; + const gridSize = 20; + + ctx.beginPath(); + ctx.strokeStyle = '#e0e0e0'; + ctx.lineWidth = 0.5; + + for(let y = 0; y < this.height; y += gridSize) { + ctx.moveTo(0, y); + ctx.lineTo(this.width, y); + } + + for(let x = 0; x < this.width; x += gridSize) { + ctx.moveTo(x, 0); + ctx.lineTo(x, this.height); + } + + ctx.stroke(); + } + + this.offscreenCtx.drawImage(this.gridCache, 0, 0); + } + + drawSelectionFrame(layer) { + const ctx = this.offscreenCtx; + + ctx.beginPath(); + + ctx.rect(-layer.width/2, -layer.height/2, layer.width, layer.height); + + ctx.moveTo(0, -layer.height/2); + ctx.lineTo(0, -layer.height/2 - 20); + + ctx.strokeStyle = '#00ff00'; + ctx.lineWidth = 2; + ctx.stroke(); + + ctx.beginPath(); + + const points = [ + {x: 0, y: -layer.height/2 - 20}, + {x: -layer.width/2, y: -layer.height/2}, + {x: layer.width/2, y: -layer.height/2}, + {x: layer.width/2, y: layer.height/2}, + {x: -layer.width/2, y: layer.height/2} + ]; + + points.forEach(point => { + ctx.moveTo(point.x, point.y); + ctx.arc(point.x, point.y, 5, 0, Math.PI * 2); + }); + + ctx.fillStyle = '#ffffff'; + ctx.fill(); + ctx.stroke(); + } + + async saveToServer(fileName) { + return new Promise((resolve) => { + // 创建临时画布 + const tempCanvas = document.createElement('canvas'); + const maskCanvas = document.createElement('canvas'); + tempCanvas.width = this.width; + tempCanvas.height = this.height; + maskCanvas.width = this.width; + maskCanvas.height = this.height; + + const tempCtx = tempCanvas.getContext('2d'); + const maskCtx = maskCanvas.getContext('2d'); + + // 填充白色背景 + tempCtx.fillStyle = '#ffffff'; + tempCtx.fillRect(0, 0, this.width, this.height); + + // 填充黑色背景作为遮罩的基础(表示完全透明) + maskCtx.fillStyle = '#000000'; + maskCtx.fillRect(0, 0, this.width, this.height); + + // 绘制所有图层 + this.layers.sort((a, b) => a.zIndex - b.zIndex).forEach(layer => { + // 绘制主图像 + tempCtx.save(); + 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( + layer.image, + 0, + 0, + layer.width, + layer.height + ); + + // 获取图层的像素数据 + 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); + + 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保持不变 + } + maskCtx.putImageData(maskData, 0, 0); + + // 保存主图像和遮罩 + tempCanvas.toBlob(async (blob) => { + const formData = new FormData(); + formData.append("image", blob, fileName); + formData.append("overwrite", "true"); + + try { + const resp = await fetch("/upload/image", { + method: "POST", + body: formData, + }); + + if (resp.status === 200) { + // 保存遮罩图像 + maskCanvas.toBlob(async (maskBlob) => { + const maskFormData = new FormData(); + const maskFileName = fileName.replace('.png', '_mask.png'); + maskFormData.append("image", maskBlob, maskFileName); + maskFormData.append("overwrite", "true"); + + try { + const maskResp = await fetch("/upload/image", { + method: "POST", + body: maskFormData, + }); + + if (maskResp.status === 200) { + const data = await resp.json(); + this.widget.value = data.name; + resolve(true); + } else { + console.error("Error saving mask: " + maskResp.status); + resolve(false); + } + } catch (error) { + console.error("Error saving mask:", error); + resolve(false); + } + }, "image/png"); + } else { + console.error(resp.status + " - " + resp.statusText); + resolve(false); + } + } catch (error) { + console.error(error); + resolve(false); + } + }, "image/png"); + }); + } + + moveLayerUp() { + if (!this.selectedLayer) return; + const index = this.layers.indexOf(this.selectedLayer); + if (index < this.layers.length - 1) { + const temp = this.layers[index].zIndex; + this.layers[index].zIndex = this.layers[index + 1].zIndex; + this.layers[index + 1].zIndex = temp; + [this.layers[index], this.layers[index + 1]] = [this.layers[index + 1], this.layers[index]]; + this.render(); + } + } + + moveLayerDown() { + if (!this.selectedLayer) return; + const index = this.layers.indexOf(this.selectedLayer); + if (index > 0) { + const temp = this.layers[index].zIndex; + this.layers[index].zIndex = this.layers[index - 1].zIndex; + this.layers[index - 1].zIndex = temp; + [this.layers[index], this.layers[index - 1]] = [this.layers[index - 1], this.layers[index]]; + this.render(); + } + } + + getLayerAtPosition(x, y) { + // 获取画布的实际显示尺寸和位置 + const rect = this.canvas.getBoundingClientRect(); + + // 计算画布的缩放比例 + const displayWidth = rect.width; + const displayHeight = rect.height; + const scaleX = this.width / displayWidth; + const scaleY = this.height / displayHeight; + + // 计算鼠标在画布上的实际位置 + const canvasX = (x) * scaleX; + const canvasY = (y) * scaleY; + + // 从上层到下层遍历所有图层 + for (let i = this.layers.length - 1; i >= 0; i--) { + const layer = this.layers[i]; + + // 计算旋转后的点击位置 + const centerX = layer.x + layer.width/2; + const centerY = layer.y + layer.height/2; + const rad = -layer.rotation * Math.PI / 180; + + // 将点击坐标转换到图层的本地坐标系 + const dx = canvasX - centerX; + const dy = canvasY - centerY; + const rotatedX = dx * Math.cos(rad) - dy * Math.sin(rad) + centerX; + const rotatedY = dx * Math.sin(rad) + dy * Math.cos(rad) + centerY; + + // 检查点击位置是否在图层范围内 + if (rotatedX >= layer.x && + rotatedX <= layer.x + layer.width && + rotatedY >= layer.y && + rotatedY <= layer.y + layer.height) { + + // 创建临时画布来检查透明度 + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = layer.width; + tempCanvas.height = layer.height; + + // 绘制图层到临时画布 + tempCtx.save(); + tempCtx.clearRect(0, 0, layer.width, layer.height); + tempCtx.drawImage( + layer.image, + 0, + 0, + layer.width, + layer.height + ); + tempCtx.restore(); + + // 获取点击位置的像素数据 + const localX = rotatedX - layer.x; + const localY = rotatedY - layer.y; + + try { + const pixel = tempCtx.getImageData( + Math.round(localX), + Math.round(localY), + 1, 1 + ).data; + // 检查像素的alpha值 + if (pixel[3] > 10) { + return { + layer: layer, + localX: localX, + localY: localY + }; + } + } catch(e) { + console.error("Error checking pixel transparency:", e); + } + } + } + return null; + } + + getResizeHandle(x, y) { + if (!this.selectedLayer) return null; + + const handleRadius = 5; + const handles = { + 'nw': {x: this.selectedLayer.x, y: this.selectedLayer.y}, + 'ne': {x: this.selectedLayer.x + this.selectedLayer.width, y: this.selectedLayer.y}, + 'se': {x: this.selectedLayer.x + this.selectedLayer.width, y: this.selectedLayer.y + this.selectedLayer.height}, + 'sw': {x: this.selectedLayer.x, y: this.selectedLayer.y + this.selectedLayer.height} + }; + + for (const [position, point] of Object.entries(handles)) { + if (Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)) <= handleRadius) { + return position; + } + } + return null; + } + + // 修改水平镜像方法 + mirrorHorizontal() { + if (!this.selectedLayer) return; + + // 创建临时画布 + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = this.selectedLayer.image.width; + tempCanvas.height = this.selectedLayer.image.height; + + // 水平翻转绘制 + tempCtx.translate(tempCanvas.width, 0); + tempCtx.scale(-1, 1); + tempCtx.drawImage(this.selectedLayer.image, 0, 0); + + // 创建新图像 + const newImage = new Image(); + newImage.onload = () => { + this.selectedLayer.image = newImage; + this.render(); + }; + newImage.src = tempCanvas.toDataURL(); + } + + // 修改垂直镜像方法 + mirrorVertical() { + if (!this.selectedLayer) return; + + // 创建临时画布 + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = this.selectedLayer.image.width; + tempCanvas.height = this.selectedLayer.image.height; + + // 垂直翻转绘制 + tempCtx.translate(0, tempCanvas.height); + tempCtx.scale(1, -1); + tempCtx.drawImage(this.selectedLayer.image, 0, 0); + + // 创建新图像 + const newImage = new Image(); + newImage.onload = () => { + this.selectedLayer.image = newImage; + this.render(); + }; + newImage.src = tempCanvas.toDataURL(); + } +} \ No newline at end of file diff --git a/js/Canvas_view.js b/js/Canvas_view.js new file mode 100644 index 0000000..73fba3e --- /dev/null +++ b/js/Canvas_view.js @@ -0,0 +1,434 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; +import { $el } from "../../scripts/ui.js"; +import { Canvas } from "./Canvas.js"; + +async function createCanvasWidget(node, widget, app) { + const canvas = new Canvas(node, widget); + + // 添加全局样式 + const style = document.createElement('style'); + style.textContent = ` + .painter-button { + background: linear-gradient(to bottom, #4a4a4a, #3a3a3a); + border: 1px solid #2a2a2a; + border-radius: 4px; + color: #ffffff; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; + min-width: 80px; + text-align: center; + margin: 2px; + text-shadow: 0 1px 1px rgba(0,0,0,0.2); + } + + .painter-button:hover { + background: linear-gradient(to bottom, #5a5a5a, #4a4a4a); + box-shadow: 0 1px 3px rgba(0,0,0,0.2); + } + + .painter-button:active { + background: linear-gradient(to bottom, #3a3a3a, #4a4a4a); + transform: translateY(1px); + } + + .painter-button.primary { + background: linear-gradient(to bottom, #4a6cd4, #3a5cc4); + border-color: #2a4cb4; + } + + .painter-button.primary:hover { + background: linear-gradient(to bottom, #5a7ce4, #4a6cd4); + } + + .painter-controls { + background: linear-gradient(to bottom, #404040, #383838); + border-bottom: 1px solid #2a2a2a; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + padding: 8px; + display: flex; + gap: 6px; + flex-wrap: wrap; + align-items: center; + } + + .painter-container { + background: #607080; /* 带蓝色的灰色背景 */ + border: 1px solid #4a5a6a; + border-radius: 6px; + box-shadow: inset 0 0 10px rgba(0,0,0,0.1); + } + + .painter-dialog { + background: #404040; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + padding: 20px; + color: #ffffff; + } + + .painter-dialog input { + background: #303030; + border: 1px solid #505050; + border-radius: 4px; + color: #ffffff; + padding: 4px 8px; + margin: 4px; + width: 80px; + } + + .painter-dialog button { + background: #505050; + border: 1px solid #606060; + border-radius: 4px; + color: #ffffff; + padding: 4px 12px; + margin: 4px; + cursor: pointer; + } + + .painter-dialog button:hover { + background: #606060; + } + `; + document.head.appendChild(style); + + // 修改控制面板,使其高度自适应 + const controlPanel = $el("div.painterControlPanel", {}, [ + $el("div.controls.painter-controls", { + style: { + position: "absolute", + top: "0", + left: "0", + right: "0", + minHeight: "50px", // 改为最小高度 + zIndex: "10", + background: "linear-gradient(to bottom, #404040, #383838)", + borderBottom: "1px solid #2a2a2a", + boxShadow: "0 2px 4px rgba(0,0,0,0.1)", + padding: "8px", + display: "flex", + gap: "6px", + flexWrap: "wrap", + alignItems: "center" + }, + // 添加监听器来动态调整画布容器的位置 + onresize: (entries) => { + const controlsHeight = entries[0].target.offsetHeight; + canvasContainer.style.top = (controlsHeight + 10) + "px"; + } + }, [ + $el("button.painter-button.primary", { + textContent: "Add Image", + onclick: () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.multiple = true; + input.onchange = async (e) => { + for (const file of e.target.files) { + // 创建图片对象 + const img = new Image(); + img.onload = async () => { + // 计算适当的缩放比例 + const scale = Math.min( + canvas.width / img.width * 0.8, + canvas.height / img.height * 0.8 + ); + + // 创建新图层 + const layer = { + image: img, + x: (canvas.width - img.width * scale) / 2, + y: (canvas.height - img.height * scale) / 2, + width: img.width * scale, + height: img.height * scale, + rotation: 0, + zIndex: canvas.layers.length + }; + + // 添加图层并选中 + canvas.layers.push(layer); + canvas.selectedLayer = layer; + + // 渲染画布 + canvas.render(); + + // 立即保存并触发输出更新 + await canvas.saveToServer(widget.value); + + // 触发节点更新 + app.graph.runStep(); + }; + img.src = URL.createObjectURL(file); + } + }; + input.click(); + } + }), + $el("button.painter-button", { + textContent: "Canvas Size", + onclick: () => { + const dialog = $el("div.painter-dialog", { + style: { + position: 'fixed', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%)', + zIndex: '1000' + } + }, [ + $el("div", { + style: { + color: "white", + marginBottom: "10px" + } + }, [ + $el("label", { + style: { + marginRight: "5px" + } + }, [ + $el("span", {}, ["Width: "]) + ]), + $el("input", { + type: "number", + id: "canvas-width", + value: canvas.width, + min: "1", + max: "4096" + }) + ]), + $el("div", { + style: { + color: "white", + marginBottom: "10px" + } + }, [ + $el("label", { + style: { + marginRight: "5px" + } + }, [ + $el("span", {}, ["Height: "]) + ]), + $el("input", { + type: "number", + id: "canvas-height", + value: canvas.height, + min: "1", + max: "4096" + }) + ]), + $el("div", { + style: { + textAlign: "right" + } + }, [ + $el("button", { + id: "cancel-size", + textContent: "Cancel" + }), + $el("button", { + id: "confirm-size", + textContent: "OK" + }) + ]) + ]); + document.body.appendChild(dialog); + + document.getElementById('confirm-size').onclick = () => { + const width = parseInt(document.getElementById('canvas-width').value) || canvas.width; + const height = parseInt(document.getElementById('canvas-height').value) || canvas.height; + canvas.updateCanvasSize(width, height); + document.body.removeChild(dialog); + }; + + document.getElementById('cancel-size').onclick = () => { + document.body.removeChild(dialog); + }; + } + }), + $el("button.painter-button", { + textContent: "Remove Layer", + onclick: () => { + const index = canvas.layers.indexOf(canvas.selectedLayer); + canvas.removeLayer(index); + } + }), + $el("button.painter-button", { + textContent: "Rotate +90°", + onclick: () => canvas.rotateLayer(90) + }), + $el("button.painter-button", { + textContent: "Scale +5%", + onclick: () => canvas.resizeLayer(1.05) + }), + $el("button.painter-button", { + textContent: "Scale -5%", + onclick: () => canvas.resizeLayer(0.95) + }), + $el("button.painter-button", { + textContent: "Layer Up", + onclick: async () => { + canvas.moveLayerUp(); + await canvas.saveToServer(widget.value); + app.graph.runStep(); + } + }), + $el("button.painter-button", { + textContent: "Layer Down", + onclick: async () => { + canvas.moveLayerDown(); + await canvas.saveToServer(widget.value); + app.graph.runStep(); + } + }), + // 添加水平镜像按钮 + $el("button.painter-button", { + textContent: "Mirror H", + onclick: () => { + canvas.mirrorHorizontal(); + } + }), + // 添加垂直镜像按钮 + $el("button.painter-button", { + textContent: "Mirror V", + onclick: () => { + canvas.mirrorVertical(); + } + }) + ]) + ]); + + // 创建ResizeObserver来监控控制面板的高度变化 + const resizeObserver = new ResizeObserver((entries) => { + const controlsHeight = entries[0].target.offsetHeight; + canvasContainer.style.top = (controlsHeight + 10) + "px"; + }); + + // 监控控制面板的大小变化 + resizeObserver.observe(controlPanel.querySelector('.controls')); + + // 获取触发器widget + const triggerWidget = node.widgets.find(w => w.name === "trigger"); + + // 创建更新函数 + const updateOutput = async () => { + // 保存画布 + await canvas.saveToServer(widget.value); + // 更新触发器值 + triggerWidget.value = (triggerWidget.value + 1) % 99999999; + // 触发节点更新 + app.graph.runStep(); + }; + + // 修改所有可能触发更新的操作 + const addUpdateToButton = (button) => { + const origClick = button.onclick; + button.onclick = async (...args) => { + await origClick?.(...args); + await updateOutput(); + }; + }; + + // 为所有按钮添加更新逻辑 + controlPanel.querySelectorAll('button').forEach(addUpdateToButton); + + // 修改画布容器样式,使用动态top值 + const canvasContainer = $el("div.painterCanvasContainer.painter-container", { + style: { + position: "absolute", + top: "60px", // 初始值 + left: "10px", + right: "10px", + bottom: "10px", + display: "flex", + justifyContent: "center", + alignItems: "center", + overflow: "hidden" + } + }, [canvas.canvas]); + + // 修改节点大小调整逻辑 + node.onResize = function() { + const minSize = 300; + const controlsElement = controlPanel.querySelector('.controls'); + const controlPanelHeight = controlsElement.offsetHeight; // 获取实际高度 + const padding = 20; + + // 保持节点宽度,高度根据画布比例调整 + const width = Math.max(this.size[0], minSize); + const height = Math.max( + width * (canvas.height / canvas.width) + controlPanelHeight + padding * 2, + minSize + controlPanelHeight + ); + + this.size[0] = width; + this.size[1] = height; + + // 计算画布的实际可用空间 + const availableWidth = width - padding * 2; + const availableHeight = height - controlPanelHeight - padding * 2; + + // 更新画布尺寸,保持比例 + const scale = Math.min( + availableWidth / canvas.width, + availableHeight / canvas.height + ); + + canvas.canvas.style.width = (canvas.width * scale) + "px"; + canvas.canvas.style.height = (canvas.height * scale) + "px"; + + // 强制重新渲染 + canvas.render(); + }; + + // 添加拖拽事件监听 + canvas.canvas.addEventListener('mouseup', updateOutput); + canvas.canvas.addEventListener('mouseleave', updateOutput); + + // 创建一个包含控制面板和画布的容器 + const mainContainer = $el("div.painterMainContainer", { + style: { + position: "relative", + width: "100%", + height: "100%" + } + }, [controlPanel, canvasContainer]); + + // 将主容器添加到节点 + const mainWidget = node.addDOMWidget("mainContainer", "widget", mainContainer); + + // 设置节点的默认大小 + node.size = [500, 500]; // 设置初始大小为正方形 + + // 在执行时保存画布 + api.addEventListener("execution_start", async () => { + await canvas.saveToServer(widget.value); + }); + + return { + canvas: canvas, + panel: controlPanel + }; +} + +app.registerExtension({ + name: "Comfy.CanvasView", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeType.comfyClass === "CanvasView") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = async function() { + const r = onNodeCreated?.apply(this, arguments); + + const widget = this.widgets.find(w => w.name === "canvas_image"); + await createCanvasWidget(this, widget, app); + + return r; + }; + } + } +}); \ No newline at end of file