From dc1b4989b4cd83d06b7e2032e709fb88e0cb3261 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 27 May 2026 14:33:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/css/style.css | 17 ++--- public/index.html | 8 +-- public/js/app.js | 167 +++++++++++++++++++++++++++++-------------- 3 files changed, 125 insertions(+), 67 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 5b66eb9..93fc75a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -217,7 +217,7 @@ body { } #overlay-canvas.can-drag { - cursor: ns-resize; + cursor: move; } .footer { @@ -235,23 +235,24 @@ body { .legend { display: inline-block; - width: 24px; - height: 3px; + width: 0; + height: 0; vertical-align: middle; - margin-left: 4px; - border-radius: 1px; + margin-left: 6px; + border-left: 6px solid transparent; + border-right: 6px solid transparent; } .legend.entry { - background: var(--entry); + border-bottom: 10px solid var(--entry); } .legend.exit { - background: var(--exit); + border-top: 10px solid var(--exit); } .legend.stop { - background: var(--stop); + border-top: 10px solid var(--stop); } .footer-note { diff --git a/public/index.html b/public/index.html index 6f029e0..6b4287c 100644 --- a/public/index.html +++ b/public/index.html @@ -15,9 +15,9 @@
- - - + + +
@@ -44,7 +44,7 @@ 入场 出场 止损 - 单击添加水平线 · 拖拽调整位置 + 单击添加箭头 · 拖拽调整位置 · 可连续标注多个点位 diff --git a/public/js/app.js b/public/js/app.js index 042fc34..04fdd22 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,14 +1,17 @@ (function () { "use strict"; - const LINE_TYPES = { - entry: { label: "入场线", color: "#00ff00" }, - exit: { label: "出场线", color: "#0099ff" }, - stop: { label: "止损线", color: "#ff3333" }, + const MARKER_TYPES = { + entry: { label: "入场", color: "#00ff00", direction: "up" }, + exit: { label: "出场", color: "#0099ff", direction: "down" }, + stop: { label: "止损", color: "#ff3333", direction: "down" }, }; - const LINE_WIDTH = 3; - const HIT_TOLERANCE = 8; + const ARROW_HEAD = 12; + const ARROW_STEM = 14; + const ARROW_WIDTH = 9; + const LINE_WIDTH = 2; + const HIT_RADIUS = 18; const fileInput = document.getElementById("file-input"); const dropZone = document.getElementById("drop-zone"); @@ -24,7 +27,7 @@ const modeButtons = document.querySelectorAll(".mode-btn"); let currentMode = "entry"; - let lines = []; + let markers = []; let displayWidth = 0; let displayHeight = 0; let imageLoaded = false; @@ -38,9 +41,9 @@ function updateButtons() { const hasImage = imageLoaded; - const hasLines = lines.length > 0; - btnUndo.disabled = !hasImage || !hasLines; - btnClear.disabled = !hasImage || !hasLines; + const hasMarkers = markers.length > 0; + btnUndo.disabled = !hasImage || !hasMarkers; + btnClear.disabled = !hasImage || !hasMarkers; btnDownload.disabled = !hasImage; } @@ -54,25 +57,67 @@ }; } - function findLineAtY(y) { - for (let i = lines.length - 1; i >= 0; i--) { - if (Math.abs(lines[i].y - y) <= HIT_TOLERANCE) { + function getMarkerDirection(type) { + return MARKER_TYPES[type].direction; + } + + function dist(x1, y1, x2, y2) { + const dx = x1 - x2; + const dy = y1 - y2; + return Math.sqrt(dx * dx + dy * dy); + } + + function findMarkerAt(x, y) { + for (let i = markers.length - 1; i >= 0; i--) { + if (dist(x, y, markers[i].x, markers[i].y) <= HIT_RADIUS) { return i; } } return -1; } + function drawArrowMarker(targetCtx, x, y, type, color) { + const direction = getMarkerDirection(type); + const w = ARROW_WIDTH; + const h = ARROW_HEAD; + const stem = ARROW_STEM; + + targetCtx.fillStyle = color; + targetCtx.strokeStyle = color; + targetCtx.lineWidth = LINE_WIDTH; + targetCtx.lineCap = "round"; + targetCtx.lineJoin = "round"; + + if (direction === "up") { + targetCtx.beginPath(); + targetCtx.moveTo(x, y); + targetCtx.lineTo(x - w, y + h); + targetCtx.lineTo(x + w, y + h); + targetCtx.closePath(); + targetCtx.fill(); + targetCtx.beginPath(); + targetCtx.moveTo(x, y + h); + targetCtx.lineTo(x, y + h + stem); + targetCtx.stroke(); + } else { + targetCtx.beginPath(); + targetCtx.moveTo(x, y); + targetCtx.lineTo(x - w, y - h); + targetCtx.lineTo(x + w, y - h); + targetCtx.closePath(); + targetCtx.fill(); + targetCtx.beginPath(); + targetCtx.moveTo(x, y - h); + targetCtx.lineTo(x, y - h - stem); + targetCtx.stroke(); + } + } + function redraw() { if (!imageLoaded) return; ctx.clearRect(0, 0, canvas.width, canvas.height); - for (const line of lines) { - ctx.beginPath(); - ctx.strokeStyle = line.color; - ctx.lineWidth = LINE_WIDTH; - ctx.moveTo(0, line.y); - ctx.lineTo(canvas.width, line.y); - ctx.stroke(); + for (const m of markers) { + drawArrowMarker(ctx, m.x, m.y, m.type, m.color); } } @@ -87,9 +132,10 @@ } function clearAnnotations() { - lines = []; + markers = []; dragIndex = -1; isDragging = false; + didDragMove = false; redraw(); updateButtons(); } @@ -116,8 +162,8 @@ syncCanvasSize(); setHint( "当前模式:" + - LINE_TYPES[currentMode].label + - " — 在图上单击添加水平线,可拖拽调整" + MARKER_TYPES[currentMode].label + + " — 在图上单击添加箭头标记,可拖拽调整" ); updateButtons(); }); @@ -135,19 +181,18 @@ reader.readAsDataURL(file); } - function addLine(y) { + function addMarker(x, y) { const type = currentMode; - const info = LINE_TYPES[type]; - lines.push({ y: y, type: type, color: info.color }); + const info = MARKER_TYPES[type]; + markers.push({ x: x, y: y, type: type, color: info.color }); redraw(); updateButtons(); setHint( "已添加 " + info.label + - "(共 " + - lines.length + - " 条)— 当前模式:" + - info.label + " 标记(共 " + + markers.length + + " 个)— 可切换模式继续标注" ); } @@ -156,6 +201,7 @@ const natW = chartImage.naturalWidth; const natH = chartImage.naturalHeight; + const scaleX = natW / displayWidth; const scaleY = natH / displayHeight; const exportCanvas = document.createElement("canvas"); @@ -165,14 +211,14 @@ exCtx.drawImage(chartImage, 0, 0, natW, natH); - for (const line of lines) { - const y = line.y * scaleY; - exCtx.beginPath(); - exCtx.strokeStyle = line.color; - exCtx.lineWidth = LINE_WIDTH; - exCtx.moveTo(0, y); - exCtx.lineTo(natW, y); - exCtx.stroke(); + for (const m of markers) { + drawArrowMarker( + exCtx, + m.x * scaleX, + m.y * scaleY, + m.type, + m.color + ); } exportCanvas.toBlob(function (blob) { @@ -204,8 +250,8 @@ }, "image/png"); } - function updateCursor(y) { - const idx = findLineAtY(y); + function updateCursor(x, y) { + const idx = findMarkerAt(x, y); if (idx >= 0) { canvas.classList.add("can-drag"); } else { @@ -214,14 +260,20 @@ } modeButtons.forEach(function (btn) { - btn.addEventListener("click", function () { + btn.addEventListener("click", function (e) { + e.stopPropagation(); modeButtons.forEach(function (b) { b.classList.remove("active"); }); btn.classList.add("active"); currentMode = btn.dataset.mode; + didDragMove = false; if (imageLoaded) { - setHint("当前模式:" + LINE_TYPES[currentMode].label + " — 单击添加水平线"); + setHint( + "当前模式:" + + MARKER_TYPES[currentMode].label + + " — 单击添加箭头,可连续标注多个点位" + ); } }); }); @@ -254,7 +306,7 @@ if (!imageLoaded) return; e.preventDefault(); const pt = getCanvasPoint(e); - const idx = findLineAtY(pt.y); + const idx = findMarkerAt(pt.x, pt.y); if (idx >= 0) { dragIndex = idx; isDragging = true; @@ -269,12 +321,13 @@ if (isDragging && dragIndex >= 0) { didDragMove = true; - lines[dragIndex].y = Math.max(0, Math.min(canvas.height, pt.y)); + markers[dragIndex].x = Math.max(0, Math.min(canvas.width, pt.x)); + markers[dragIndex].y = Math.max(0, Math.min(canvas.height, pt.y)); redraw(); return; } - updateCursor(pt.y); + updateCursor(pt.x, pt.y); }); canvas.addEventListener("mouseup", function () { @@ -289,27 +342,31 @@ }); canvas.addEventListener("click", function (e) { - if (!imageLoaded || didDragMove) return; + if (!imageLoaded) return; + if (didDragMove) { + didDragMove = false; + return; + } const pt = getCanvasPoint(e); - if (findLineAtY(pt.y) >= 0) return; - addLine(pt.y); + if (findMarkerAt(pt.x, pt.y) >= 0) return; + addMarker(pt.x, pt.y); }); btnUndo.addEventListener("click", function () { - if (lines.length === 0) return; - lines.pop(); + if (markers.length === 0) return; + markers.pop(); redraw(); updateButtons(); setHint( - lines.length - ? "已撤销最后一条,剩余 " + lines.length + " 条" - : "已撤销全部线条" + markers.length + ? "已撤销最后一个标记,剩余 " + markers.length + " 个" + : "已撤销全部标记" ); }); btnClear.addEventListener("click", function () { - if (lines.length === 0) return; - if (!confirm("确定清空所有标注线条?")) return; + if (markers.length === 0) return; + if (!confirm("确定清空所有标注?")) return; clearAnnotations(); setHint("已清空全部标注"); });