修改
This commit is contained in:
+159
-74
@@ -40,22 +40,25 @@
|
||||
const angleSlider = document.getElementById("angle-slider");
|
||||
const angleInput = document.getElementById("angle-input");
|
||||
|
||||
let currentMode = "entry";
|
||||
let currentMode = null;
|
||||
let pendingAngle = 0;
|
||||
let markers = [];
|
||||
let baseWidth = 0;
|
||||
let baseHeight = 0;
|
||||
let zoom = 1;
|
||||
let imageLoaded = false;
|
||||
let uploadedFileName = "";
|
||||
let dragIndex = -1;
|
||||
let selectedIndex = -1;
|
||||
let isDragging = false;
|
||||
let isDraggingMarker = false;
|
||||
let didDragMove = false;
|
||||
let isPanning = false;
|
||||
let didPanMove = false;
|
||||
let panStartX = 0;
|
||||
let panStartY = 0;
|
||||
let panScrollLeft = 0;
|
||||
let panScrollTop = 0;
|
||||
const PAN_THRESHOLD = 4;
|
||||
|
||||
function setHint(text) {
|
||||
statusHint.textContent = text;
|
||||
@@ -121,6 +124,60 @@
|
||||
return -1;
|
||||
}
|
||||
|
||||
function findMarkerIndexByType(type) {
|
||||
return markers.findIndex(function (m) {
|
||||
return m.type === type;
|
||||
});
|
||||
}
|
||||
|
||||
function updateModeButtonStates() {
|
||||
modeButtons.forEach(function (btn) {
|
||||
const type = btn.dataset.mode;
|
||||
btn.classList.toggle("has-marker", findMarkerIndexByType(type) >= 0);
|
||||
btn.classList.toggle("active", currentMode === type);
|
||||
});
|
||||
}
|
||||
|
||||
function clearModeSelection() {
|
||||
currentMode = null;
|
||||
updateModeButtonStates();
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
if (currentMode === mode) {
|
||||
clearModeSelection();
|
||||
setHint("已取消选择 · 拖动空白处平移画布 · 点击入场/出场/止损开始标注");
|
||||
return;
|
||||
}
|
||||
|
||||
currentMode = mode;
|
||||
const info = MARKER_TYPES[mode];
|
||||
const existIdx = findMarkerIndexByType(mode);
|
||||
|
||||
if (existIdx >= 0) {
|
||||
selectedIndex = existIdx;
|
||||
syncAngleUI(markers[existIdx].angle);
|
||||
setHint(
|
||||
info.label +
|
||||
" 已标注 · 单击图上可移动位置,或拖动箭头 · 再次点击「" +
|
||||
info.label +
|
||||
"」取消选择"
|
||||
);
|
||||
} else {
|
||||
selectedIndex = -1;
|
||||
setHint(
|
||||
"已选择「" +
|
||||
info.label +
|
||||
"」· 在图上单击放置(仅一个)· 方向 " +
|
||||
pendingAngle +
|
||||
"°"
|
||||
);
|
||||
}
|
||||
|
||||
updateModeButtonStates();
|
||||
redraw();
|
||||
}
|
||||
|
||||
function drawArrowMarker(targetCtx, x, y, angleDeg, color, highlight) {
|
||||
const w = ARROW_WIDTH;
|
||||
const h = ARROW_HEAD;
|
||||
@@ -268,8 +325,14 @@
|
||||
selectedIndex = index;
|
||||
if (index >= 0) {
|
||||
syncAngleUI(markers[index].angle);
|
||||
const label = MARKER_TYPES[markers[index].type].label;
|
||||
setHint(
|
||||
"已选中标记,可调整方向或拖拽移动;点击空白处添加新标记"
|
||||
"已选中「" +
|
||||
label +
|
||||
"」· 可拖动移动或调整方向" +
|
||||
(currentMode
|
||||
? ""
|
||||
: " · 点击对应类型按钮后可重新放置")
|
||||
);
|
||||
}
|
||||
redraw();
|
||||
@@ -279,8 +342,10 @@
|
||||
markers = [];
|
||||
dragIndex = -1;
|
||||
selectedIndex = -1;
|
||||
isDragging = false;
|
||||
isDraggingMarker = false;
|
||||
didDragMove = false;
|
||||
didPanMove = false;
|
||||
clearModeSelection();
|
||||
redraw();
|
||||
updateButtons();
|
||||
}
|
||||
@@ -292,6 +357,16 @@
|
||||
directionRow.hidden = !show;
|
||||
}
|
||||
|
||||
function getDownloadFileName() {
|
||||
if (!uploadedFileName) {
|
||||
return "标注1.png";
|
||||
}
|
||||
const lastDot = uploadedFileName.lastIndexOf(".");
|
||||
const base =
|
||||
lastDot > 0 ? uploadedFileName.slice(0, lastDot) : uploadedFileName;
|
||||
return base + "1.png";
|
||||
}
|
||||
|
||||
function loadImageFile(file) {
|
||||
if (!file) return;
|
||||
const validTypes = ["image/jpeg", "image/png"];
|
||||
@@ -302,6 +377,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
uploadedFileName = file.name;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
chartImage.onload = function () {
|
||||
@@ -310,6 +387,7 @@
|
||||
dropZone.classList.remove("drag-over");
|
||||
zoom = 1;
|
||||
clearAnnotations();
|
||||
clearModeSelection();
|
||||
syncAngleUI(0);
|
||||
requestAnimationFrame(function () {
|
||||
calculateBaseSize();
|
||||
@@ -317,9 +395,7 @@
|
||||
viewport.scrollLeft = 0;
|
||||
viewport.scrollTop = 0;
|
||||
setHint(
|
||||
"滚轮缩放 · 右键/中键拖动画布 · 设置方向后单击添加 " +
|
||||
MARKER_TYPES[currentMode].label +
|
||||
" 标记"
|
||||
"滚轮缩放 · 拖动空白处平移 · 先点「入场/出场/止损」再单击图上放置(各仅一个)"
|
||||
);
|
||||
updateButtons();
|
||||
});
|
||||
@@ -338,29 +414,35 @@
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function addMarker(x, y) {
|
||||
function placeOrMoveMarker(x, y) {
|
||||
if (!currentMode) return;
|
||||
|
||||
const type = currentMode;
|
||||
const info = MARKER_TYPES[type];
|
||||
const ratio = xyToRatio(x, y);
|
||||
markers.push({
|
||||
xRatio: ratio.xRatio,
|
||||
yRatio: ratio.yRatio,
|
||||
type: type,
|
||||
color: info.color,
|
||||
angle: pendingAngle,
|
||||
});
|
||||
selectedIndex = markers.length - 1;
|
||||
const existIdx = findMarkerIndexByType(type);
|
||||
|
||||
if (existIdx >= 0) {
|
||||
markers[existIdx].xRatio = ratio.xRatio;
|
||||
markers[existIdx].yRatio = ratio.yRatio;
|
||||
markers[existIdx].angle = pendingAngle;
|
||||
selectedIndex = existIdx;
|
||||
setHint(info.label + " 已移动到当前位置(" + pendingAngle + "°)");
|
||||
} else {
|
||||
markers.push({
|
||||
xRatio: ratio.xRatio,
|
||||
yRatio: ratio.yRatio,
|
||||
type: type,
|
||||
color: info.color,
|
||||
angle: pendingAngle,
|
||||
});
|
||||
selectedIndex = markers.length - 1;
|
||||
setHint("已放置「" + info.label + "」(" + pendingAngle + "°)");
|
||||
}
|
||||
|
||||
redraw();
|
||||
updateButtons();
|
||||
setHint(
|
||||
"已添加 " +
|
||||
info.label +
|
||||
"(" +
|
||||
pendingAngle +
|
||||
"°,共 " +
|
||||
markers.length +
|
||||
" 个)"
|
||||
);
|
||||
updateModeButtonStates();
|
||||
}
|
||||
|
||||
function exportImage() {
|
||||
@@ -394,18 +476,7 @@
|
||||
}
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
const ts = new Date();
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
const name =
|
||||
"kline-label-" +
|
||||
ts.getFullYear() +
|
||||
pad(ts.getMonth() + 1) +
|
||||
pad(ts.getDate()) +
|
||||
"-" +
|
||||
pad(ts.getHours()) +
|
||||
pad(ts.getMinutes()) +
|
||||
pad(ts.getSeconds()) +
|
||||
".png";
|
||||
const name = getDownloadFileName();
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
@@ -417,33 +488,28 @@
|
||||
}
|
||||
|
||||
function updateCursor(x, y) {
|
||||
if (isPanning) return;
|
||||
if (isPanning) {
|
||||
canvas.classList.remove("can-drag");
|
||||
canvas.classList.add("can-pan");
|
||||
return;
|
||||
}
|
||||
const idx = findMarkerAt(x, y);
|
||||
canvas.classList.remove("can-pan");
|
||||
if (idx >= 0) {
|
||||
canvas.classList.add("can-drag");
|
||||
} else {
|
||||
canvas.classList.remove("can-drag");
|
||||
canvas.classList.add("can-pan");
|
||||
}
|
||||
}
|
||||
|
||||
modeButtons.forEach(function (btn) {
|
||||
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(
|
||||
"当前:" +
|
||||
MARKER_TYPES[currentMode].label +
|
||||
" · 方向 " +
|
||||
pendingAngle +
|
||||
"° · 单击添加标记"
|
||||
);
|
||||
}
|
||||
didPanMove = false;
|
||||
if (!imageLoaded) return;
|
||||
setMode(btn.dataset.mode);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -519,6 +585,7 @@
|
||||
function startPan(e) {
|
||||
if (!imageLoaded) return;
|
||||
isPanning = true;
|
||||
didPanMove = false;
|
||||
panStartX = e.clientX;
|
||||
panStartY = e.clientY;
|
||||
panScrollLeft = viewport.scrollLeft;
|
||||
@@ -537,31 +604,41 @@
|
||||
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
didDragMove = false;
|
||||
didPanMove = false;
|
||||
|
||||
const pt = getCanvasPoint(e);
|
||||
const idx = findMarkerAt(pt.x, pt.y);
|
||||
|
||||
if (idx >= 0) {
|
||||
dragIndex = idx;
|
||||
isDragging = true;
|
||||
didDragMove = false;
|
||||
isDraggingMarker = true;
|
||||
selectMarker(idx);
|
||||
canvas.classList.add("can-drag");
|
||||
} else {
|
||||
selectMarker(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
selectMarker(-1);
|
||||
startPan(e);
|
||||
});
|
||||
|
||||
canvas.addEventListener("mousemove", function (e) {
|
||||
if (!imageLoaded) return;
|
||||
|
||||
if (isPanning) {
|
||||
viewport.scrollLeft = panScrollLeft - (e.clientX - panStartX);
|
||||
viewport.scrollTop = panScrollTop - (e.clientY - panStartY);
|
||||
const dx = e.clientX - panStartX;
|
||||
const dy = e.clientY - panStartY;
|
||||
if (Math.abs(dx) > PAN_THRESHOLD || Math.abs(dy) > PAN_THRESHOLD) {
|
||||
didPanMove = true;
|
||||
}
|
||||
viewport.scrollLeft = panScrollLeft - dx;
|
||||
viewport.scrollTop = panScrollTop - dy;
|
||||
return;
|
||||
}
|
||||
|
||||
const pt = getCanvasPoint(e);
|
||||
|
||||
if (isDragging && dragIndex >= 0) {
|
||||
if (isDraggingMarker && dragIndex >= 0) {
|
||||
didDragMove = true;
|
||||
const ratio = xyToRatio(
|
||||
Math.max(0, Math.min(canvas.width, pt.x)),
|
||||
@@ -581,19 +658,20 @@
|
||||
viewport.classList.remove("is-panning");
|
||||
}
|
||||
|
||||
canvas.addEventListener("mouseup", function (e) {
|
||||
if (e.button === 1 || e.button === 2 || isPanning) {
|
||||
endPan();
|
||||
}
|
||||
isDragging = false;
|
||||
function endPointer() {
|
||||
endPan();
|
||||
isDraggingMarker = false;
|
||||
dragIndex = -1;
|
||||
canvas.classList.remove("can-drag", "can-pan");
|
||||
}
|
||||
|
||||
canvas.addEventListener("mouseup", function () {
|
||||
endPointer();
|
||||
});
|
||||
|
||||
canvas.addEventListener("mouseleave", function () {
|
||||
endPan();
|
||||
isDragging = false;
|
||||
dragIndex = -1;
|
||||
canvas.classList.remove("can-drag");
|
||||
endPointer();
|
||||
canvas.classList.remove("can-pan");
|
||||
});
|
||||
|
||||
canvas.addEventListener("contextmenu", function (e) {
|
||||
@@ -602,26 +680,32 @@
|
||||
|
||||
canvas.addEventListener("click", function (e) {
|
||||
if (!imageLoaded) return;
|
||||
if (didDragMove) {
|
||||
if (didDragMove || didPanMove) {
|
||||
didDragMove = false;
|
||||
didPanMove = false;
|
||||
return;
|
||||
}
|
||||
if (!currentMode) return;
|
||||
|
||||
const pt = getCanvasPoint(e);
|
||||
if (findMarkerAt(pt.x, pt.y) >= 0) return;
|
||||
addMarker(pt.x, pt.y);
|
||||
placeOrMoveMarker(pt.x, pt.y);
|
||||
});
|
||||
|
||||
btnUndo.addEventListener("click", function () {
|
||||
if (markers.length === 0) return;
|
||||
markers.pop();
|
||||
if (selectedIndex >= markers.length) {
|
||||
const removed = markers.pop();
|
||||
if (currentMode === removed.type) {
|
||||
selectedIndex = -1;
|
||||
} else if (selectedIndex >= markers.length) {
|
||||
selectedIndex = markers.length - 1;
|
||||
}
|
||||
redraw();
|
||||
updateButtons();
|
||||
updateModeButtonStates();
|
||||
setHint(
|
||||
markers.length
|
||||
? "已撤销最后一个标记,剩余 " + markers.length + " 个"
|
||||
? "已撤销「" + MARKER_TYPES[removed.type].label + "」"
|
||||
: "已撤销全部标记"
|
||||
);
|
||||
});
|
||||
@@ -641,9 +725,10 @@
|
||||
applyLayout();
|
||||
});
|
||||
|
||||
window.addEventListener("mouseup", endPan);
|
||||
window.addEventListener("mouseup", endPointer);
|
||||
|
||||
showEditorUI(false);
|
||||
updateButtons();
|
||||
updateModeButtonStates();
|
||||
syncAngleUI(0);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user