实现一个HTML页面,上传图片后可以测量两条辅助线之间的距离,支持点击添加、拖动和右键删除辅助线

一、项目背景

实现一个HTML页面,上传图片后可以测量两条辅助线之间的距离,支持点击添加、拖动和右键删除辅助线_第1张图片

偶尔需要测量图片上元素的宽度高度和间距。因此实现一个交互式、可视化的测距工具。

开发一个简单易用的 HTML 页面,用户可以上传任意图片,在图片上通过点击添加辅助线,拖动调整辅助线位置,右键删除不需要的辅助线,同时自动计算并显示相邻辅助线间的距离,提升效率和准确度。

二、核心功能

  • 图片上传
    用户可以上传本地图片作为测距背景,图片会按用户指定的宽高展示,支持任意尺寸,不做限制。

  • 辅助线添加
    用户点击图片区域添加辅助线。默认点击添加水平线,按住 Shift 键点击添加垂直线。

  • 辅助线拖动
    添加的辅助线可以通过拖拽调整位置,拖动范围限制在图片容器内。

  • 辅助线删除
    右键点击辅助线即可删除,界面简洁,操作直观。

  • 自动测距显示
    自动计算相邻两条水平线和垂直线之间的像素距离,实时显示在辅助线中间位置,方便用户读取。

  • 界面交互优化
    为拖拽范围做了限制,辅助线外包裹了更大可拖动区域,拖拽更容易。拖拽时不会触发误新增线条。

三、技术栈与实现细节

1. 原生 HTML + CSS + JavaScript

  • 结构简单,方便快速构建页面布局
  • CSS 控制辅助线样式及距离标签样式
  • JavaScript 处理上传图片、辅助线添加、拖动、删除及距离计算逻辑

2. 使用 Interact.js 实现拖拽

Interact.js 是一个轻量且功能丰富的 JavaScript 库,专门用于实现拖拽(drag)、缩放(resize)、多点触控手势(gesture)等交互操作。

  • 兼容桌面和移动端,性能稳定
  • 通过简单 API 绑定元素拖拽行为,极大简化事件监听和状态管理
  • 支持拖拽范围限制,避免元素拖出边界
  • 拖拽过程中实时触发回调,实现界面动态刷新

在本项目中,Interact.js 负责辅助线的拖动功能:

  • 绑定拖动事件,动态修改辅助线的 CSS topleft 属性
  • 利用内置的 restrictRect 修饰器限制拖动范围在图片容器内部
  • 拖动过程中触发距离更新,实现测距数据的实时显示
  • 拖拽结束不会触发新增辅助线,解决用户体验中的误操作问题

3. 事件管理与交互设计

  • 鼠标点击事件添加辅助线
  • 拖拽时禁用新增线条,防止操作冲突
  • 右键菜单删除对应辅助线,并刷新测距结果
  • 输入框动态设置容器大小,图片自适应显示

四、总结

本项目通过简洁的界面和流畅的交互,帮助用户高效测量图片中辅助线之间的间距。结合开源的 Interact.js 库,拖拽操作稳定且易用,极大提升了功能的开发效率和用户体验。

DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8" />
    <title>辅助线间距标注工具(无宽高限制)title>
    <script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js">script>
    <style>
        body {
            font-family: sans-serif;
            margin: 20px;
        }

        #controls {
            margin-bottom: 10px;
        }

        label {
            margin-right: 10px;
        }

        input[type="number"] {
            width: 80px;
            padding: 4px;
            margin-right: 20px;
        }

        #container {
            position: relative;
            width: 1800px;
            height: 800px;
            border: 1px solid #ccc;
            background-size: contain;
            background-repeat: no-repeat;
            background-position: top left;
            user-select: none;
            overflow: hidden;
        }

        .line-wrapper {
            position: absolute;
            z-index: 10;
            cursor: pointer;
        }

        .line-wrapper.h {
            width: 100%;
            height: 14px;
            margin-top: -7px;
        }

        .line-wrapper.v {
            height: 100%;
            width: 14px;
            margin-left: -7px;
        }

        .line {
            position: absolute;
            background: #FF5722;
        }

        .line.h {
            height: 2px;
            width: 100%;
            top: 50%;
            left: 0;
            transform: translateY(-50%);
            pointer-events: none;
        }

        .line.v {
            width: 2px;
            height: 100%;
            left: 50%;
            top: 0;
            transform: translateX(-50%);
            pointer-events: none;
        }

        .distance-label {
            position: absolute;
            background: #FFEB3B;
            color: #000;
            font-size: 12px;
            padding: 2px 4px;
            border-radius: 3px;
            z-index: 5;
            user-select: none;
            pointer-events: none;
        }
    style>
head>

