乐于分享
好东西不私藏

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

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

一,简单介绍:

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

二,使用方法

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;br        let 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 < 0return;            const ann = allImages[selectedImageIndex].annotations;            history.push(JSON.parse(JSON.stringify(ann)));        }        // 渲染所有图片        function render() {            ctx.clearRect(00, 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([53]);                    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();            });br            ctx.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, 40Math.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;br            for (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 === 0return;            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;br                selectedAnnot = 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;            }br            render();        });        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 > 5return;            // 鼠标位置缩放(以鼠标为中心)            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 < 0return;            const d = allImages[selectedImageIndex];br            const 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.srcx:i.xy:i.yannotations: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;br                    allImages.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 = trueif (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.xy:it.yannotations: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>