This commit is contained in:
dekun
2026-05-27 14:33:52 +08:00
parent aa33cb5da6
commit dc1b4989b4
3 changed files with 125 additions and 67 deletions
+9 -8
View File
@@ -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 {
+4 -4
View File
@@ -15,9 +15,9 @@
<input type="file" id="file-input" accept="image/jpeg,image/png,.jpg,.jpeg,.png" hidden>
</label>
<div class="mode-group" role="group" aria-label="标注模式">
<button type="button" class="btn mode-btn active" data-mode="entry">入场线</button>
<button type="button" class="btn mode-btn" data-mode="exit">出场线</button>
<button type="button" class="btn mode-btn" data-mode="stop">止损线</button>
<button type="button" class="btn mode-btn active" data-mode="entry">入场</button>
<button type="button" class="btn mode-btn" data-mode="exit">出场</button>
<button type="button" class="btn mode-btn" data-mode="stop">止损</button>
</div>
<button type="button" class="btn" id="btn-undo" disabled>撤销</button>
<button type="button" class="btn" id="btn-clear" disabled>清空全部</button>
@@ -44,7 +44,7 @@
<span>入场 <i class="legend entry"></i></span>
<span>出场 <i class="legend exit"></i></span>
<span>止损 <i class="legend stop"></i></span>
<span class="footer-note">单击添加水平线 · 拖拽调整位置</span>
<span class="footer-note">单击添加箭头 · 拖拽调整位置 · 可连续标注多个点位</span>
</footer>
<script src="js/app.js"></script>
+112 -55
View File
@@ -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("已清空全部标注");
});