通过对话,deepseek能辅助完成一个绘图H5页面。如下:
完成之后的效果如下:
如上
咨询步骤
- 给选中的图形加轮廓线
选中多线段后出现虚线轮廓
移动时,轮廓线没有移动
- 要求轮廓然随拖动移动
- 增加右键菜单,用来删除选中对象
添加右键菜单
完整代码
SVG Drawing Toolbox
SVG Drawing Toolbox
<script>
const svgContainer = document.getElementById("svg-container");
const codeViewer = document.getElementById("code-viewer");
const contextMenu = document.getElementById("context-menu");
let currentTool = null;
let startX, startY;
let currentElement = null; // 当前正在绘制的元素
let selectedElements = []; // 存储选中的图形
let isDragging = false;
let dragStartX, dragStartY;
// 在全局变量中添加虚线预览元素
let previewLine = null;
// 线段绘制相关状态
let isDrawingLine = false; // 是否正在绘制线段
let initialPoint = null; // 初始点
let lastPoint = null; // 上一个起始点
let currentLineGroup = null; // 当前线段的组
// Undo/Redo 相关状态
let historyStack = []; // 操作历史栈
let redoStack = []; // 重做栈
// 工具箱事件监听
document.getElementById("select-btn").addEventListener("click", () => {
currentTool = "select";
resetLineDrawing();
});
document.getElementById("rect-btn").addEventListener("click", () => {
currentTool = "rect";
resetLineDrawing();
});
document.getElementById("circle-btn").addEventListener("click", () => {
currentTool = "circle";
resetLineDrawing();
});
document.getElementById("line-btn").addEventListener("click", () => {
currentTool = "line";
resetLineDrawing();
});
document.getElementById("text-btn").addEventListener("click", () => {
currentTool = "text";
resetLineDrawing();
});
document.getElementById("undo-btn").addEventListener("click", () => {
undo();
});
document.getElementById("redo-btn").addEventListener("click", () => {
redo();
});
// 自定义上下文菜单事件监听
document.getElementById("end-line").addEventListener("click", () => {
resetLineDrawing();
hideContextMenu();
});
document.getElementById("close-loop").addEventListener("click", () => {
if (isDrawingLine && initialPoint && lastPoint) {
drawLineSegment(lastPoint.x, lastPoint.y, initialPoint.x, initialPoint.y);
resetLineDrawing();
}
hideContextMenu();
});
document.getElementById("select-drag").addEventListener("click", () => {
currentTool = "select";
hideContextMenu();
});
document.getElementById("delete-element").addEventListener("click", () => {
// 删除选中的元素
selectedElements.forEach(element => {
element.remove();
removeSelectionBorder(element);
});
selectedElements = [];
saveState(); // 保存状态
updateCodeViewer();
hideContextMenu();
});
function releasePreview(){
if(previewLine){
previewLine.remove();
previewLine = null;
}
}
// 显示自定义上下文菜单
function showContextMenu(x, y) {
contextMenu.style.display = "block";
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
// 显示或隐藏删除按钮
const deleteButton = document.getElementById("delete-element");
if (selectedElements.length > 0) {
deleteButton.style.display = "block";
} else {
deleteButton.style.display = "none";
}
}
// 隐藏自定义上下文菜单
function hideContextMenu() {
contextMenu.style.display = "none";
}
// 鼠标事件监听
svgContainer.addEventListener("mousedown", (event) => {
const x = event.offsetX;
const y = event.offsetY;
if (event.button === 2) {
// 右键点击,显示自定义上下文菜单
event.preventDefault();
showContextMenu(event.clientX, event.clientY);
return;
}
if (currentTool === "select") {
// 选择工具:选中图形
console.log(event.target);
if (event.target.tagName === "rect" || event.target.tagName === "circle" || event.target.tagName === "text" || event.target.tagName === "line" || event.target.tagName === "g") {
var element = event.target;
if ((event.target.tagName === "line") && (event.target.parentNode && event.target.parentNode.tagName === "g")) {
element = event.target.parentNode;
}
console.log(element);
// 按住 Shift 键多选
if (!event.shiftKey) {
selectedElements.forEach(el => {
el.classList.remove("selected");
removeSelectionBorder(el); // 移除选中边框
});
selectedElements = [];
}
if (!selectedElements.includes(element)) {
selectedElements.push(element);
element.classList.add("selected");
addSelectionBorder(element); // 添加选中边框
}
// 开始拖动
isDragging = true;
dragStartX = event.offsetX;
dragStartY = event.offsetY;
} else {
// 点击空白区域取消选中
selectedElements.forEach(el => {
el.classList.remove("selected");
removeSelectionBorder(el); // 移除选中边框
});
selectedElements = [];
}
} else if (currentTool === "text") {
// 文字工具
const text = prompt("Enter text:");
if (text) {
const textElement = document.createElementNS("http://www.w3.org/2000/svg", "text");
textElement.setAttribute("x", x);
textElement.setAttribute("y", y);
textElement.setAttribute("fill", document.getElementById("stroke-color").value);
textElement.setAttribute("font-size", "20");
textElement.textContent = text;
svgContainer.appendChild(textElement);
saveState(); // 保存状态
updateCodeViewer();
}
} else if (currentTool === "line") {
if (event.detail === 2) {
// 双击清空初始点
resetLineDrawing();
} else if (event.button === 0) {
// 左键点击
if (!isDrawingLine) {
// 第一次点击,设置初始点和起始点
initialPoint = { x, y };
lastPoint = { x, y };
isDrawingLine = true;
// 创建新的线段组
currentLineGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
svgContainer.appendChild(currentLineGroup);
} else {
// 后续点击,绘制线段并更新起始点
drawLineSegment(lastPoint.x, lastPoint.y, x, y);
lastPoint = { x, y }; // 更新起始点
}
}
} else if (currentTool === "rect" || currentTool === "circle") {
// 矩形或圆形工具
startX = x;
startY = y;
switch (currentTool) {
case "rect":
currentElement = document.createElementNS("http://www.w3.org/2000/svg", "rect");
currentElement.setAttribute("x", startX);
currentElement.setAttribute("y", startY);
break;
case "circle":
currentElement = document.createElementNS("http://www.w3.org/2000/svg", "circle");
currentElement.setAttribute("cx", startX);
currentElement.setAttribute("cy", startY);
break;
}
if (currentElement) {
currentElement.setAttribute("stroke", document.getElementById("stroke-color").value);
currentElement.setAttribute("stroke-width", document.getElementById("stroke-width").value);
currentElement.setAttribute("fill", document.getElementById("fill-color").value);
svgContainer.appendChild(currentElement);
}
}
});
svgContainer.addEventListener("mousemove", (event) => {
if (currentTool === "select" && isDragging) {
// 选择工具:拖动图形
const dx = event.offsetX - dragStartX;
const dy = event.offsetY - dragStartY;
selectedElements.forEach(element => {
if (element.tagName === "rect") {
const x = parseFloat(element.getAttribute("x")) + dx;
const y = parseFloat(element.getAttribute("y")) + dy;
element.setAttribute("x", x);
element.setAttribute("y", y);
} else if (element.tagName === "circle") {
const cx = parseFloat(element.getAttribute("cx")) + dx;
const cy = parseFloat(element.getAttribute("cy")) + dy;
element.setAttribute("cx", cx);
element.setAttribute("cy", cy);
} else if (element.tagName === "text") {
const x = parseFloat(element.getAttribute("x")) + dx;
const y = parseFloat(element.getAttribute("y")) + dy;
element.setAttribute("x", x);
element.setAttribute("y", y);
} else if (element.tagName === "line") {
const x1 = parseFloat(element.getAttribute("x1")) + dx;
const y1 = parseFloat(element.getAttribute("y1")) + dy;
const x2 = parseFloat(element.getAttribute("x2")) + dx;
const y2 = parseFloat(element.getAttribute("y2")) + dy;
element.setAttribute("x1", x1);
element.setAttribute("y1", y1);
element.setAttribute("x2", x2);
element.setAttribute("y2", y2);
} else if (element.tagName === "g") {
// 移动整个线段组
Array.from(element.children).forEach(line => {
const x1 = parseFloat(line.getAttribute("x1")) + dx;
const y1 = parseFloat(line.getAttribute("y1")) + dy;
const x2 = parseFloat(line.getAttribute("x2")) + dx;
const y2 = parseFloat(line.getAttribute("y2")) + dy;
line.setAttribute("x1", x1);
line.setAttribute("y1", y1);
line.setAttribute("x2", x2);
line.setAttribute("y2", y2);
});
}
updateSelectionBorder(element); // 更新选中边框位置
});
dragStartX = event.offsetX;
dragStartY = event.offsetY;
updateCodeViewer();
} else if ((currentTool === "rect" || currentTool === "circle") && currentElement) {
// 矩形或圆形工具:调整图形大小
const currentX = event.offsetX;
const currentY = event.offsetY;
switch (currentTool) {
case "rect":
currentElement.setAttribute("width", Math.abs(currentX - startX));
currentElement.setAttribute("height", Math.abs(currentY - startY));
break;
case "circle":
const radius = Math.sqrt(Math.pow(currentX - startX, 2) + Math.pow(currentY - startY, 2));
currentElement.setAttribute("r", radius);
break;
}
updateCodeViewer();
}else if (currentTool === "line" && isDrawingLine && lastPoint) {
// 线段工具:绘制虚线预览
if (!previewLine) {
previewLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
previewLine.setAttribute("stroke", "#000000"); // 虚线颜色
previewLine.setAttribute("stroke-width", document.getElementById("stroke-width").value);
previewLine.setAttribute("stroke-dasharray", "5,5"); // 虚线样式
previewLine.setAttribute("fill", "none");
svgContainer.appendChild(previewLine);
}
const x = event.offsetX;
const y = event.offsetY;
console.log(lastPoint,x,y)
previewLine.setAttribute("x1", lastPoint.x);
previewLine.setAttribute("y1", lastPoint.y);
previewLine.setAttribute("x2", x);
previewLine.setAttribute("y2", y);
}
});
svgContainer.addEventListener("mouseup", () => {
if (currentTool === "select") {
isDragging = false;
} else if (currentTool === "rect" || currentTool === "circle") {
currentElement = null;
saveState(); // 保存状态
}else if (currentTool === "line") {
// 移除虚线预览
releasePreview();
}
});
// 绘制线段
function drawLineSegment(x1, y1, x2, y2) {
if (!currentLineGroup) {
// 如果 currentLineGroup 未初始化,则创建一个新的组
currentLineGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
svgContainer.appendChild(currentLineGroup);
}
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", x1);
line.setAttribute("y1", y1);
line.setAttribute("x2", x2);
line.setAttribute("y2", y2);
line.setAttribute("stroke", document.getElementById("stroke-color").value);
line.setAttribute("stroke-width", document.getElementById("stroke-width").value);
currentLineGroup.appendChild(line);
saveState(); // 保存状态
updateCodeViewer();
}
// 重置线段绘制状态
function resetLineDrawing() {
isDrawingLine = false;
initialPoint = null;
lastPoint = null;
currentLineGroup = null;
// 移除虚线预览
releasePreview();
}
// 更新代码查看器
function updateCodeViewer() {
const serializer = new XMLSerializer();
const svgCode = serializer.serializeToString(svgContainer);
codeViewer.value = svgCode;
}
// 保存当前状态到历史栈
function saveState() {
const serializer = new XMLSerializer();
const svgCode = serializer.serializeToString(svgContainer);
historyStack.push(svgCode);
redoStack = []; // 清空重做栈
}
// 撤销操作
function undo() {
if (historyStack.length > 1) {
redoStack.push(historyStack.pop()); // 将当前状态移到重做栈
const previousState = historyStack[historyStack.length - 1];
svgContainer.innerHTML = previousState; // 恢复到上一个状态
updateCodeViewer();
}
}
// 重做操作
function redo() {
if (redoStack.length > 0) {
const nextState = redoStack.pop(); // 从重做栈中取出下一个状态
historyStack.push(nextState); // 将状态移回历史栈
svgContainer.innerHTML = nextState; // 恢复到下一个状态
updateCodeViewer();
}
}
// 添加选中边框
function addSelectionBorder(element) {
const bbox = element.getBBox();
const padding = 5; // 边框比图形大 2px
const border = document.createElementNS("http://www.w3.org/2000/svg", "rect");
border.setAttribute("x", bbox.x - padding);
border.setAttribute("y", bbox.y - padding);
border.setAttribute("width", bbox.width + 2 * padding);
border.setAttribute("height", bbox.height + 2 * padding);
border.setAttribute("stroke", "#FF0000"); // 红色虚线
border.setAttribute("stroke-width", "2");
border.setAttribute("stroke-dasharray", "5,5");
border.setAttribute("fill", "none");
border.classList.add("selection-border");
svgContainer.appendChild(border);
element.border = border; // 将边框引用存储在元素上
}
// 移除选中边框
function removeSelectionBorder(element) {
if (element && element.border) {
element.border.remove();
delete element.border;
}
}
// 更新选中边框位置
function updateSelectionBorder(element) {
if (element && element.border) {
const bbox = element.getBBox();
const padding = 2; // 边框比图形大 2px
element.border.setAttribute("x", bbox.x - padding);
element.border.setAttribute("y", bbox.y - padding);
element.border.setAttribute("width", bbox.width + 2 * padding);
element.border.setAttribute("height", bbox.height + 2 * padding);
}
}
// 点击页面其他区域隐藏上下文菜单
document.addEventListener("click", () => {
hideContextMenu();
});
// 阻止浏览器默认右键菜单
document.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
</script>
参考资料
上述页面的完整代码在gitee上,路径如下:
https://gitee.com/wapuboy/learning-programming-with-gauss/blob/master/code/javascript/src/svg.html