Canvas图片起止点悬停红点实现及网格对齐技术问询
电路设计工具:起止点标识与网格对齐问题解决
需求与现存问题
- 核心需求:实现元件拖拽到Canvas后,鼠标悬停在元件的起止点(可连接位置)时显示红色小圆点,操作流程:
- 拖拽左侧栏的元件图片
- 将图片放置到Canvas内
- 鼠标悬停元件的起止点,显示红色小圆点
- 当前问题:元件放置到Canvas时无法对齐网格
修改后的完整代码
HTML 代码
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" /> <style> .components { display: flex; gap: 20px; padding: 20px; } #components_circuit_border { list-style: none; padding: 0; } #components_circuit_border li { margin-bottom: 30px; } .elementi_disegno { flex: 1; } button { margin: 0 10px 10px 0; padding: 8px 16px; cursor: pointer; } canvas { border: 1px solid #eee; } </style> <title>电路设计工具</title> </head> <body> <div class="container"> <div class="components"> <div id="components_circuit"> <h3 id="h3_componenti_circuit">元件库:</h3> <ul id="components_circuit_border"> <li><img id="component_circuit_resistor" src="https://www.elprocus.com/wp-content/uploads/Resistor-Symbol-768x288.png" height="50" draggable="true" data-type="resistor"></li> <li><img id="component_circuit_condensator" src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Capacitor_Symbol_alternative.svg/1280px-Capacitor_Symbol_alternative.svg.png" height="50" draggable="true" data-type="capacitor"></li> <li><img id="component_circuit_transistor" src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/25/NPN_transistor_symbol.svg/1200px-NPN_transistor_symbol.svg.png" height="50" draggable="true" data-type="transistor"></li> <li><img id="component_circuit_alimentator" src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Battery_symbol_2.svg/1200px-Battery_symbol_2.svg.png" height="50" draggable="true" data-type="battery"></li> </ul> </div> <div class="elementi_disegno"> <h1 id="h1_disegna">绘制你的电路!</h1> <button id="back-button">撤销 <span class="material-symbols-outlined">undo</span></button> <button id="foward-button">重做 <span class="material-symbols-outlined">redo</span></button> <button id="clear-button">清空所有 <span class="material-symbols-outlined">delete</span></button> <canvas id="canvas" class="dropzone"></canvas> </div> </div> </div> <script src="script.js"></script> </body> </html>
JavaScript 代码
const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const back_button = document.getElementById('back-button'); const clear_button = document.getElementById('clear-button'); const foward_button = document.getElementById('foward-button'); const draggableImages = document.querySelectorAll('img[draggable]'); // 画布与网格配置 const squareSize = 10; canvas.width = 1200; canvas.height = 800; canvas.style.backgroundColor = 'white'; canvas.style.borderRadius = '10px'; canvas.style.display = 'block'; // 元件起止点配置(相对元件左上角的坐标) const componentPins = { resistor: [{x: 0, y: 37}, {x: 75, y: 37}], // 电阻左右两端 capacitor: [{x: 0, y: 37}, {x: 75, y: 37}], // 电容左右两端 transistor: [{x: 25, y: 0}, {x: 25, y: 75}, {x: 75, y: 37}], // 晶体管三个引脚 battery: [{x: 37, y: 0}, {x: 37, y: 75}] // 电源正负两端 }; // 存储已放置的元件信息 const placedComponents = []; const orderStack = []; const deletedOrderStack = []; const deletedComponents = []; // 初始化按钮状态 clear_button.disabled = true; clear_button.style.cursor = 'not-allowed'; foward_button.disabled = true; foward_button.style.cursor = 'not-allowed'; back_button.disabled = true; back_button.style.cursor = 'not-allowed'; // 绘制网格 function drawSquares() { const numColumns = Math.floor(canvas.width / squareSize); const numRows = Math.floor(canvas.height / squareSize); ctx.fillStyle = "#FAF9F9"; ctx.strokeStyle = "#F4F1F0"; ctx.lineWidth = 1; for (let i = 0; i < numColumns; i++) { for (let j = 0; j < numRows; j++) { const x = i * squareSize; const y = j * squareSize; if (i % 10 === 0 && j % 10 === 0) { ctx.lineWidth = 2.6; ctx.strokeStyle = "#E6E0DE"; ctx.strokeRect(x, y, squareSize * 10, squareSize * 10); ctx.lineWidth = 1; ctx.strokeStyle = "#F4F1F0"; } else { ctx.strokeRect(x, y, squareSize, squareSize); } } } } // 重绘所有内容 function redrawCanvas() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawSquares(); placedComponents.forEach(comp => { ctx.drawImage(comp.img, comp.x, comp.y, comp.width, comp.height); }); } // 检测鼠标是否悬停在元件引脚上 function checkPinHover(mouseX, mouseY) { let hoveredPin = null; placedComponents.forEach(comp => { componentPins[comp.type].forEach(pin => { const absoluteX = comp.x + pin.x; const absoluteY = comp.y + pin.y; if (Math.abs(mouseX - absoluteX) <= 5 && Math.abs(mouseY - absoluteY) <= 5) { hoveredPin = {x: absoluteX, y: absoluteY}; } }); }); return hoveredPin; } // 拖拽事件处理 draggableImages.forEach((img, index) => { img.ondragstart = (ev) => { ev.dataTransfer.setData('text/plain', index.toString()); }; }); canvas.ondragover = (ev) => ev.preventDefault(); canvas.ondrop = (ev) => { const index = parseInt(ev.dataTransfer.getData('text/plain')); const img = draggableImages[index]; const componentType = img.dataset.type; // 计算鼠标在画布内的坐标 const canvasRect = canvas.getBoundingClientRect(); const mouseX = ev.clientX - canvasRect.left; const mouseY = ev.clientY - canvasRect.top; // 网格对齐:让元件左上角对齐网格线 const maxSize = 75; const aspectRatio = img.width / img.height; let width = maxSize; let height = maxSize / aspectRatio; if (height > maxSize) { height = maxSize; width = height * aspectRatio; } // 修正对齐逻辑:取整到最近的网格单元 const snappedX = Math.floor(mouseX / squareSize) * squareSize; const snappedY = Math.floor(mouseY / squareSize) * squareSize; // 存储元件信息 placedComponents.push({ img: img, x: snappedX, y: snappedY, width: width, height: height, type: componentType }); // 更新历史栈 orderStack.push('add'); deletedComponents.length = 0; deletedOrderStack.length = 0; // 更新按钮状态 back_button.disabled = false; back_button.style.cursor = 'pointer'; clear_button.disabled = false; clear_button.style.cursor = 'pointer'; foward_button.disabled = true; foward_button.style.cursor = 'not-allowed'; redrawCanvas(); return false; }; // 鼠标移动事件:绘制悬停的红色圆点 canvas.onmousemove = (ev) => { const canvasRect = canvas.getBoundingClientRect(); const mouseX = ev.clientX - canvasRect.left; const mouseY = ev.clientY - canvasRect.top; redrawCanvas(); const hoveredPin = checkPinHover(mouseX, mouseY); if (hoveredPin) { ctx.fillStyle = 'red'; ctx.beginPath(); ctx.arc(hoveredPin.x, hoveredPin.y, 5, 0, Math.PI * 2); ctx.fill(); } }; // 撤销按钮事件 back_button.onclick = () => { if (orderStack.length === 0) return; const lastAction = orderStack.pop(); if (lastAction === 'add') { deletedComponents.push(placedComponents.pop()); } deletedOrderStack.push(lastAction); redrawCanvas(); foward_button.disabled = false; foward_button.style.cursor = 'pointer'; if (orderStack.length === 0) { back_button.disabled = true; back_button.style.cursor = 'not-allowed'; } }; // 重做按钮事件 foward_button.onclick = () => { if (deletedOrderStack.length === 0) return; const lastDeletedAction = deletedOrderStack.pop(); if (lastDeletedAction === 'add') { placedComponents.push(deletedComponents.pop()); } orderStack.push(lastDeletedAction); redrawCanvas(); back_button.disabled = false; back_button.style.cursor = 'pointer'; if (deletedOrderStack.length === 0) { foward_button.disabled = true; foward_button.style.cursor = 'not-allowed'; } }; // 清空按钮事件 clear_button.onclick = () => { placedComponents.length = 0; orderStack.length = 0; deletedOrderStack.length = 0; deletedComponents.length = 0; redrawCanvas(); back_button.disabled = true; back_button.style.cursor = 'not-allowed'; clear_button.disabled = true; clear_button.style.cursor = 'not-allowed'; foward_button.disabled = true; foward_button.style.cursor = 'not-allowed'; }; // 初始化绘制网格 drawSquares();
关键修改说明
- 网格对齐修复:调整了元件放置时的坐标计算逻辑,将元件左上角对齐到网格线,确保元件完全贴合网格
- 起止点标识实现:为每种元件定义了固定的起止点(引脚)坐标,鼠标移动时实时检测悬停状态,在对应位置绘制红色圆点
- 元件数据管理:新增了
placedComponents数组存储所有已放置元件的位置、尺寸和类型信息,支持重绘、撤销和重做操作 - 交互优化:完善了按钮状态管理,添加了鼠标移动事件监听,实现动态的起止点高亮效果
内容的提问来源于stack exchange,提问作者Skerdi Velo
相关产品推荐
相关产品推荐

