自创工具分享——图片标注(软件学习构架图)

一,简单介绍:
这是一款有助于学习软件的截图标注工具,如果你是一位软件小白,而且因为软件的功能庞大且复杂,无法一一有效记住的话,可以借助此工具进行反复记忆,或需要时查看。

二,使用方法
1.把图片拖进工作区
2.在想进行标注的地方右键单击创建标注
3.写上文字,按shift+enter键换行
4.调整标注的位置时,按住空格键移动到适合的位置5.如果想修改标注上的文字可以选择标注单击右键删除标注
6.标注完成后,点击下载按钮可以把工作区存为png格式到本地7.开始新的一轮标注点按清楚工作区按钮。
三,得到方法
-
复制一下代码到txt文件 -
存储为.html格式的文件到本地 -
双击打开
html><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>专业图片标注工具 - 多图独立移动title><style>* {margin: 0;padding: 0;box-sizing: border-box;font-family: "Microsoft YaHei", sans-serif;user-select: none;}body {width: 100vw;height: 100vh;overflow: hidden;background: #f8fafc;color: #1e293b;}.toolbar {position: fixed;top: 0;left: 0;width: 100%;height: 50px;background: #ffffff;border-bottom: 1px solid #e2e8f0;display: flex;align-items: center;padding: 0 20px;gap: 12px;z-index: 100;box-shadow: 0 1px 4px rgba(0,0,0,0.05);}.tool-btn {padding: 8px 16px;border: none;border-radius: 6px;background: #b1caff;color: #1e293b;font-size: 14px;cursor: pointer;transition: all 0.2s;display: flex;align-items: center;gap: 6px;}.tool-btn:hover {background: #8fb4f2;}.tool-btn:active {transform: scale(0.96);}#imageSelector {padding: 6px 10px;border: 1px solid #e2e8f0;border-radius: 6px;background: #fff;font-size: 14px;outline: none;min-width: 180px;}.workspace {position: fixed;top: 50px;left: 0;width: 100%;height: calc(100vh - 50px);background: #f1f5f9;overflow: hidden;}#canvas {display: block;cursor: grab;}#canvas:active {cursor: grabbing;}.context-menu {position: absolute;width: 140px;background: #ffffff;border: 1px solid #e2e8f0;border-radius: 6px;box-shadow: 0 4px 12px rgba(0,0,0,0.1);z-index: 200;display: none;}.menu-item {padding: 10px 14px;font-size: 14px;cursor: pointer;transition: background 0.2s;color: #334155;}.menu-item:hover {background: #b1caff;color: #1e293b;}.annotation-input {position: absolute;min-width: 120px;min-height: 36px;padding: 6px 8px;border: 2px solid #b1caff;border-radius: 4px;background: rgba(255,255,255,0.95);color: #1e293b;font-size: 14px;outline: none;z-index: 150;display: none;overflow: hidden;white-space: pre-wrap;}.empty-tip {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);color: #94a3b8;font-size: 16px;text-align: center;pointer-events: none;}style>head><body><divclass="toolbar"><buttonclass="tool-btn"id="deleteBtn">🗑️ 删除标注button><buttonclass="tool-btn"id="undoBtn">↶ 撤销操作button><buttonclass="tool-btn"id="clearBtn">🧹 清除工作区button><buttonclass="tool-btn"id="saveBtn">💾 本地保存button><buttonclass="tool-btn"id="downloadBtn">📥 下载PNGbutton><selectid="imageSelector">select>div><divclass="workspace"><canvasid="canvas">canvas><divclass="empty-tip"id="emptyTip">拖拽图片到此处开始标注div>div><divclass="context-menu"id="contextMenu"><divclass="menu-item"id="createAnnot">创建标注div><divclass="menu-item"id="editAnnot">编辑文字div>div><textareaclass="annotation-input"id="annotInput">textarea><script>const canvas = document.getElementById('canvas');const ctx = canvas.getContext('2d');const emptyTip = document.getElementById('emptyTip');const contextMenu = document.getElementById('contextMenu');const annotInput = document.getElementById('annotInput');const imageSelector = document.getElementById('imageSelector');let canvasWidth = window.innerWidth;let canvasHeight = window.innerHeight - 50;br// 全局视口缩放 + 平移let viewScale = 1;let viewX = 0;let viewY = 0;brlet isDragging = false;let lastX, lastY;// 所有图片:每张独立位置 + 独立标注let allImages = [];let selectedImageIndex = -1;let selectedAnnot = null;let history = [];let isMovingText = false;let spacePressed = false;let editingAnnot = null;canvas.width = canvasWidth;canvas.height = canvasHeight;const deleteBtn = document.getElementById('deleteBtn');const undoBtn = document.getElementById('undoBtn');const clearBtn = document.getElementById('clearBtn');const saveBtn = document.getElementById('saveBtn');const downloadBtn = document.getElementById('downloadBtn');// 更新下拉框function updateImageSelector() {imageSelector.innerHTML = '';allImages.forEach((_, i) => {const opt = document.createElement('option');opt.value = i;opt.textContent = `图片 ${i+1}`;imageSelector.appendChild(opt);});if (selectedImageIndex >= 0) imageSelector.value = selectedImageIndex;else imageSelector.selectedIndex = -1;}// 切换选中图片imageSelector.addEventListener('change', () => {selectedImageIndex = +imageSelector.value;render();});function adjustInputSize() {annotInput.style.width = 'auto';annotInput.style.height = 'auto';annotInput.style.width = Math.max(120, annotInput.scrollWidth + 16) + 'px';annotInput.style.height = Math.max(36, annotInput.scrollHeight + 8) + 'px';}// 保存历史function saveHistory() {if (selectedImageIndex < 0) return;const ann = allImages[selectedImageIndex].annotations;history.push(JSON.parse(JSON.stringify(ann)));}// 渲染所有图片function render() {ctx.clearRect(0, 0, canvasWidth, canvasHeight);if (allImages.length === 0) { emptyTip.style.display = 'block'; return; }emptyTip.style.display = 'none';// 应用全局视口变换(缩放+平移整个界面)ctx.save();ctx.translate(viewX, viewY);ctx.scale(viewScale, viewScale);allImages.forEach((imgData, idx) => {ctx.save();// 图片自身位置ctx.translate(imgData.x, imgData.y);// 绘制图片const img = imgData.img;const w = img.width;const h = img.height;ctx.drawImage(img, -w/2, -h/2, w, h);// 选中框if (idx === selectedImageIndex) {ctx.strokeStyle = '#2563eb';ctx.lineWidth = 2 / viewScale;ctx.setLineDash([5, 3]);ctx.strokeRect(-w/2 - 10, -h/2 - 10, w + 20, h + 20);ctx.setLineDash([]);}// 绘制标注imgData.annotations.forEach(annot => {const sel = selectedAnnot === annot && idx === selectedImageIndex;drawAnnotation(annot, sel);});ctx.restore();});brctx.restore();}function drawAnnotation(annot, selected) {const { x, y, text, tx, ty } = annot;const lines = text.split('\n');const lineHeight = 22;const padding = 8;let maxW = 0;lines.forEach(l => maxW = Math.max(maxW, ctx.measureText(l).width));const boxW = maxW + padding*2;const boxH = lines.length * lineHeight + padding;const bx = tx ?? x + 20;const by = ty ?? y - boxH/2;if (selected) {ctx.strokeStyle = '#8fb4f2';ctx.lineWidth = 2;ctx.setLineDash([5,3]);ctx.strokeRect(bx-4, by-4, boxW+8, boxH+8);ctx.setLineDash([]);}ctx.beginPath();ctx.moveTo(x, y);ctx.lineTo(bx, by + boxH/2);ctx.strokeStyle = '#b1caff';ctx.lineWidth = 2;ctx.stroke();ctx.beginPath();ctx.arc(x, y, 4, 0, Math.PI*2);ctx.fillStyle = selected ? '#8fb4f2' : '#b1caff';ctx.fill();ctx.fillStyle = 'rgba(177,202,255,0.92)';ctx.fillRect(bx, by, boxW, boxH);ctx.fillStyle = '#f00';ctx.font = '14px Microsoft YaHei';ctx.textAlign = 'left';ctx.textBaseline = 'top';lines.forEach((l, i) => {ctx.fillText(l, bx + 4, by + 4 + i*lineHeight);});}// 点中图片检测(基于视口变换)function getImageAt(mx, my) {// 转换为世界坐标const wx = (mx - viewX) / viewScale;const wy = (my - viewY) / viewScale;brfor (let i = allImages.length-1; i >= 0; i--) {const d = allImages[i];const ix = d.x;const iy = d.y;const iw = d.img.width;const ih = d.img.height;if (wx >= ix - iw/2 && wx <= ix + iw/2 && wy >= iy - ih/2 && wy <= iy + ih/2) {return i;}}return -1;}// 鼠标按下canvas.addEventListener('mousedown', e => {if (allImages.length === 0) return;const mx = e.offsetX;const my = e.offsetY;// 选中图片 / 点击空白取消选中const hitImg = getImageAt(mx, my);if (hitImg >= 0) {selectedImageIndex = hitImg;} else {// 点击空白处:取消选中selectedImageIndex = -1;selectedAnnot = null;}updateImageSelector();// 选中标注selectedAnnot = null;if (selectedImageIndex >= 0) {const d = allImages[selectedImageIndex];// 世界坐标const wx = (mx - viewX) / viewScale;const wy = (my - viewY) / viewScale;brselectedAnnot = d.annotations.find(a => {const lines = a.text.split('\n');let maxW = 0;lines.forEach(l => maxW = Math.max(maxW, ctx.measureText(l).width));const bx = a.tx ?? a.x+20;const by = a.ty ?? a.y - (lines.length*22+8)/2;const inP = Math.hypot(wx - (a.x + d.x), wy - (a.y + d.y)) < 15;const inB = wx >= (bx + d.x) && wx <= (bx + d.x + maxW+16) && wy >= (by + d.y) && wy <= (by + d.y + lines.length*22+8);return inP || inB;});}isDragging = true;lastX = mx;lastY = my;if (spacePressed && selectedAnnot) isMovingText = true;render();});// 鼠标移动:只移动选中图片 | 无选中则平移画布window.addEventListener('mousemove', e => {if (!isDragging) return;const dx = e.offsetX - lastX;const dy = e.offsetY - lastY;lastX = e.offsetX;lastY = e.offsetY;// 有选中图片 → 只移动图片if (selectedImageIndex >= 0 && !isMovingText) {const img = allImages[selectedImageIndex];img.x += dx / viewScale;img.y += dy / viewScale;}// 无选中图片 → 平移整个画布else if (selectedImageIndex === -1 && !isMovingText) {viewX += dx;viewY += dy;}br// 按住空格移动标注if (isMovingText && selectedAnnot) {const wx = (e.offsetX - viewX) / viewScale;const wy = (e.offsetY - viewY) / viewScale;const img = allImages[selectedImageIndex];selectedAnnot.tx = wx - img.x;selectedAnnot.ty = wy - img.y;}brrender();});window.addEventListener('mouseup', () => {if (isMovingText && selectedAnnot) saveHistory();isDragging = false;isMovingText = false;});// 鼠标滚轮:缩放整个界面canvas.addEventListener('wheel', e => {e.preventDefault();const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;const newScale = viewScale * zoomFactor;if (newScale < 0.2 || newScale > 5) return;// 鼠标位置缩放(以鼠标为中心)const mx = e.offsetX;const my = e.offsetY;viewX = mx - (mx - viewX) * zoomFactor;viewY = my - (my - viewY) * zoomFactor;viewScale = newScale;render();});// 右键菜单canvas.addEventListener('contextmenu', e => {e.preventDefault();if (selectedImageIndex < 0) return;const d = allImages[selectedImageIndex];brconst wx = (e.offsetX - viewX) / viewScale;const wy = (e.offsetY - viewY) / viewScale;const rx = wx - d.x;const ry = wy - d.y;const ann = d.annotations.find(a => {const lines = a.text.split('\n');let maxW=0; lines.forEach(l=>maxW=Math.max(maxW,ctx.measureText(l).width));const bx=a.tx??a.x+20, by=a.ty??a.y-(lines.length*22+8)/2;return Math.hypot(rx-a.x,ry-a.y)<15 || (rx>=bx&&rx<=bx+maxW+16&&ry>=by&&ry<=by+lines.length*22+8);});contextMenu.style.left = e.clientX+'px';contextMenu.style.top = e.clientY+'px';contextMenu.style.display = 'block';createAnnot.style.display = ann ? 'none' : 'block';editAnnot.style.display = ann ? 'block' : 'none';createAnnot.onclick = () => {contextMenu.style.display='none';editingAnnot = null;annotInput.style.display='block';annotInput.style.left = e.clientX+20+'px';annotInput.style.top = e.clientY-10+'px';annotInput.value='';annotInput.focus();annotInput.onkeydown = ev => {if (ev.key === 'Enter' && !ev.shiftKey) {ev.preventDefault();const t = annotInput.value.trim();if (t) {saveHistory();d.annotations.push({x:rx, y:ry, text:t});}annotInput.style.display='none';render();}};};editAnnot.onclick = () => {contextMenu.style.display='none';if (!ann) return;editingAnnot = ann;annotInput.style.display='block';annotInput.style.left = e.clientX+'px';annotInput.style.top = e.clientY+'px';annotInput.value = ann.text;annotInput.focus();annotInput.onkeydown = ev => {if (ev.key === 'Enter' && !ev.shiftKey) {ev.preventDefault();const t = annotInput.value.trim();if (t) { saveHistory(); ann.text = t; }annotInput.style.display='none';render();}};};});window.addEventListener('click', () => contextMenu.style.display='none');// 撤销undoBtn.onclick = () => {if (history.length === 0 || selectedImageIndex < 0) { alert('无操作可撤销'); return; }allImages[selectedImageIndex].annotations = history.pop();render();};// 删除标注deleteBtn.onclick = () => {if (!selectedAnnot || selectedImageIndex < 0) { alert('请先选中标注'); return; }saveHistory();const d = allImages[selectedImageIndex];d.annotations = d.annotations.filter(a => a !== selectedAnnot);selectedAnnot = null;render();};// 清空clearBtn.onclick = () => {if (!confirm('清空所有?')) return;allImages = [];selectedImageIndex = -1;selectedAnnot = null;history = [];viewScale = 1;viewX = viewY = 0;updateImageSelector();render();};// 保存saveBtn.onclick = () => {const data = {images: allImages.map(i => ({src:i.img.src, x:i.x, y:i.y, annotations:i.annotations})),view: { scale: viewScale, x: viewX, y: viewY }};localStorage.setItem('multiImageData', JSON.stringify(data));alert('保存成功');};// 下载downloadBtn.onclick = () => {const a = document.createElement('a');a.download = '标注结果.png';a.href = canvas.toDataURL('image/png');a.click();};// 拖入图片window.addEventListener('drop', e => {e.preventDefault();const f = e.dataTransfer.files[0];if (!f || !f.type.startsWith('image/')) return;const r = new FileReader();r.onload = e => {const img = new Image();img.src = e.target.result;img.onload = () => {// 新图片放在中心const wx = (canvasWidth/2 - viewX) / viewScale;const wy = (canvasHeight/2 - viewY) / viewScale;brallImages.push({img:img,x: wx,y: wy,annotations:[]});selectedImageIndex = allImages.length-1;updateImageSelector();render();};};r.readAsDataURL(f);});window.addEventListener('dragover', e => e.preventDefault());window.addEventListener('resize', () => {canvasWidth = innerWidth;canvasHeight = innerHeight-50;canvas.width = canvasWidth;canvas.height = canvasHeight;render();});window.addEventListener('keydown', e => {if (e.keyCode === 32) { spacePressed = true; if (selectedAnnot) isMovingText = true; }});window.addEventListener('keyup', e => {if (e.keyCode === 32) { spacePressed = false; isMovingText = false; }});// 加载window.onload = () => {const s = localStorage.getItem('multiImageData');if (!s) return;try {const { images, view } = JSON.parse(s);let c = 0;images.forEach(it => {const img = new Image();img.src = it.src;img.onload = () => {allImages.push({img:img, x:it.x, y:it.y, annotations:it.annotations||[]});c++;if (c === images.length) {selectedImageIndex = 0;if (view) {viewScale = view.scale;viewX = view.x;viewY = view.y;}updateImageSelector();render();}};});} catch(e) {}};render();script>body>html>
夜雨聆风