Compare commits
2 Commits
cd2ca2e220
...
709a565397
| Author | SHA1 | Date | |
|---|---|---|---|
| 709a565397 | |||
| 07a27a12a4 |
@ -101,7 +101,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Chart Container -->
|
<!-- Chart Container -->
|
||||||
<section class="relative w-full bg-[#0d1421] h-[60vh] md:h-[70vh]" data-purpose="chart-container" id="chartWrapper">
|
<section class="relative w-full bg-[#0d1421] h-[60vh] md:h-[70vh]" data-purpose="chart-container" id="chartWrapper">
|
||||||
<div id="chart" class="w-full h-full"></div>
|
<div id="chart" class="w-full h-full"></div>
|
||||||
|
|
||||||
|
|||||||
364
js/ui/chart.js
364
js/ui/chart.js
@ -149,10 +149,8 @@ function throttle(func, limit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import { DrawingManager } from './drawing-tools.js';
|
|
||||||
|
|
||||||
export class TradingDashboard {
|
export class TradingDashboard {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.chart = null;
|
this.chart = null;
|
||||||
this.candleSeries = null;
|
this.candleSeries = null;
|
||||||
// Load settings from local storage or defaults
|
// Load settings from local storage or defaults
|
||||||
@ -171,13 +169,14 @@ export class TradingDashboard {
|
|||||||
this.avgPriceSeries = null;
|
this.avgPriceSeries = null;
|
||||||
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
|
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
|
||||||
this.currentMouseTime = null;
|
this.currentMouseTime = null;
|
||||||
this.drawingManager = null;
|
this.drawingItems = []; // Store drawing items for management
|
||||||
|
|
||||||
// Throttled versions of heavy functions
|
// Throttled versions of heavy functions
|
||||||
this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
|
this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadDailyMAData() {
|
async loadDailyMAData() {
|
||||||
try {
|
try {
|
||||||
// Use 1d interval for this calculation
|
// Use 1d interval for this calculation
|
||||||
@ -394,7 +393,7 @@ export class TradingDashboard {
|
|||||||
if (candleDownInput) candleDownInput.value = savedDownColor;
|
if (candleDownInput) candleDownInput.value = savedDownColor;
|
||||||
|
|
||||||
// Calculate initial minMove based on saved precision
|
// Calculate initial minMove based on saved precision
|
||||||
const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(precision));
|
const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision));
|
||||||
|
|
||||||
this.candleSeries = this.chart.addSeries(LightweightCharts.CandlestickSeries, {
|
this.candleSeries = this.chart.addSeries(LightweightCharts.CandlestickSeries, {
|
||||||
upColor: savedUpColor,
|
upColor: savedUpColor,
|
||||||
@ -455,14 +454,6 @@ export class TradingDashboard {
|
|||||||
|
|
||||||
this.initPriceScaleControls();
|
this.initPriceScaleControls();
|
||||||
this.initNavigationControls();
|
this.initNavigationControls();
|
||||||
|
|
||||||
// Initialize Drawing Manager
|
|
||||||
const drawingLayer = document.getElementById('drawingLayer');
|
|
||||||
if (drawingLayer) {
|
|
||||||
this.drawingManager = new DrawingManager(this.chart, this.candleSeries, drawingLayer, chartContainer);
|
|
||||||
window.activateDrawingTool = (tool) => this.drawingManager.setTool(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup price format selector change handler
|
// Setup price format selector change handler
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const priceSelect = document.getElementById("priceFormatSelect");
|
const priceSelect = document.getElementById("priceFormatSelect");
|
||||||
@ -511,6 +502,8 @@ export class TradingDashboard {
|
|||||||
initPriceScaleControls() {
|
initPriceScaleControls() {
|
||||||
const btnSettings = document.getElementById('btnSettings');
|
const btnSettings = document.getElementById('btnSettings');
|
||||||
const settingsPopup = document.getElementById('settingsPopup');
|
const settingsPopup = document.getElementById('settingsPopup');
|
||||||
|
const toggleToolbarBtn = document.getElementById('toggleChartToolbar');
|
||||||
|
const chartToolbar = document.getElementById('chartToolbar');
|
||||||
|
|
||||||
// Settings Popup Toggle and Outside Click
|
// Settings Popup Toggle and Outside Click
|
||||||
if (btnSettings && settingsPopup) {
|
if (btnSettings && settingsPopup) {
|
||||||
@ -532,6 +525,16 @@ export class TradingDashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle Chart Toolbar
|
||||||
|
if (toggleToolbarBtn && chartToolbar) {
|
||||||
|
toggleToolbarBtn.addEventListener('click', () => {
|
||||||
|
chartToolbar.style.display = chartToolbar.style.display === 'flex' ? 'none' : 'flex';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawing Tools
|
||||||
|
this.initDrawingTools();
|
||||||
|
|
||||||
// Initialize state from storage
|
// Initialize state from storage
|
||||||
this.scaleState = {
|
this.scaleState = {
|
||||||
autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false',
|
autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false',
|
||||||
@ -589,6 +592,341 @@ export class TradingDashboard {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initDrawingTools() {
|
||||||
|
const btnDrawingTools = document.getElementById('btnDrawingTools');
|
||||||
|
const drawingToolsPopup = document.getElementById('drawingToolsPopup');
|
||||||
|
|
||||||
|
if (btnDrawingTools && drawingToolsPopup) {
|
||||||
|
btnDrawingTools.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
drawingToolsPopup.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', closeDrawingToolsPopup);
|
||||||
|
document.addEventListener('touchstart', closeDrawingToolsPopup, { passive: true });
|
||||||
|
|
||||||
|
function closeDrawingToolsPopup(e) {
|
||||||
|
const isInside = drawingToolsPopup.contains(e.target) || e.target === btnDrawingTools;
|
||||||
|
const isDrawingButton = e.target.closest('#btnDrawingTools');
|
||||||
|
|
||||||
|
if (!isInside && !isDrawingButton) {
|
||||||
|
drawingToolsPopup.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isDrawing = false;
|
||||||
|
let drawMode = null;
|
||||||
|
let startPoint = null;
|
||||||
|
let drawLine = null;
|
||||||
|
let drawArrow = null;
|
||||||
|
let drawText = null;
|
||||||
|
|
||||||
|
window.activateDrawingTool = (tool) => {
|
||||||
|
if (tool === 'clear') {
|
||||||
|
this.clearDrawingTools();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawMode = tool;
|
||||||
|
isDrawing = true;
|
||||||
|
|
||||||
|
const chart = this.chart;
|
||||||
|
const container = chart.container();
|
||||||
|
|
||||||
|
const onMouseDown = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
if (drawMode === 'trend_line' || drawMode === 'horizontal_line' || drawMode === 'vertical_line') {
|
||||||
|
this.createLine(x, y);
|
||||||
|
} else if (drawMode === 'arrow_up' || drawMode === 'arrow_down') {
|
||||||
|
this.createArrow(drawMode === 'arrow_up' ? 'arrowUp' : 'arrowDown', x, y);
|
||||||
|
} else if (drawMode === 'text') {
|
||||||
|
this.createText(x, y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchStart = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const x = touch.clientX - rect.left;
|
||||||
|
const y = touch.clientY - rect.top;
|
||||||
|
|
||||||
|
if (drawMode === 'trend_line' || drawMode === 'horizontal_line' || drawMode === 'vertical_line') {
|
||||||
|
this.createLine(x, y);
|
||||||
|
} else if (drawMode === 'arrow_up' || drawMode === 'arrow_down') {
|
||||||
|
this.createArrow(drawMode === 'arrow_up' ? 'arrowUp' : 'arrowDown', x, y);
|
||||||
|
} else if (drawMode === 'text') {
|
||||||
|
this.createText(x, y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener('mousedown', onMouseDown);
|
||||||
|
container.addEventListener('touchstart', onTouchStart);
|
||||||
|
|
||||||
|
const closeDrawing = () => {
|
||||||
|
container.removeEventListener('mousedown', onMouseDown);
|
||||||
|
container.removeEventListener('touchstart', onTouchStart);
|
||||||
|
document.removeEventListener('click', closeDrawing);
|
||||||
|
document.removeEventListener('touchstart', closeDrawing);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', closeDrawing);
|
||||||
|
document.addEventListener('touchstart', closeDrawing);
|
||||||
|
|
||||||
|
drawingToolsPopup.classList.add('hidden');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createLine(x, y) {
|
||||||
|
const chart = this.chart;
|
||||||
|
const candleSeries = this.candleSeries;
|
||||||
|
|
||||||
|
let lineData = {
|
||||||
|
time: [],
|
||||||
|
value: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const line = chart.addLineSeries({
|
||||||
|
color: '#2962ff',
|
||||||
|
lineWidth: 2,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||||
|
crosshairMarkerVisible: true,
|
||||||
|
crosshairMarkerRadius: 4,
|
||||||
|
baseIndex: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let points = [];
|
||||||
|
|
||||||
|
const container = chart.container();
|
||||||
|
let startTime = null;
|
||||||
|
let startPrice = null;
|
||||||
|
|
||||||
|
const onMouseDown = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const clientX = e.clientX;
|
||||||
|
const clientY = e.clientY;
|
||||||
|
|
||||||
|
const timeCoord = chart.timeScale().coordinateToTime(x);
|
||||||
|
const price = chart.priceScale().coordinateToPrice(y);
|
||||||
|
|
||||||
|
if (startTime === null) {
|
||||||
|
startTime = timeCoord;
|
||||||
|
startPrice = price;
|
||||||
|
|
||||||
|
line.setData([{
|
||||||
|
time: startTime,
|
||||||
|
value: startPrice
|
||||||
|
}]);
|
||||||
|
} else {
|
||||||
|
line.setData([
|
||||||
|
{ time: startTime, value: startPrice },
|
||||||
|
{ time: timeCoord, value: price }
|
||||||
|
]);
|
||||||
|
startTime = null;
|
||||||
|
startPrice = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchStart = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const clientX = touch.clientX;
|
||||||
|
const clientY = touch.clientY;
|
||||||
|
|
||||||
|
const timeCoord = chart.timeScale().coordinateToTime(x);
|
||||||
|
const price = chart.priceScale().coordinateToPrice(y);
|
||||||
|
|
||||||
|
if (startTime === null) {
|
||||||
|
startTime = timeCoord;
|
||||||
|
startPrice = price;
|
||||||
|
|
||||||
|
line.setData([{
|
||||||
|
time: startTime,
|
||||||
|
value: startPrice
|
||||||
|
}]);
|
||||||
|
} else {
|
||||||
|
line.setData([
|
||||||
|
{ time: startTime, value: startPrice },
|
||||||
|
{ time: timeCoord, value: price }
|
||||||
|
]);
|
||||||
|
startTime = null;
|
||||||
|
startPrice = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener('mousedown', onMouseDown);
|
||||||
|
container.addEventListener('touchstart', onTouchStart);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
container.removeEventListener('mousedown', onMouseDown);
|
||||||
|
container.removeEventListener('touchstart', onTouchStart);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', cleanup);
|
||||||
|
document.addEventListener('touchstart', cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
createArrow(arrowType, x, y) {
|
||||||
|
const chart = this.chart;
|
||||||
|
const container = chart.container();
|
||||||
|
|
||||||
|
const markerPrimitive = new SeriesMarkersPrimitive();
|
||||||
|
this.candleSeries.attachPrimitive(markerPrimitive);
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const time = chart.timeScale().coordinateToTime(x);
|
||||||
|
const price = chart.priceScale().coordinateToPrice(y);
|
||||||
|
|
||||||
|
const marker = {
|
||||||
|
time: time,
|
||||||
|
position: arrowType === 'arrowUp' ? 'belowBar' : 'aboveBar',
|
||||||
|
color: arrowType === 'arrowUp' ? '#26a69a' : '#ef5350',
|
||||||
|
shape: arrowType === 'arrowUp' ? 'arrowUp' : 'arrowDown',
|
||||||
|
text: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
markerPrimitive.setMarkers([marker]);
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let startTime = time;
|
||||||
|
let startPrice = price;
|
||||||
|
|
||||||
|
const onMouseDown = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
isDragging = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const dx = e.clientX - startX;
|
||||||
|
const dy = e.clientY - startY;
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
const timeCoord = chart.timeScale().coordinateToTime(x + dx);
|
||||||
|
const newPrice = chart.priceScale().coordinateToPrice(y + dy);
|
||||||
|
|
||||||
|
const newMarker = {
|
||||||
|
time: timeCoord,
|
||||||
|
position: arrowType === 'arrowUp' ? 'belowBar' : 'aboveBar',
|
||||||
|
color: arrowType === 'arrowUp' ? '#26a69a' : '#ef5350',
|
||||||
|
shape: arrowType === 'arrowUp' ? 'arrowUp' : 'arrowDown',
|
||||||
|
text: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
markerPrimitive.setMarkers([newMarker]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDragging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener('mousedown', onMouseDown);
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
container.removeEventListener('mousedown', onMouseDown);
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
document.removeEventListener('click', cleanup);
|
||||||
|
document.removeEventListener('touchstart', cleanup);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', cleanup);
|
||||||
|
document.addEventListener('touchstart', cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
createText(x, y) {
|
||||||
|
const chart = this.chart;
|
||||||
|
const container = chart.container();
|
||||||
|
|
||||||
|
const text = prompt('Enter text label:', '');
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const time = chart.timeScale().coordinateToTime(x);
|
||||||
|
const price = chart.priceScale().coordinateToPrice(y);
|
||||||
|
|
||||||
|
const label = chart.addLabel({
|
||||||
|
text: text,
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 11,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
crosshairMarkerVisible: false,
|
||||||
|
time: time,
|
||||||
|
price: price
|
||||||
|
});
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let startTime = time;
|
||||||
|
let startPrice = price;
|
||||||
|
|
||||||
|
const onMouseDown = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
isDragging = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const dx = e.clientX - startX;
|
||||||
|
const dy = e.clientY - startY;
|
||||||
|
|
||||||
|
const newTime = chart.timeScale().coordinateToTime(x + dx);
|
||||||
|
const newPrice = chart.priceScale().coordinateToPrice(y + dy);
|
||||||
|
|
||||||
|
label.applyOptions({
|
||||||
|
time: newTime,
|
||||||
|
price: newPrice
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDragging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener('mousedown', onMouseDown);
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
container.removeEventListener('mousedown', onMouseDown);
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
document.removeEventListener('click', cleanup);
|
||||||
|
document.removeEventListener('touchstart', cleanup);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', cleanup);
|
||||||
|
document.addEventListener('touchstart', cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDrawingTools() {
|
||||||
|
const allSeries = this.chart.series();
|
||||||
|
allSeries.forEach(series => {
|
||||||
|
if (series.type !== 'Candlestick' && series.type !== 'Line' && series.type !== 'Area') {
|
||||||
|
this.chart.removeSeries(series);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
initNavigationControls() {
|
initNavigationControls() {
|
||||||
const chartWrapper = document.getElementById('chartWrapper');
|
const chartWrapper = document.getElementById('chartWrapper');
|
||||||
const navLeft = document.getElementById('navLeft');
|
const navLeft = document.getElementById('navLeft');
|
||||||
|
|||||||
@ -1,483 +0,0 @@
|
|||||||
/**
|
|
||||||
* Drawing Tools for Lightweight Charts
|
|
||||||
* Implements professional trading chart drawing primitives
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class DrawingManager {
|
|
||||||
constructor(chart, series, canvas, container) {
|
|
||||||
this.chart = chart;
|
|
||||||
this.series = series;
|
|
||||||
this.canvas = canvas;
|
|
||||||
this.container = container;
|
|
||||||
this.ctx = canvas.getContext('2d');
|
|
||||||
this.drawings = [];
|
|
||||||
this.activeTool = null;
|
|
||||||
this.currentDrawing = null;
|
|
||||||
this.selectedDrawing = null;
|
|
||||||
this.dragMode = null;
|
|
||||||
this.isMouseDown = false;
|
|
||||||
|
|
||||||
// Dragging offsets
|
|
||||||
this.dragStartPos = null;
|
|
||||||
this.startP1 = null;
|
|
||||||
this.startP2 = null;
|
|
||||||
this.startPrice = null;
|
|
||||||
this.startTime = null;
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
const container = this.container;
|
|
||||||
|
|
||||||
container.addEventListener('mousedown', (e) => this.handleMouseDown(e));
|
|
||||||
window.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
|
||||||
window.addEventListener('mouseup', (e) => this.handleMouseUp(e));
|
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
|
||||||
if ((e.key === 'Delete' || e.key === 'Backspace') && this.selectedDrawing) {
|
|
||||||
this.drawings = this.drawings.filter(d => d !== this.selectedDrawing);
|
|
||||||
this.selectedDrawing = null;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.drawingPrimitive = {
|
|
||||||
attached: (param) => {
|
|
||||||
this.requestUpdate = param.requestUpdate;
|
|
||||||
},
|
|
||||||
detached: () => {
|
|
||||||
this.requestUpdate = undefined;
|
|
||||||
},
|
|
||||||
updateAllViews: () => {},
|
|
||||||
paneViews: () => [{
|
|
||||||
renderer: () => ({
|
|
||||||
draw: (target) => this.render(target)
|
|
||||||
})
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
this.series.attachPrimitive(this.drawingPrimitive);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateChartInteractions() {
|
|
||||||
// Disable chart interactions if a tool is active, if we are currently drawing,
|
|
||||||
// or if we are dragging an existing object.
|
|
||||||
const isInteracting = this.activeTool !== null || this.currentDrawing !== null || (this.selectedDrawing !== null && this.isMouseDown);
|
|
||||||
|
|
||||||
this.chart.applyOptions({
|
|
||||||
handleScroll: {
|
|
||||||
mouseWheel: !isInteracting,
|
|
||||||
pressedMouseMove: !isInteracting,
|
|
||||||
horzTouchDrag: !isInteracting,
|
|
||||||
vertTouchDrag: !isInteracting,
|
|
||||||
},
|
|
||||||
handleScale: {
|
|
||||||
axisPressedMouseMove: !isInteracting,
|
|
||||||
mouseWheel: !isInteracting,
|
|
||||||
pinch: !isInteracting,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setTool(tool) {
|
|
||||||
this.activeTool = tool;
|
|
||||||
|
|
||||||
if (tool === 'cursor') {
|
|
||||||
this.activeTool = null;
|
|
||||||
document.body.style.cursor = 'default';
|
|
||||||
} else if (tool === 'clear') {
|
|
||||||
this.drawings = [];
|
|
||||||
this.selectedDrawing = null;
|
|
||||||
this.activeTool = null;
|
|
||||||
this.update();
|
|
||||||
} else {
|
|
||||||
document.body.style.cursor = 'crosshair';
|
|
||||||
this.selectedDrawing = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateChartInteractions();
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMousePos(e) {
|
|
||||||
const rect = this.container.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
const time = this.chart.timeScale().coordinateToTime(x);
|
|
||||||
const price = this.series.coordinateToPrice(y);
|
|
||||||
return { x, y, time, price };
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseDown(e) {
|
|
||||||
const pos = this.getMousePos(e);
|
|
||||||
this.isMouseDown = true;
|
|
||||||
this.dragStartPos = pos;
|
|
||||||
|
|
||||||
// Shift + Click shortcut for Measurement Tool
|
|
||||||
if (e.shiftKey && !this.activeTool) {
|
|
||||||
this.setTool('measure');
|
|
||||||
this.currentDrawing = {
|
|
||||||
type: 'measure',
|
|
||||||
p1: { time: pos.time, price: pos.price },
|
|
||||||
p2: { time: pos.time, price: pos.price },
|
|
||||||
color: '#2962ff'
|
|
||||||
};
|
|
||||||
this.update();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click-Move-Click logic for Measurement Tool
|
|
||||||
if (this.activeTool === 'measure') {
|
|
||||||
if (this.currentDrawing) {
|
|
||||||
// 2nd Click: Finish
|
|
||||||
this.drawings.push(this.currentDrawing);
|
|
||||||
this.currentDrawing = null;
|
|
||||||
this.setTool(null);
|
|
||||||
this.isMouseDown = false;
|
|
||||||
this.update();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// 1st Click: Start
|
|
||||||
this.currentDrawing = {
|
|
||||||
type: 'measure',
|
|
||||||
p1: { time: pos.time, price: pos.price },
|
|
||||||
p2: { time: pos.time, price: pos.price },
|
|
||||||
color: '#2962ff'
|
|
||||||
};
|
|
||||||
this.update();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.activeTool) {
|
|
||||||
const hit = this.findHit(pos.x, pos.y);
|
|
||||||
if (hit) {
|
|
||||||
this.selectedDrawing = hit.drawing;
|
|
||||||
this.dragMode = hit.part;
|
|
||||||
|
|
||||||
// Store initial state for offset movement
|
|
||||||
const d = this.selectedDrawing;
|
|
||||||
if (d.p1) this.startP1 = { ...d.p1 };
|
|
||||||
if (d.p2) this.startP2 = { ...d.p2 };
|
|
||||||
if (d.price !== undefined) this.startPrice = d.price;
|
|
||||||
if (d.time !== undefined) this.startTime = d.time;
|
|
||||||
|
|
||||||
this.updateChartInteractions(); // Freeze chart for dragging
|
|
||||||
this.update();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this.selectedDrawing = null;
|
|
||||||
this.dragMode = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.activeTool || pos.time === null || pos.price === null) {
|
|
||||||
this.update();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = '#2962ff';
|
|
||||||
const p1 = { time: pos.time, price: pos.price };
|
|
||||||
const p2 = { time: pos.time, price: pos.price };
|
|
||||||
|
|
||||||
if (this.activeTool === 'trend_line' || this.activeTool === 'ray' || this.activeTool === 'rectangle') {
|
|
||||||
this.currentDrawing = { type: this.activeTool, p1, p2, color };
|
|
||||||
} else if (this.activeTool === 'horizontal_line') {
|
|
||||||
this.drawings.push({ type: 'horizontal_line', price: pos.price, color });
|
|
||||||
this.setTool(null);
|
|
||||||
} else if (this.activeTool === 'vertical_line') {
|
|
||||||
this.drawings.push({ type: 'vertical_line', time: pos.time, color });
|
|
||||||
this.setTool(null);
|
|
||||||
} else if (this.activeTool === 'fib_retracement') {
|
|
||||||
this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' };
|
|
||||||
} else if (this.activeTool === 'measure') {
|
|
||||||
this.currentDrawing = { type: 'measure', p1, p2, color };
|
|
||||||
} else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down') {
|
|
||||||
this.drawings.push({
|
|
||||||
type: 'arrow', time: pos.time, price: pos.price,
|
|
||||||
direction: this.activeTool === 'arrow_up' ? 'up' : 'down',
|
|
||||||
color: this.activeTool === 'arrow_up' ? '#26a69a' : '#ef5350'
|
|
||||||
});
|
|
||||||
this.setTool(null);
|
|
||||||
} else if (this.activeTool === 'text') {
|
|
||||||
const text = prompt('Enter text:');
|
|
||||||
if (text) this.drawings.push({ type: 'text', time: pos.time, price: pos.price, text, color: '#ffffff' });
|
|
||||||
this.setTool(null);
|
|
||||||
}
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseMove(e) {
|
|
||||||
const pos = this.getMousePos(e);
|
|
||||||
if (!this.isMouseDown && !this.activeTool) {
|
|
||||||
const hit = this.findHit(pos.x, pos.y);
|
|
||||||
this.container.style.cursor = hit ? 'pointer' : 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track measurement even if mouse is up (Click-Move-Click)
|
|
||||||
if (this.currentDrawing && this.currentDrawing.type === 'measure') {
|
|
||||||
if (pos.time !== null) {
|
|
||||||
this.currentDrawing.p2 = { time: pos.time, price: pos.price };
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMouseDown) return;
|
|
||||||
|
|
||||||
if (this.selectedDrawing && this.dragMode) {
|
|
||||||
if (pos.time === null || pos.price === null) return;
|
|
||||||
const d = this.selectedDrawing;
|
|
||||||
if (this.dragMode === 'p1') {
|
|
||||||
d.p1 = { time: pos.time, price: pos.price };
|
|
||||||
} else if (this.dragMode === 'p2') {
|
|
||||||
d.p2 = { time: pos.time, price: pos.price };
|
|
||||||
} else if (this.dragMode === 'all') {
|
|
||||||
const timeDiff = pos.time - this.dragStartPos.time;
|
|
||||||
const priceDiff = pos.price - this.dragStartPos.price;
|
|
||||||
if (d.p1 && d.p2) {
|
|
||||||
d.p1.time = this.startP1.time + timeDiff;
|
|
||||||
d.p1.price = this.startP1.price + priceDiff;
|
|
||||||
d.p2.time = this.startP2.time + timeDiff;
|
|
||||||
d.p2.price = this.startP2.price + priceDiff;
|
|
||||||
} else if (d.time !== undefined && d.price !== undefined) {
|
|
||||||
d.time = this.startTime + timeDiff;
|
|
||||||
d.price = this.startPrice + priceDiff;
|
|
||||||
} else if (d.price !== undefined) {
|
|
||||||
d.price = this.startPrice + priceDiff;
|
|
||||||
} else if (d.time !== undefined) {
|
|
||||||
d.time = this.startTime + timeDiff;
|
|
||||||
}
|
|
||||||
} else if (this.dragMode === 'price') {
|
|
||||||
d.price = pos.price;
|
|
||||||
} else if (this.dragMode === 'time') {
|
|
||||||
d.time = pos.time;
|
|
||||||
}
|
|
||||||
this.update();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentDrawing && pos.time !== null) {
|
|
||||||
this.currentDrawing.p2 = { time: pos.time, price: pos.price };
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseUp() {
|
|
||||||
// Special case for measure tool (Click-Move-Click)
|
|
||||||
if (this.currentDrawing && this.currentDrawing.type === 'measure') {
|
|
||||||
this.isMouseDown = false;
|
|
||||||
this.updateChartInteractions();
|
|
||||||
this.update();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentDrawing) {
|
|
||||||
this.drawings.push(this.currentDrawing);
|
|
||||||
this.currentDrawing = null;
|
|
||||||
this.setTool(null);
|
|
||||||
}
|
|
||||||
this.dragMode = null;
|
|
||||||
this.isMouseDown = false;
|
|
||||||
this.updateChartInteractions(); // Ensure interactions are restored (panning/zooming)
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
findHit(x, y) {
|
|
||||||
const threshold = 10;
|
|
||||||
for (let i = this.drawings.length - 1; i >= 0; i--) {
|
|
||||||
const d = this.drawings[i];
|
|
||||||
if (d.p1 && d.p2) {
|
|
||||||
const x1 = this.chart.timeScale().timeToCoordinate(d.p1.time);
|
|
||||||
const y1 = this.series.priceToCoordinate(d.p1.price);
|
|
||||||
const x2 = this.chart.timeScale().timeToCoordinate(d.p2.time);
|
|
||||||
const y2 = this.series.priceToCoordinate(d.p2.price);
|
|
||||||
if (x1 === null || y1 === null || x2 === null || y2 === null) continue;
|
|
||||||
if (Math.hypot(x - x1, y - y1) < threshold) return { drawing: d, part: 'p1' };
|
|
||||||
if (Math.hypot(x - x2, y - y2) < threshold) return { drawing: d, part: 'p2' };
|
|
||||||
|
|
||||||
if (d.type === 'rectangle') {
|
|
||||||
if (x >= Math.min(x1, x2) && x <= Math.max(x1, x2) && y >= Math.min(y1, y2) && y <= Math.max(y1, y2)) return { drawing: d, part: 'all' };
|
|
||||||
} else {
|
|
||||||
const dist = this.distToSegment({ x, y }, { x: x1, y: y1 }, { x: x2, y: y2 });
|
|
||||||
if (dist < threshold) return { drawing: d, part: 'all' };
|
|
||||||
}
|
|
||||||
} else if (d.price !== undefined && d.type === 'horizontal_line') {
|
|
||||||
const dy = this.series.priceToCoordinate(d.price);
|
|
||||||
if (Math.abs(y - dy) < threshold) return { drawing: d, part: 'price' };
|
|
||||||
} else if (d.time !== undefined && d.type === 'vertical_line') {
|
|
||||||
const dx = this.chart.timeScale().timeToCoordinate(d.time);
|
|
||||||
if (Math.abs(x - dx) < threshold) return { drawing: d, part: 'time' };
|
|
||||||
} else if (d.time !== undefined && d.price !== undefined) {
|
|
||||||
const dx = this.chart.timeScale().timeToCoordinate(d.time);
|
|
||||||
const dy = this.series.priceToCoordinate(d.price);
|
|
||||||
if (Math.hypot(x - dx, y - dy) < threshold) return { drawing: d, part: 'all' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
distToSegment(p, v, w) {
|
|
||||||
const l2 = Math.pow(v.x - w.x, 2) + Math.pow(v.y - w.y, 2);
|
|
||||||
if (l2 === 0) return Math.hypot(p.x - v.x, p.y - v.y);
|
|
||||||
let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
|
|
||||||
t = Math.max(0, Math.min(1, t));
|
|
||||||
return Math.hypot(p.x - (v.x + t * (w.x - v.x)), p.y - (v.y + t * (w.y - v.y)));
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (this.requestUpdate) this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
render(target) {
|
|
||||||
target.useMediaCoordinateSpace((scope) => {
|
|
||||||
const ctx = scope.context;
|
|
||||||
const chart = this.chart;
|
|
||||||
const series = this.series;
|
|
||||||
const allDrawings = [...this.drawings];
|
|
||||||
if (this.currentDrawing) allDrawings.push(this.currentDrawing);
|
|
||||||
|
|
||||||
allDrawings.forEach(d => {
|
|
||||||
const isSelected = d === this.selectedDrawing;
|
|
||||||
ctx.save();
|
|
||||||
ctx.strokeStyle = d.color;
|
|
||||||
ctx.lineWidth = isSelected ? 3 : 2;
|
|
||||||
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
|
|
||||||
|
|
||||||
if (d.type === 'trend_line' || d.type === 'ray') {
|
|
||||||
const x1 = chart.timeScale().timeToCoordinate(d.p1.time);
|
|
||||||
const y1 = series.priceToCoordinate(d.p1.price);
|
|
||||||
const x2 = chart.timeScale().timeToCoordinate(d.p2.time);
|
|
||||||
const y2 = series.priceToCoordinate(d.p2.price);
|
|
||||||
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x1, y1);
|
|
||||||
if (d.type === 'ray') {
|
|
||||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
||||||
ctx.lineTo(x1 + Math.cos(angle) * 10000, y1 + Math.sin(angle) * 10000);
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x2, y2);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
if (isSelected) {
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
||||||
ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (d.type === 'rectangle') {
|
|
||||||
const x1 = chart.timeScale().timeToCoordinate(d.p1.time);
|
|
||||||
const y1 = series.priceToCoordinate(d.p1.price);
|
|
||||||
const x2 = chart.timeScale().timeToCoordinate(d.p2.time);
|
|
||||||
const y2 = series.priceToCoordinate(d.p2.price);
|
|
||||||
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
|
|
||||||
ctx.fillStyle = d.color + '22';
|
|
||||||
ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
|
|
||||||
ctx.strokeRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
|
|
||||||
if (isSelected) {
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
||||||
ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (d.type === 'horizontal_line') {
|
|
||||||
const y = series.priceToCoordinate(d.price);
|
|
||||||
if (y !== null) {
|
|
||||||
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(scope.mediaSize.width, y); ctx.stroke();
|
|
||||||
}
|
|
||||||
} else if (d.type === 'vertical_line') {
|
|
||||||
const x = chart.timeScale().timeToCoordinate(d.time);
|
|
||||||
if (x !== null) {
|
|
||||||
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, scope.mediaSize.height); ctx.stroke();
|
|
||||||
}
|
|
||||||
} else if (d.type === 'fib_retracement') {
|
|
||||||
const x1 = chart.timeScale().timeToCoordinate(d.p1.time);
|
|
||||||
const y1 = series.priceToCoordinate(d.p1.price);
|
|
||||||
const x2 = chart.timeScale().timeToCoordinate(d.p2.time);
|
|
||||||
const y2 = series.priceToCoordinate(d.p2.price);
|
|
||||||
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
|
|
||||||
const levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
||||||
const range = d.p2.price - d.p1.price;
|
|
||||||
ctx.setLineDash([5, 5]); ctx.lineWidth = 1;
|
|
||||||
levels.forEach(level => {
|
|
||||||
const price = d.p1.price + (range * level);
|
|
||||||
const y = series.priceToCoordinate(price);
|
|
||||||
if (y !== null) {
|
|
||||||
ctx.beginPath(); ctx.moveTo(Math.min(x1, x2), y); ctx.lineTo(Math.max(x1, x2), y); ctx.stroke();
|
|
||||||
ctx.fillStyle = d.color; ctx.font = '10px Inter';
|
|
||||||
ctx.fillText(`${(level * 100).toFixed(1)}% (${price.toFixed(2)})`, Math.max(x1, x2) + 5, y + 3);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ctx.setLineDash([]); ctx.strokeStyle = d.color + '44';
|
|
||||||
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
|
|
||||||
if (isSelected) {
|
|
||||||
ctx.fillStyle = '#ffffff'; ctx.strokeStyle = d.color; ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
||||||
ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (d.type === 'arrow') {
|
|
||||||
const x = chart.timeScale().timeToCoordinate(d.time);
|
|
||||||
const y = series.priceToCoordinate(d.price);
|
|
||||||
if (x !== null && y !== null) {
|
|
||||||
ctx.fillStyle = d.color; ctx.beginPath();
|
|
||||||
const size = isSelected ? 15 : 10;
|
|
||||||
if (d.direction === 'up') {
|
|
||||||
ctx.moveTo(x, y); ctx.lineTo(x - size/2, y + size); ctx.lineTo(x + size/2, y + size);
|
|
||||||
} else {
|
|
||||||
ctx.moveTo(x, y); ctx.lineTo(x - size/2, y - size); ctx.lineTo(x + size/2, y - size);
|
|
||||||
}
|
|
||||||
ctx.fill();
|
|
||||||
if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); }
|
|
||||||
}
|
|
||||||
} else if (d.type === 'text') {
|
|
||||||
const x = chart.timeScale().timeToCoordinate(d.time);
|
|
||||||
const y = series.priceToCoordinate(d.price);
|
|
||||||
if (x !== null && y !== null) {
|
|
||||||
ctx.fillStyle = d.color;
|
|
||||||
ctx.font = isSelected ? 'bold 14px Inter' : '12px Inter';
|
|
||||||
ctx.fillText(d.text, x, y);
|
|
||||||
if (isSelected) {
|
|
||||||
const metrics = ctx.measureText(d.text);
|
|
||||||
ctx.strokeStyle = d.color;
|
|
||||||
ctx.strokeRect(x - 2, y - 12, metrics.width + 4, 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (d.type === 'measure') {
|
|
||||||
const x1 = chart.timeScale().timeToCoordinate(d.p1.time);
|
|
||||||
const y1 = series.priceToCoordinate(d.p1.price);
|
|
||||||
const x2 = chart.timeScale().timeToCoordinate(d.p2.time);
|
|
||||||
const y2 = series.priceToCoordinate(d.p2.price);
|
|
||||||
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
|
|
||||||
const priceDiff = d.p2.price - d.p1.price;
|
|
||||||
const percentChange = (priceDiff / d.p1.price) * 100;
|
|
||||||
const isPositive = priceDiff >= 0;
|
|
||||||
const measureColor = isPositive ? '#26a69a' : '#ef5350';
|
|
||||||
const timeSpanSeconds = Math.abs(d.p2.time - d.p1.time);
|
|
||||||
const days = (timeSpanSeconds / 86400).toFixed(1);
|
|
||||||
ctx.fillStyle = measureColor + '22';
|
|
||||||
ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
|
|
||||||
ctx.strokeStyle = measureColor;
|
|
||||||
ctx.lineWidth = isSelected ? 2 : 1;
|
|
||||||
ctx.strokeRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
|
|
||||||
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
|
|
||||||
const text = [`${priceDiff.toFixed(2)} (${percentChange.toFixed(2)}%)`, `${days} days`];
|
|
||||||
const padding = 5, lineHeight = 14, boxWidth = 100, boxHeight = text.length * lineHeight + padding * 2;
|
|
||||||
const boxX = x2 + 10, boxY = y2 - boxHeight / 2;
|
|
||||||
ctx.fillStyle = '#1a2333'; ctx.fillRect(boxX, boxY, boxWidth, boxHeight);
|
|
||||||
ctx.strokeStyle = measureColor; ctx.strokeRect(boxX, boxY, boxWidth, boxHeight);
|
|
||||||
ctx.fillStyle = '#ffffff'; ctx.font = '11px Inter';
|
|
||||||
text.forEach((line, i) => { ctx.fillText(line, boxX + padding, boxY + padding + 10 + (i * lineHeight)); });
|
|
||||||
if (isSelected) {
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
||||||
ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.restore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user