偶尔需要测量图片上元素的宽度高度和间距。因此实现一个交互式、可视化的测距工具。
开发一个简单易用的 HTML 页面,用户可以上传任意图片,在图片上通过点击添加辅助线,拖动调整辅助线位置,右键删除不需要的辅助线,同时自动计算并显示相邻辅助线间的距离,提升效率和准确度。
图片上传
用户可以上传本地图片作为测距背景,图片会按用户指定的宽高展示,支持任意尺寸,不做限制。
辅助线添加
用户点击图片区域添加辅助线。默认点击添加水平线,按住 Shift 键点击添加垂直线。
辅助线拖动
添加的辅助线可以通过拖拽调整位置,拖动范围限制在图片容器内。
辅助线删除
右键点击辅助线即可删除,界面简洁,操作直观。
自动测距显示
自动计算相邻两条水平线和垂直线之间的像素距离,实时显示在辅助线中间位置,方便用户读取。
界面交互优化
为拖拽范围做了限制,辅助线外包裹了更大可拖动区域,拖拽更容易。拖拽时不会触发误新增线条。
Interact.js 是一个轻量且功能丰富的 JavaScript 库,专门用于实现拖拽(drag)、缩放(resize)、多点触控手势(gesture)等交互操作。
在本项目中,Interact.js 负责辅助线的拖动功能:
top
或 left
属性restrictRect
修饰器限制拖动范围在图片容器内部本项目通过简洁的界面和流畅的交互,帮助用户高效测量图片中辅助线之间的间距。结合开源的 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>