<body>
    <h3>辅助线间距标注工具h3>

    <div id="controls">
        <label for="widthInput">宽度 (px):label>
        <input type="number" id="widthInput" value="1800" />
        <label for="heightInput">高度 (px):label>
        <input type="number" id="heightInput" value="800" />
    div>

    <input type="file" id="upload" accept="image/*" />
    <p>点击图片添加辅助线(默认点击添加水平线,按住Shift键点击添加垂直线。线条可拖动。两条线条间自动显示间距数值。右键删除线条。p>
    <div id="container">div>

    <script>
        const container = document.getElementById('container');
        const upload = document.getElementById('upload');
        const widthInput = document.getElementById('widthInput');
        const heightInput = document.getElementById('heightInput');
        let lines = [];

        // 更新容器尺寸,没有限制
        function updateContainerSize() {
            const w = parseInt(widthInput.value, 10) || 1800;
            const h = parseInt(heightInput.value, 10) || 800;
            container.style.width = w + 'px';
            container.style.height = h + 'px';

            // 限制线条位置,避免超出容器范围
            lines.forEach(wrapper => {
                if (wrapper.classList.contains('h')) {
                    let top = parseFloat(wrapper.style.top);
                    if (top > h) wrapper.style.top = h + 'px';
                    if (top < 0) wrapper.style.top = '0px';
                } else {
                    let left = parseFloat(wrapper.style.left);
                    if (left > w) wrapper.style.left = w + 'px';
                    if (left < 0) wrapper.style.left = '0px';
                }
            });
            updateDistances();
        }

        widthInput.addEventListener('input', updateContainerSize);
        heightInput.addEventListener('input', updateContainerSize);

        // 上传图片设置背景
        upload.addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = function (event) {
                container.style.backgroundImage = `url(${event.target.result})`;
            };
            reader.readAsDataURL(file);
        });

        let pointerDownPos = null;
        let isDraggingWrapper = false;

        container.addEventListener('mousedown', e => {
            if (e.target.classList.contains('line-wrapper')) {
                isDraggingWrapper = true;
                return;
            }
            pointerDownPos = { x: e.clientX, y: e.clientY };
            isDraggingWrapper = false;
        });

        container.addEventListener('mouseup', e => {
            if (isDraggingWrapper) {
                isDraggingWrapper = false;
                pointerDownPos = null;
                return;
            }
            if (!pointerDownPos) return;
            const rect = container.getBoundingClientRect();
            const offsetX = e.clientX - rect.left;
            const offsetY = e.clientY - rect.top;
            if (e.shiftKey) {
                addLine('v', offsetX);
            } else {
                addLine('h', offsetY);
            }
            updateDistances();
            pointerDownPos = null;
        });

        function addLine(type, position) {
            const wrapper = document.createElement('div');
            wrapper.classList.add('line-wrapper', type);

            if (type === 'h') {
                wrapper.style.top = position + 'px';
                wrapper.style.left = '0';
            } else {
                wrapper.style.left = position + 'px';
                wrapper.style.top = '0';
            }

            const line = document.createElement('div');
            line.classList.add('line', type);

            wrapper.appendChild(line);
            container.appendChild(wrapper);
            lines.push(wrapper);

            interact(wrapper).draggable({
                modifiers: [
                    interact.modifiers.restrictRect({
                        restriction: 'parent',
                        endOnly: true
                    })
                ],
                listeners: {
                    move(event) {
                        const target = event.target;
                        if (type === 'h') {
                            let newY = parseFloat(target.style.top) + event.dy;
                            if (newY < 0) newY = 0;
                            if (newY > container.clientHeight) newY = container.clientHeight;
                            target.style.top = newY + 'px';
                        } else {
                            let newX = parseFloat(target.style.left) + event.dx;
                            if (newX < 0) newX = 0;
                            if (newX > container.clientWidth) newX = container.clientWidth;
                            target.style.left = newX + 'px';
                        }
                        updateDistances();
                    }
                }
            });

            wrapper.addEventListener('contextmenu', e => {
                e.preventDefault();
                container.removeChild(wrapper);
                lines = lines.filter(l => l !== wrapper);
                updateDistances();
            });
        }

        function updateDistances() {
            document.querySelectorAll('.distance-label').forEach(el => el.remove());

            const horizontal = lines.filter(l => l.classList.contains('h'))
                .map(l => parseFloat(l.style.top))
                .sort((a, b) => a - b);

            const vertical = lines.filter(l => l.classList.contains('v'))
                .map(l => parseFloat(l.style.left))
                .sort((a, b) => a - b);

            for (let i = 0; i < horizontal.length - 1; i++) {
                const y1 = horizontal[i];
                const y2 = horizontal[i + 1];
                const label = document.createElement('div');
                label.className = 'distance-label';
                label.style.top = (y1 + (y2 - y1) / 2 - 10) + 'px';
                label.style.left = '5px';
                label.textContent = Math.abs(y2 - y1).toFixed(0) + ' px';
                container.appendChild(label);
            }

            for (let i = 0; i < vertical.length - 1; i++) {
                const x1 = vertical[i];
                const x2 = vertical[i + 1];
                const label = document.createElement('div');
                label.className = 'distance-label';
                label.style.left = (x1 + (x2 - x1) / 2 - 15) + 'px';
                label.style.top = '5px';
                label.textContent = Math.abs(x2 - x1).toFixed(0) + ' px';
                container.appendChild(label);
            }
        }
    script>
body>

html>

你可能感兴趣的:(实现一个HTML页面,上传图片后可以测量两条辅助线之间的距离,支持点击添加、拖动和右键删除辅助线)