fix: TV-style fib colors, range badge, polyline preview, one-shot tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,8 +4,30 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const STORAGE_PREFIX = "hubMarketDraw:";
|
const STORAGE_PREFIX = "hubMarketDraw:";
|
||||||
const HIT_PX = 8;
|
const HIT_PX = 8;
|
||||||
const FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
const FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 0.886, 1];
|
||||||
|
const FIB_LINE_COLORS = {
|
||||||
|
0: "#787b86",
|
||||||
|
0.236: "#f23645",
|
||||||
|
0.382: "#e6b422",
|
||||||
|
0.5: "#5d606b",
|
||||||
|
0.618: "#d97706",
|
||||||
|
0.786: "#26a69a",
|
||||||
|
0.886: "#9c27b0",
|
||||||
|
1: "#11734b",
|
||||||
|
};
|
||||||
|
const FIB_ZONE_FILLS = [
|
||||||
|
{ top: 1, bot: 0.886, fill: "rgba(156, 39, 176, 0.14)" },
|
||||||
|
{ top: 0.886, bot: 0.786, fill: "rgba(38, 166, 154, 0.14)" },
|
||||||
|
{ top: 0.786, bot: 0.618, fill: "rgba(0, 188, 212, 0.14)" },
|
||||||
|
{ top: 0.618, bot: 0.5, fill: "rgba(244, 143, 177, 0.16)" },
|
||||||
|
{ top: 0.5, bot: 0.382, fill: "rgba(120, 123, 134, 0.12)" },
|
||||||
|
{ top: 0.382, bot: 0.236, fill: "rgba(255, 183, 77, 0.16)" },
|
||||||
|
{ top: 0.236, bot: 0, fill: "rgba(242, 54, 69, 0.12)" },
|
||||||
|
];
|
||||||
const DRAG_TOOLS = new Set(["trend", "rect", "range", "fib"]);
|
const DRAG_TOOLS = new Set(["trend", "rect", "range", "fib"]);
|
||||||
|
const ONE_SHOT_TOOLS = new Set([
|
||||||
|
"hline", "cross", "channel", "rect", "brush", "range", "text", "fib", "trend", "path", "erase",
|
||||||
|
]);
|
||||||
const MIN_DRAG_PX = 6;
|
const MIN_DRAG_PX = 6;
|
||||||
|
|
||||||
const TOOL_LABELS = {
|
const TOOL_LABELS = {
|
||||||
@@ -19,7 +41,7 @@
|
|||||||
text: "文字",
|
text: "文字",
|
||||||
fib: "斐波那契",
|
fib: "斐波那契",
|
||||||
trend: "趋势线",
|
trend: "趋势线",
|
||||||
path: "折线箭头",
|
path: "折线",
|
||||||
erase: "删除选中",
|
erase: "删除选中",
|
||||||
clear: "清除全部",
|
clear: "清除全部",
|
||||||
};
|
};
|
||||||
@@ -41,6 +63,7 @@
|
|||||||
let brushPointerId = null;
|
let brushPointerId = null;
|
||||||
let dragActive = false;
|
let dragActive = false;
|
||||||
let dragStartPx = null;
|
let dragStartPx = null;
|
||||||
|
let pathPreviewPt = null;
|
||||||
|
|
||||||
function uid() {
|
function uid() {
|
||||||
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||||||
@@ -222,6 +245,73 @@
|
|||||||
return n.toFixed(6);
|
return n.toFixed(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function signedPrice(n) {
|
||||||
|
const s = formatPrice(Math.abs(n));
|
||||||
|
return n < 0 ? "-" + s : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateTickSize(price) {
|
||||||
|
const a = Math.abs(Number(price)) || 1;
|
||||||
|
if (a >= 10000) return 0.01;
|
||||||
|
if (a >= 100) return 0.01;
|
||||||
|
if (a >= 1) return 0.0001;
|
||||||
|
if (a >= 0.01) return 0.000001;
|
||||||
|
return 0.0000001;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tickCount(diff, refPrice) {
|
||||||
|
const step = estimateTickSize(refPrice);
|
||||||
|
if (!step) return 0;
|
||||||
|
return Math.round(diff / step);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fibPriceAt(top, bot, lv) {
|
||||||
|
return bot + (top - bot) * (1 - lv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawHandle(ctx, x, y, large, color) {
|
||||||
|
if (x == null || y == null) return;
|
||||||
|
const r = large ? 6 : 4.5;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "#ffffff";
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = color || "#2962ff";
|
||||||
|
ctx.lineWidth = large ? 2 : 1.5;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundBadge(ctx, x, y, text) {
|
||||||
|
ctx.font = "11px sans-serif";
|
||||||
|
const padX = 8;
|
||||||
|
const padY = 5;
|
||||||
|
const tw = ctx.measureText(text).width;
|
||||||
|
const bw = tw + padX * 2;
|
||||||
|
const bh = 20;
|
||||||
|
const left = x - bw / 2;
|
||||||
|
const top = y - bh / 2;
|
||||||
|
ctx.fillStyle = "rgba(30, 58, 138, 0.92)";
|
||||||
|
ctx.beginPath();
|
||||||
|
const r = 4;
|
||||||
|
ctx.moveTo(left + r, top);
|
||||||
|
ctx.lineTo(left + bw - r, top);
|
||||||
|
ctx.quadraticCurveTo(left + bw, top, left + bw, top + r);
|
||||||
|
ctx.lineTo(left + bw, top + bh - r);
|
||||||
|
ctx.quadraticCurveTo(left + bw, top + bh, left + bw - r, top + bh);
|
||||||
|
ctx.lineTo(left + r, top + bh);
|
||||||
|
ctx.quadraticCurveTo(left, top + bh, left, top + bh - r);
|
||||||
|
ctx.lineTo(left, top + r);
|
||||||
|
ctx.quadraticCurveTo(left, top, left + r, top);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = "#f8fafc";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(text, x, y + 1);
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.textBaseline = "alphabetic";
|
||||||
|
}
|
||||||
|
|
||||||
function isDragTool(tool) {
|
function isDragTool(tool) {
|
||||||
return DRAG_TOOLS.has(tool);
|
return DRAG_TOOLS.has(tool);
|
||||||
}
|
}
|
||||||
@@ -230,9 +320,16 @@
|
|||||||
draft = null;
|
draft = null;
|
||||||
dragActive = false;
|
dragActive = false;
|
||||||
dragStartPx = null;
|
dragStartPx = null;
|
||||||
|
pathPreviewPt = null;
|
||||||
scheduleRedraw();
|
scheduleRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function returnToCursorIfOneShot() {
|
||||||
|
if (ONE_SHOT_TOOLS.has(activeTool)) {
|
||||||
|
setActiveTool("cursor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function drawLine(ctx, x1, y1, x2, y2, selected) {
|
function drawLine(ctx, x1, y1, x2, y2, selected) {
|
||||||
if (x1 == null || y1 == null || x2 == null || y2 == null) return;
|
if (x1 == null || y1 == null || x2 == null || y2 == null) return;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -296,25 +393,54 @@
|
|||||||
const bot = Math.min(p1.price, p2.price);
|
const bot = Math.min(p1.price, p2.price);
|
||||||
const x1 = timeToX(p1.time);
|
const x1 = timeToX(p1.time);
|
||||||
const x2 = timeToX(p2.time);
|
const x2 = timeToX(p2.time);
|
||||||
|
const y1 = priceToY(p1.price);
|
||||||
|
const y2 = priceToY(p2.price);
|
||||||
const yTop = priceToY(top);
|
const yTop = priceToY(top);
|
||||||
const yBot = priceToY(bot);
|
const yBot = priceToY(bot);
|
||||||
if (x1 == null || x2 == null || yTop == null || yBot == null) return;
|
if (x1 == null || x2 == null || yTop == null || yBot == null || y1 == null || y2 == null) return;
|
||||||
const left = Math.min(x1, x2);
|
const left = Math.min(x1, x2);
|
||||||
const right = Math.max(x1, x2);
|
const right = Math.max(x1, x2);
|
||||||
const span = Math.max(right - left, 48);
|
const span = Math.max(right - left, 48);
|
||||||
const drawRight = left + span;
|
const drawRight = left + span;
|
||||||
const color = strokeStyle(selected);
|
|
||||||
|
|
||||||
drawLine(ctx, left, yTop, left, yBot, selected);
|
FIB_ZONE_FILLS.forEach(function (zone) {
|
||||||
drawLine(ctx, drawRight, yTop, drawRight, yBot, selected);
|
const yA = priceToY(fibPriceAt(top, bot, zone.top));
|
||||||
|
const yB = priceToY(fibPriceAt(top, bot, zone.bot));
|
||||||
|
if (yA == null || yB == null) return;
|
||||||
|
const zt = Math.min(yA, yB);
|
||||||
|
const zb = Math.max(yA, yB);
|
||||||
|
ctx.fillStyle = zone.fill;
|
||||||
|
ctx.fillRect(left, zt, drawRight - left, Math.max(zb - zt, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = "rgba(120, 123, 134, 0.7)";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([4, 4]);
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
ctx.lineTo(x2, y2);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
|
ctx.strokeStyle = selected ? "#f59e0b" : "#787b86";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(left, yTop);
|
||||||
|
ctx.lineTo(left, yBot);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(drawRight, yTop);
|
||||||
|
ctx.lineTo(drawRight, yBot);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
let lastLabelY = -9999;
|
let lastLabelY = -9999;
|
||||||
FIB_LEVELS.forEach(function (lv) {
|
FIB_LEVELS.forEach(function (lv) {
|
||||||
const price = bot + (top - bot) * (1 - lv);
|
const price = fibPriceAt(top, bot, lv);
|
||||||
const y = priceToY(price);
|
const y = priceToY(price);
|
||||||
if (y == null) return;
|
if (y == null) return;
|
||||||
|
const lineColor = FIB_LINE_COLORS[lv] || "#787b86";
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = lineColor;
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.setLineDash(lv === 0 || lv === 1 ? [] : [5, 4]);
|
ctx.setLineDash(lv === 0 || lv === 1 ? [] : [5, 4]);
|
||||||
ctx.moveTo(left, y);
|
ctx.moveTo(left, y);
|
||||||
@@ -322,11 +448,16 @@
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
if (Math.abs(y - lastLabelY) < 13) return;
|
if (Math.abs(y - lastLabelY) < 13) return;
|
||||||
lastLabelY = y;
|
lastLabelY = y;
|
||||||
ctx.fillStyle = "#9aa4b8";
|
const lvLabel = lv === 1 || lv === 0 ? String(lv) : String(lv);
|
||||||
|
ctx.fillStyle = lineColor;
|
||||||
ctx.font = "10px sans-serif";
|
ctx.font = "10px sans-serif";
|
||||||
ctx.fillText((lv * 100).toFixed(1) + "% " + formatPrice(price), drawRight + 6, y + 3);
|
ctx.fillText(lvLabel + " (" + formatPrice(price) + ")", drawRight + 6, y + 3);
|
||||||
});
|
});
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
if (selected) {
|
||||||
|
drawHandle(ctx, x1, y1, false, "#2962ff");
|
||||||
|
drawHandle(ctx, x2, y2, false, "#2962ff");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawRange(ctx, p1, p2, selected) {
|
function drawRange(ctx, p1, p2, selected) {
|
||||||
@@ -341,25 +472,41 @@
|
|||||||
const bot = Math.max(y1, y2);
|
const bot = Math.max(y1, y2);
|
||||||
const boxW = Math.max(right - left, 10);
|
const boxW = Math.max(right - left, 10);
|
||||||
const boxH = Math.max(bot - top, 6);
|
const boxH = Math.max(bot - top, 6);
|
||||||
const color = strokeStyle(selected);
|
const borderColor = selected ? "#f59e0b" : "#1e293b";
|
||||||
|
|
||||||
ctx.fillStyle = selected ? "rgba(245,158,11,0.14)" : "rgba(96,165,250,0.12)";
|
ctx.fillStyle = selected ? "rgba(245,158,11,0.18)" : "rgba(45, 212, 191, 0.22)";
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = borderColor;
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1;
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
ctx.fillRect(left, top, boxW, boxH);
|
ctx.fillRect(left, top, boxW, boxH);
|
||||||
ctx.strokeRect(left, top, boxW, boxH);
|
ctx.strokeRect(left, top, boxW, boxH);
|
||||||
|
|
||||||
const pct = p1.price ? ((p2.price - p1.price) / p1.price) * 100 : 0;
|
|
||||||
const diff = p2.price - p1.price;
|
|
||||||
const midX = left + boxW / 2;
|
const midX = left + boxW / 2;
|
||||||
const midY = top + boxH / 2;
|
ctx.beginPath();
|
||||||
ctx.fillStyle = "#e2e8f0";
|
ctx.strokeStyle = borderColor;
|
||||||
ctx.font = "11px sans-serif";
|
ctx.lineWidth = 1;
|
||||||
ctx.textAlign = "center";
|
ctx.moveTo(midX, top);
|
||||||
ctx.fillText(pct.toFixed(2) + "%", midX, midY - 5);
|
ctx.lineTo(midX, bot);
|
||||||
ctx.fillText(formatPrice(diff), midX, midY + 11);
|
ctx.stroke();
|
||||||
ctx.textAlign = "left";
|
const arrowY = bot + 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.fillStyle = borderColor;
|
||||||
|
ctx.moveTo(midX, arrowY + 7);
|
||||||
|
ctx.lineTo(midX - 5, arrowY);
|
||||||
|
ctx.lineTo(midX + 5, arrowY);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
const diff = p2.price - p1.price;
|
||||||
|
const pct = p1.price ? (diff / p1.price) * 100 : 0;
|
||||||
|
const ticks = tickCount(diff, p1.price);
|
||||||
|
const tickStr = ticks < 0 ? String(ticks) : ticks > 0 ? "+" + ticks : "0";
|
||||||
|
const badgeText =
|
||||||
|
signedPrice(diff) + " (" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%) " + tickStr;
|
||||||
|
roundBadge(ctx, midX, bot + 22, badgeText);
|
||||||
|
|
||||||
|
drawHandle(ctx, left, top, false, "#2962ff");
|
||||||
|
drawHandle(ctx, right, bot, false, "#2962ff");
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawText(ctx, p, text, selected) {
|
function drawText(ctx, p, text, selected) {
|
||||||
@@ -414,28 +561,53 @@
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawPath(ctx, pts, selected) {
|
function drawPath(ctx, pts, selected, previewPt) {
|
||||||
drawBrush(ctx, pts, selected);
|
if (!pts || !pts.length) return;
|
||||||
if (!pts || pts.length < 2) return;
|
const color = strokeStyle(selected);
|
||||||
const last = pts[pts.length - 1];
|
const coords = [];
|
||||||
const prev = pts[pts.length - 2];
|
pts.forEach(function (p) {
|
||||||
const x1 = timeToX(prev.time);
|
const x = timeToX(p.time);
|
||||||
const y1 = priceToY(prev.price);
|
const y = priceToY(p.price);
|
||||||
const x2 = timeToX(last.time);
|
if (x != null && y != null) coords.push({ x: x, y: y });
|
||||||
const y2 = priceToY(last.price);
|
});
|
||||||
if (x1 == null || y1 == null || x2 == null || y2 == null) return;
|
if (coords.length < 1) return;
|
||||||
const ang = Math.atan2(y2 - y1, x2 - x1);
|
|
||||||
const al = 8;
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.fillStyle = strokeStyle(selected);
|
ctx.strokeStyle = color;
|
||||||
ctx.moveTo(x2, y2);
|
ctx.lineWidth = selected ? 2 : 1.5;
|
||||||
ctx.lineTo(x2 - al * Math.cos(ang - 0.4), y2 - al * Math.sin(ang - 0.4));
|
ctx.lineJoin = "round";
|
||||||
ctx.lineTo(x2 - al * Math.cos(ang + 0.4), y2 - al * Math.sin(ang + 0.4));
|
ctx.lineCap = "round";
|
||||||
ctx.closePath();
|
ctx.setLineDash([]);
|
||||||
ctx.fill();
|
ctx.moveTo(coords[0].x, coords[0].y);
|
||||||
|
for (let i = 1; i < coords.length; i++) {
|
||||||
|
ctx.lineTo(coords[i].x, coords[i].y);
|
||||||
|
}
|
||||||
|
if (coords.length > 1) ctx.stroke();
|
||||||
|
|
||||||
|
if (previewPt) {
|
||||||
|
const px = timeToX(previewPt.time);
|
||||||
|
const py = priceToY(previewPt.price);
|
||||||
|
const last = coords[coords.length - 1];
|
||||||
|
if (px != null && py != null && last) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.setLineDash([5, 4]);
|
||||||
|
ctx.moveTo(last.x, last.y);
|
||||||
|
ctx.lineTo(px, py);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
drawHandle(ctx, px, py, true, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coords.forEach(function (c, idx) {
|
||||||
|
const isLast = idx === coords.length - 1;
|
||||||
|
drawHandle(ctx, c.x, c.y, isLast && !!previewPt, color);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDrawing(ctx, d, w, h, selected) {
|
function renderDrawing(ctx, d, w, h, selected, previewPt) {
|
||||||
const pts = d.points || [];
|
const pts = d.points || [];
|
||||||
if (!pts.length) return;
|
if (!pts.length) return;
|
||||||
switch (d.type) {
|
switch (d.type) {
|
||||||
@@ -497,7 +669,7 @@
|
|||||||
if (pts.length >= 2) drawFib(ctx, pts[0], pts[1], w, selected);
|
if (pts.length >= 2) drawFib(ctx, pts[0], pts[1], w, selected);
|
||||||
break;
|
break;
|
||||||
case "path":
|
case "path":
|
||||||
drawPath(ctx, pts, selected);
|
drawPath(ctx, pts, selected, previewPt || null);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -515,7 +687,10 @@
|
|||||||
drawings.forEach(function (d) {
|
drawings.forEach(function (d) {
|
||||||
renderDrawing(ctx, d, w, h, d.id === selectedId);
|
renderDrawing(ctx, d, w, h, d.id === selectedId);
|
||||||
});
|
});
|
||||||
if (draft) renderDrawing(ctx, draft, w, h, true);
|
if (draft) {
|
||||||
|
const preview = draft.type === "path" ? pathPreviewPt : null;
|
||||||
|
renderDrawing(ctx, draft, w, h, true, preview);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitDrawing(d) {
|
function commitDrawing(d) {
|
||||||
@@ -523,8 +698,18 @@
|
|||||||
d.id = uid();
|
d.id = uid();
|
||||||
drawings.push(d);
|
drawings.push(d);
|
||||||
draft = null;
|
draft = null;
|
||||||
|
pathPreviewPt = null;
|
||||||
saveDrawings();
|
saveDrawings();
|
||||||
scheduleRedraw();
|
scheduleRedraw();
|
||||||
|
returnToCursorIfOneShot();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishPath() {
|
||||||
|
if (draft && draft.type === "path" && draft.points.length > 1) {
|
||||||
|
commitDrawing({ type: "path", points: draft.points.slice() });
|
||||||
|
} else {
|
||||||
|
cancelDraft();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointsNeeded(tool) {
|
function pointsNeeded(tool) {
|
||||||
@@ -552,7 +737,9 @@
|
|||||||
if (!chart || !series || !canvasEl) return;
|
if (!chart || !series || !canvasEl) return;
|
||||||
const loc = clientToLocal(ev);
|
const loc = clientToLocal(ev);
|
||||||
if (activeTool === "erase") {
|
if (activeTool === "erase") {
|
||||||
tryEraseAt(loc.x, loc.y);
|
if (tryEraseAt(loc.x, loc.y)) {
|
||||||
|
returnToCursorIfOneShot();
|
||||||
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -560,10 +747,13 @@
|
|||||||
if (!pt) return;
|
if (!pt) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
try {
|
const capturePointer = activeTool === "brush" || isDragTool(activeTool);
|
||||||
canvasEl.setPointerCapture(ev.pointerId);
|
if (capturePointer) {
|
||||||
brushPointerId = ev.pointerId;
|
try {
|
||||||
} catch (_) {}
|
canvasEl.setPointerCapture(ev.pointerId);
|
||||||
|
brushPointerId = ev.pointerId;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (activeTool === "brush") {
|
if (activeTool === "brush") {
|
||||||
draft = { type: "brush", points: [pt] };
|
draft = { type: "brush", points: [pt] };
|
||||||
@@ -572,8 +762,23 @@
|
|||||||
if (activeTool === "path") {
|
if (activeTool === "path") {
|
||||||
if (!draft || draft.type !== "path") {
|
if (!draft || draft.type !== "path") {
|
||||||
draft = { type: "path", points: [pt] };
|
draft = { type: "path", points: [pt] };
|
||||||
|
pathPreviewPt = pt;
|
||||||
} else {
|
} else {
|
||||||
draft.points.push(pt);
|
const last = draft.points[draft.points.length - 1];
|
||||||
|
const lx = timeToX(last.time);
|
||||||
|
const ly = priceToY(last.price);
|
||||||
|
const cx = timeToX(pt.time);
|
||||||
|
const cy = priceToY(pt.price);
|
||||||
|
if (
|
||||||
|
lx != null &&
|
||||||
|
ly != null &&
|
||||||
|
cx != null &&
|
||||||
|
cy != null &&
|
||||||
|
Math.hypot(cx - lx, cy - ly) > 4
|
||||||
|
) {
|
||||||
|
draft.points.push(pt);
|
||||||
|
}
|
||||||
|
pathPreviewPt = pt;
|
||||||
}
|
}
|
||||||
scheduleRedraw();
|
scheduleRedraw();
|
||||||
return;
|
return;
|
||||||
@@ -634,6 +839,14 @@
|
|||||||
draft.points[1] = pt;
|
draft.points[1] = pt;
|
||||||
scheduleRedraw();
|
scheduleRedraw();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeTool === "path" && draft && draft.type === "path") {
|
||||||
|
const pt = xyToPoint(loc.x, loc.y);
|
||||||
|
if (!pt) return;
|
||||||
|
pathPreviewPt = pt;
|
||||||
|
scheduleRedraw();
|
||||||
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,12 +884,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onDblClick(ev) {
|
function onDblClick(ev) {
|
||||||
if (activeTool === "path" && draft && draft.type === "path" && draft.points.length > 1) {
|
if (activeTool === "path" && draft && draft.type === "path") {
|
||||||
commitDrawing(draft);
|
finishPath();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onContextMenu(ev) {
|
||||||
|
if (activeTool !== "path" || !draft || draft.type !== "path") return;
|
||||||
|
ev.preventDefault();
|
||||||
|
finishPath();
|
||||||
|
}
|
||||||
|
|
||||||
function distSeg(px, py, x1, y1, x2, y2) {
|
function distSeg(px, py, x1, y1, x2, y2) {
|
||||||
const dx = x2 - x1;
|
const dx = x2 - x1;
|
||||||
const dy = y2 - y1;
|
const dy = y2 - y1;
|
||||||
@@ -717,18 +936,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
case "rect":
|
case "path":
|
||||||
|
case "brush":
|
||||||
if (pts.length >= 2) {
|
if (pts.length >= 2) {
|
||||||
const x1 = timeToX(pts[0].time);
|
for (let i = 1; i < pts.length; i++) {
|
||||||
const y1 = priceToY(pts[0].price);
|
const x1 = timeToX(pts[i - 1].time);
|
||||||
const x2 = timeToX(pts[1].time);
|
const y1 = priceToY(pts[i - 1].price);
|
||||||
const y2 = priceToY(pts[1].price);
|
const x2 = timeToX(pts[i].time);
|
||||||
if (x1 == null || y1 == null || x2 == null || y2 == null) return false;
|
const y2 = priceToY(pts[i].price);
|
||||||
const l = Math.min(x1, x2) - HIT_PX;
|
if (x1 != null && y1 != null && x2 != null && y2 != null) {
|
||||||
const r = Math.max(x1, x2) + HIT_PX;
|
if (distSeg(x, y, x1, y1, x2, y2) <= HIT_PX) return true;
|
||||||
const t = Math.min(y1, y2) - HIT_PX;
|
}
|
||||||
const b = Math.max(y1, y2) + HIT_PX;
|
}
|
||||||
return x >= l && x <= r && y >= t && y <= b;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
case "text": {
|
case "text": {
|
||||||
@@ -790,6 +1009,7 @@
|
|||||||
activeTool = tool;
|
activeTool = tool;
|
||||||
dragActive = false;
|
dragActive = false;
|
||||||
dragStartPx = null;
|
dragStartPx = null;
|
||||||
|
pathPreviewPt = null;
|
||||||
draft = null;
|
draft = null;
|
||||||
if (toolbarEl) {
|
if (toolbarEl) {
|
||||||
toolbarEl.querySelectorAll("[data-tool]").forEach(function (btn) {
|
toolbarEl.querySelectorAll("[data-tool]").forEach(function (btn) {
|
||||||
@@ -820,11 +1040,18 @@
|
|||||||
canvasEl.addEventListener("pointerup", onPointerUp);
|
canvasEl.addEventListener("pointerup", onPointerUp);
|
||||||
canvasEl.addEventListener("pointercancel", onPointerUp);
|
canvasEl.addEventListener("pointercancel", onPointerUp);
|
||||||
canvasEl.addEventListener("dblclick", onDblClick);
|
canvasEl.addEventListener("dblclick", onDblClick);
|
||||||
|
canvasEl.addEventListener("contextmenu", onContextMenu);
|
||||||
document.addEventListener("keydown", function (ev) {
|
document.addEventListener("keydown", function (ev) {
|
||||||
if (ev.key !== "Escape") return;
|
|
||||||
const page = document.getElementById("page-market");
|
const page = document.getElementById("page-market");
|
||||||
if (!page || page.classList.contains("hidden")) return;
|
if (!page || page.classList.contains("hidden")) return;
|
||||||
if (draft || dragActive) cancelDraft();
|
if (ev.key === "Escape" && (draft || dragActive)) {
|
||||||
|
cancelDraft();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.key === "Enter" && activeTool === "path" && draft && draft.type === "path") {
|
||||||
|
finishPath();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,6 +1086,7 @@
|
|||||||
selectedId = null;
|
selectedId = null;
|
||||||
dragActive = false;
|
dragActive = false;
|
||||||
dragStartPx = null;
|
dragStartPx = null;
|
||||||
|
pathPreviewPt = null;
|
||||||
draft = null;
|
draft = null;
|
||||||
scheduleRedraw();
|
scheduleRedraw();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||||
<link rel="stylesheet" href="/assets/app.css?v=20260608-market-draw-v3" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260608-market-draw-v4" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
<button type="button" class="market-draw-btn" data-tool="trend" title="趋势线(按住拖动)">
|
<button type="button" class="market-draw-btn" data-tool="trend" title="趋势线(按住拖动)">
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.8" d="M5 18L19 6"/><circle cx="5" cy="18" r="1.5" fill="currentColor"/><circle cx="19" cy="6" r="1.5" fill="currentColor"/></svg>
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.8" d="M5 18L19 6"/><circle cx="5" cy="18" r="1.5" fill="currentColor"/><circle cx="19" cy="6" r="1.5" fill="currentColor"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="market-draw-btn" data-tool="path" title="折线箭头(双击结束)">
|
<button type="button" class="market-draw-btn" data-tool="path" title="折线(连续点击,双击/右键/回车结束)">
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" d="M5 16L10 12L14 15L19 7"/><path fill="currentColor" d="M17 5l3 3-3 1z"/></svg>
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" d="M5 16L10 12L14 15L19 7"/><path fill="currentColor" d="M17 5l3 3-3 1z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="market-draw-sep" aria-hidden="true"></span>
|
<span class="market-draw-sep" aria-hidden="true"></span>
|
||||||
@@ -392,8 +392,8 @@
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart_draw.js?v=20260608-market-draw-v3"></script>
|
<script src="/assets/chart_draw.js?v=20260608-market-draw-v4"></script>
|
||||||
<script src="/assets/chart.js?v=20260608-market-draw-v3"></script>
|
<script src="/assets/chart.js?v=20260608-market-draw-v4"></script>
|
||||||
<script src="/assets/archive.js?v=20260607-hub-archive-v6"></script>
|
<script src="/assets/archive.js?v=20260607-hub-archive-v6"></script>
|
||||||
<script src="/assets/ai_review_render.js?v=2"></script>
|
<script src="/assets/ai_review_render.js?v=2"></script>
|
||||||
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script>
|
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user