修改
This commit is contained in:
+30
-1
@@ -237,6 +237,31 @@ body {
|
||||
box-shadow: inset 0 -2px 0 var(--stop);
|
||||
}
|
||||
|
||||
.mode-btn.has-marker::after {
|
||||
content: "●";
|
||||
margin-left: 4px;
|
||||
font-size: 0.55rem;
|
||||
vertical-align: super;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.mode-btn[data-mode="entry"].has-marker::after {
|
||||
color: var(--entry);
|
||||
}
|
||||
|
||||
.mode-btn[data-mode="exit"].has-marker::after {
|
||||
color: var(--exit);
|
||||
}
|
||||
|
||||
.mode-btn[data-mode="stop"].has-marker::after {
|
||||
color: var(--stop);
|
||||
}
|
||||
|
||||
.viewport.is-panning,
|
||||
.viewport.is-panning #overlay-canvas {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -325,7 +350,11 @@ body {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: crosshair;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#overlay-canvas.can-pan {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#overlay-canvas.can-drag {
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
<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="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>
|
||||
@@ -72,7 +72,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>
|
||||
|
||||
+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