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("已清空全部标注");
});