Add files via upload

This commit is contained in:
tanglup
2024-11-19 15:40:36 +00:00
committed by GitHub
parent 413bc4985c
commit 87ab68d409
2 changed files with 1299 additions and 0 deletions

865
js/Canvas.js Normal file
View File

@@ -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();
}
}