You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Canvas图片起止点悬停红点实现及网格对齐技术问询

电路设计工具:起止点标识与网格对齐问题解决

需求与现存问题

  • 核心需求:实现元件拖拽到Canvas后,鼠标悬停在元件的起止点(可连接位置)时显示红色小圆点,操作流程:
    1. 拖拽左侧栏的元件图片
    2. 将图片放置到Canvas内
    3. 鼠标悬停元件的起止点,显示红色小圆点
  • 当前问题:元件放置到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();

关键修改说明

  1. 网格对齐修复:调整了元件放置时的坐标计算逻辑,将元件左上角对齐到网格线,确保元件完全贴合网格
  2. 起止点标识实现:为每种元件定义了固定的起止点(引脚)坐标,鼠标移动时实时检测悬停状态,在对应位置绘制红色圆点
  3. 元件数据管理:新增了placedComponents数组存储所有已放置元件的位置、尺寸和类型信息,支持重绘、撤销和重做操作
  4. 交互优化:完善了按钮状态管理,添加了鼠标移动事件监听,实现动态的起止点高亮效果

内容的提问来源于stack exchange,提问作者Skerdi Velo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 01:10:07