修改
This commit is contained in:
@@ -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
@@ -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
@@ -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("已清空全部标注");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user