修改
This commit is contained in:
@@ -217,7 +217,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#overlay-canvas.can-drag {
|
#overlay-canvas.can-drag {
|
||||||
cursor: ns-resize;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
@@ -235,23 +235,24 @@ body {
|
|||||||
|
|
||||||
.legend {
|
.legend {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 24px;
|
width: 0;
|
||||||
height: 3px;
|
height: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-left: 4px;
|
margin-left: 6px;
|
||||||
border-radius: 1px;
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend.entry {
|
.legend.entry {
|
||||||
background: var(--entry);
|
border-bottom: 10px solid var(--entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend.exit {
|
.legend.exit {
|
||||||
background: var(--exit);
|
border-top: 10px solid var(--exit);
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend.stop {
|
.legend.stop {
|
||||||
background: var(--stop);
|
border-top: 10px solid var(--stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-note {
|
.footer-note {
|
||||||
|
|||||||
+4
-4
@@ -15,9 +15,9 @@
|
|||||||
<input type="file" id="file-input" accept="image/jpeg,image/png,.jpg,.jpeg,.png" hidden>
|
<input type="file" id="file-input" accept="image/jpeg,image/png,.jpg,.jpeg,.png" hidden>
|
||||||
</label>
|
</label>
|
||||||
<div class="mode-group" role="group" aria-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 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="exit">出场 ↓</button>
|
||||||
<button type="button" class="btn mode-btn" data-mode="stop">止损线</button>
|
<button type="button" class="btn mode-btn" data-mode="stop">止损 ↓</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn" id="btn-undo" disabled>撤销</button>
|
<button type="button" class="btn" id="btn-undo" disabled>撤销</button>
|
||||||
<button type="button" class="btn" id="btn-clear" 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 entry"></i></span>
|
||||||
<span>出场 <i class="legend exit"></i></span>
|
<span>出场 <i class="legend exit"></i></span>
|
||||||
<span>止损 <i class="legend stop"></i></span>
|
<span>止损 <i class="legend stop"></i></span>
|
||||||
<span class="footer-note">单击添加水平线 · 拖拽调整位置</span>
|
<span class="footer-note">单击添加箭头 · 拖拽调整位置 · 可连续标注多个点位</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="js/app.js"></script>
|
<script src="js/app.js"></script>
|
||||||
|
|||||||
+112
-55
@@ -1,14 +1,17 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const LINE_TYPES = {
|
const MARKER_TYPES = {
|
||||||
entry: { label: "入场线", color: "#00ff00" },
|
entry: { label: "入场", color: "#00ff00", direction: "up" },
|
||||||
exit: { label: "出场线", color: "#0099ff" },
|
exit: { label: "出场", color: "#0099ff", direction: "down" },
|
||||||
stop: { label: "止损线", color: "#ff3333" },
|
stop: { label: "止损", color: "#ff3333", direction: "down" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const LINE_WIDTH = 3;
|
const ARROW_HEAD = 12;
|
||||||
const HIT_TOLERANCE = 8;
|
const ARROW_STEM = 14;
|
||||||
|
const ARROW_WIDTH = 9;
|
||||||
|
const LINE_WIDTH = 2;
|
||||||
|
const HIT_RADIUS = 18;
|
||||||
|
|
||||||
const fileInput = document.getElementById("file-input");
|
const fileInput = document.getElementById("file-input");
|
||||||
const dropZone = document.getElementById("drop-zone");
|
const dropZone = document.getElementById("drop-zone");
|
||||||
@@ -24,7 +27,7 @@
|
|||||||
const modeButtons = document.querySelectorAll(".mode-btn");
|
const modeButtons = document.querySelectorAll(".mode-btn");
|
||||||
|
|
||||||
let currentMode = "entry";
|
let currentMode = "entry";
|
||||||
let lines = [];
|
let markers = [];
|
||||||
let displayWidth = 0;
|
let displayWidth = 0;
|
||||||
let displayHeight = 0;
|
let displayHeight = 0;
|
||||||
let imageLoaded = false;
|
let imageLoaded = false;
|
||||||
@@ -38,9 +41,9 @@
|
|||||||
|
|
||||||
function updateButtons() {
|
function updateButtons() {
|
||||||
const hasImage = imageLoaded;
|
const hasImage = imageLoaded;
|
||||||
const hasLines = lines.length > 0;
|
const hasMarkers = markers.length > 0;
|
||||||
btnUndo.disabled = !hasImage || !hasLines;
|
btnUndo.disabled = !hasImage || !hasMarkers;
|
||||||
btnClear.disabled = !hasImage || !hasLines;
|
btnClear.disabled = !hasImage || !hasMarkers;
|
||||||
btnDownload.disabled = !hasImage;
|
btnDownload.disabled = !hasImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,25 +57,67 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLineAtY(y) {
|
function getMarkerDirection(type) {
|
||||||
for (let i = lines.length - 1; i >= 0; i--) {
|
return MARKER_TYPES[type].direction;
|
||||||
if (Math.abs(lines[i].y - y) <= HIT_TOLERANCE) {
|
}
|
||||||
|
|
||||||
|
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 i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
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() {
|
function redraw() {
|
||||||
if (!imageLoaded) return;
|
if (!imageLoaded) return;
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
for (const line of lines) {
|
for (const m of markers) {
|
||||||
ctx.beginPath();
|
drawArrowMarker(ctx, m.x, m.y, m.type, m.color);
|
||||||
ctx.strokeStyle = line.color;
|
|
||||||
ctx.lineWidth = LINE_WIDTH;
|
|
||||||
ctx.moveTo(0, line.y);
|
|
||||||
ctx.lineTo(canvas.width, line.y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,9 +132,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearAnnotations() {
|
function clearAnnotations() {
|
||||||
lines = [];
|
markers = [];
|
||||||
dragIndex = -1;
|
dragIndex = -1;
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
|
didDragMove = false;
|
||||||
redraw();
|
redraw();
|
||||||
updateButtons();
|
updateButtons();
|
||||||
}
|
}
|
||||||
@@ -116,8 +162,8 @@
|
|||||||
syncCanvasSize();
|
syncCanvasSize();
|
||||||
setHint(
|
setHint(
|
||||||
"当前模式:" +
|
"当前模式:" +
|
||||||
LINE_TYPES[currentMode].label +
|
MARKER_TYPES[currentMode].label +
|
||||||
" — 在图上单击添加水平线,可拖拽调整"
|
" — 在图上单击添加箭头标记,可拖拽调整"
|
||||||
);
|
);
|
||||||
updateButtons();
|
updateButtons();
|
||||||
});
|
});
|
||||||
@@ -135,19 +181,18 @@
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLine(y) {
|
function addMarker(x, y) {
|
||||||
const type = currentMode;
|
const type = currentMode;
|
||||||
const info = LINE_TYPES[type];
|
const info = MARKER_TYPES[type];
|
||||||
lines.push({ y: y, type: type, color: info.color });
|
markers.push({ x: x, y: y, type: type, color: info.color });
|
||||||
redraw();
|
redraw();
|
||||||
updateButtons();
|
updateButtons();
|
||||||
setHint(
|
setHint(
|
||||||
"已添加 " +
|
"已添加 " +
|
||||||
info.label +
|
info.label +
|
||||||
"(共 " +
|
" 标记(共 " +
|
||||||
lines.length +
|
markers.length +
|
||||||
" 条)— 当前模式:" +
|
" 个)— 可切换模式继续标注"
|
||||||
info.label
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +201,7 @@
|
|||||||
|
|
||||||
const natW = chartImage.naturalWidth;
|
const natW = chartImage.naturalWidth;
|
||||||
const natH = chartImage.naturalHeight;
|
const natH = chartImage.naturalHeight;
|
||||||
|
const scaleX = natW / displayWidth;
|
||||||
const scaleY = natH / displayHeight;
|
const scaleY = natH / displayHeight;
|
||||||
|
|
||||||
const exportCanvas = document.createElement("canvas");
|
const exportCanvas = document.createElement("canvas");
|
||||||
@@ -165,14 +211,14 @@
|
|||||||
|
|
||||||
exCtx.drawImage(chartImage, 0, 0, natW, natH);
|
exCtx.drawImage(chartImage, 0, 0, natW, natH);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const m of markers) {
|
||||||
const y = line.y * scaleY;
|
drawArrowMarker(
|
||||||
exCtx.beginPath();
|
exCtx,
|
||||||
exCtx.strokeStyle = line.color;
|
m.x * scaleX,
|
||||||
exCtx.lineWidth = LINE_WIDTH;
|
m.y * scaleY,
|
||||||
exCtx.moveTo(0, y);
|
m.type,
|
||||||
exCtx.lineTo(natW, y);
|
m.color
|
||||||
exCtx.stroke();
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
exportCanvas.toBlob(function (blob) {
|
exportCanvas.toBlob(function (blob) {
|
||||||
@@ -204,8 +250,8 @@
|
|||||||
}, "image/png");
|
}, "image/png");
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCursor(y) {
|
function updateCursor(x, y) {
|
||||||
const idx = findLineAtY(y);
|
const idx = findMarkerAt(x, y);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
canvas.classList.add("can-drag");
|
canvas.classList.add("can-drag");
|
||||||
} else {
|
} else {
|
||||||
@@ -214,14 +260,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
modeButtons.forEach(function (btn) {
|
modeButtons.forEach(function (btn) {
|
||||||
btn.addEventListener("click", function () {
|
btn.addEventListener("click", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
modeButtons.forEach(function (b) {
|
modeButtons.forEach(function (b) {
|
||||||
b.classList.remove("active");
|
b.classList.remove("active");
|
||||||
});
|
});
|
||||||
btn.classList.add("active");
|
btn.classList.add("active");
|
||||||
currentMode = btn.dataset.mode;
|
currentMode = btn.dataset.mode;
|
||||||
|
didDragMove = false;
|
||||||
if (imageLoaded) {
|
if (imageLoaded) {
|
||||||
setHint("当前模式:" + LINE_TYPES[currentMode].label + " — 单击添加水平线");
|
setHint(
|
||||||
|
"当前模式:" +
|
||||||
|
MARKER_TYPES[currentMode].label +
|
||||||
|
" — 单击添加箭头,可连续标注多个点位"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -254,7 +306,7 @@
|
|||||||
if (!imageLoaded) return;
|
if (!imageLoaded) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const pt = getCanvasPoint(e);
|
const pt = getCanvasPoint(e);
|
||||||
const idx = findLineAtY(pt.y);
|
const idx = findMarkerAt(pt.x, pt.y);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
dragIndex = idx;
|
dragIndex = idx;
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
@@ -269,12 +321,13 @@
|
|||||||
|
|
||||||
if (isDragging && dragIndex >= 0) {
|
if (isDragging && dragIndex >= 0) {
|
||||||
didDragMove = true;
|
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();
|
redraw();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCursor(pt.y);
|
updateCursor(pt.x, pt.y);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("mouseup", function () {
|
canvas.addEventListener("mouseup", function () {
|
||||||
@@ -289,27 +342,31 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("click", function (e) {
|
canvas.addEventListener("click", function (e) {
|
||||||
if (!imageLoaded || didDragMove) return;
|
if (!imageLoaded) return;
|
||||||
|
if (didDragMove) {
|
||||||
|
didDragMove = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pt = getCanvasPoint(e);
|
const pt = getCanvasPoint(e);
|
||||||
if (findLineAtY(pt.y) >= 0) return;
|
if (findMarkerAt(pt.x, pt.y) >= 0) return;
|
||||||
addLine(pt.y);
|
addMarker(pt.x, pt.y);
|
||||||
});
|
});
|
||||||
|
|
||||||
btnUndo.addEventListener("click", function () {
|
btnUndo.addEventListener("click", function () {
|
||||||
if (lines.length === 0) return;
|
if (markers.length === 0) return;
|
||||||
lines.pop();
|
markers.pop();
|
||||||
redraw();
|
redraw();
|
||||||
updateButtons();
|
updateButtons();
|
||||||
setHint(
|
setHint(
|
||||||
lines.length
|
markers.length
|
||||||
? "已撤销最后一条,剩余 " + lines.length + " 条"
|
? "已撤销最后一个标记,剩余 " + markers.length + " 个"
|
||||||
: "已撤销全部线条"
|
: "已撤销全部标记"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
btnClear.addEventListener("click", function () {
|
btnClear.addEventListener("click", function () {
|
||||||
if (lines.length === 0) return;
|
if (markers.length === 0) return;
|
||||||
if (!confirm("确定清空所有标注线条?")) return;
|
if (!confirm("确定清空所有标注?")) return;
|
||||||
clearAnnotations();
|
clearAnnotations();
|
||||||
setHint("已清空全部标注");
|
setHint("已清空全部标注");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user