From 5b6b134c16f8d2a578eba4b4a32fb8b36f1285e6 Mon Sep 17 00:00:00 2001 From: DiTus Date: Sun, 22 Mar 2026 19:57:35 +0100 Subject: [PATCH] feat: Add fat arrow pointer and directional arrows with color/text settings, implement active state for drawing toolbar buttons --- index.html | 25 +++-- js/ui/drawing-tools.js | 231 +++++++++++++++++++++++++++++++++++------ 2 files changed, 216 insertions(+), 40 deletions(-) diff --git a/index.html b/index.html index d3273b7..b0c8e52 100644 --- a/index.html +++ b/index.html @@ -107,28 +107,37 @@
- - - - - - + - - + +
diff --git a/js/ui/drawing-tools.js b/js/ui/drawing-tools.js index d15d7c9..f7c707f 100644 --- a/js/ui/drawing-tools.js +++ b/js/ui/drawing-tools.js @@ -56,6 +56,19 @@ export class DrawingManager { italic: false, alignVert: 'top', alignHorz: 'left' + }, + arrow: { + color: '#2962ff', + width: 2, + style: 0, + opacity: 100, + text: '', + textColor: '#2962ff', + fontSize: 14, + bold: false, + italic: false, + alignVert: 'top', + alignHorz: 'left' } }; @@ -81,7 +94,7 @@ export class DrawingManager { if (hit) { this.selectedDrawing = hit.drawing; // Only open panel for supported types - if (['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(hit.drawing.type)) { + if (['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(hit.drawing.type)) { this.toggleSettingsPanel(true); } this.update(); @@ -229,7 +242,18 @@ export class DrawingManager { this.activeTool = tool; - if (tool === 'cursor') { + // Update UI Button states + document.querySelectorAll('#drawingToolbar button[data-tool]').forEach(btn => { + if (btn.dataset.tool === tool) { + btn.classList.add('bg-blue-600/30', 'text-blue-400'); + btn.classList.remove('text-gray-400', 'hover:bg-[#2d3a4f]'); + } else { + btn.classList.remove('bg-blue-600/30', 'text-blue-400'); + btn.classList.add('text-gray-400', 'hover:bg-[#2d3a4f]'); + } + }); + + if (tool === 'cursor' || !tool) { this.activeTool = null; document.body.style.cursor = 'default'; } else if (tool === 'clear') { @@ -237,6 +261,8 @@ export class DrawingManager { this.selectedDrawing = null; this.activeTool = null; this.update(); + // Instantly clear the 'clear' button state + setTimeout(() => this.setTool(null), 100); } else { document.body.style.cursor = 'crosshair'; this.selectedDrawing = null; @@ -421,11 +447,42 @@ export class DrawingManager { this.setTool(null); } else if (this.activeTool === 'fib_retracement') { this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' }; - } else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down') { + } else if (this.activeTool === 'arrow_pointer') { + const defs = this.defaults.arrow; + this.currentDrawing = { + type: 'arrow_pointer', p1, p2, + color: defs.color, + width: defs.width, + style: defs.style, + opacity: defs.opacity, + text: defs.text, + textColor: defs.textColor, + fontSize: defs.fontSize, + bold: defs.bold, + italic: defs.italic, + alignVert: defs.alignVert, + alignHorz: defs.alignHorz + }; + } else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down' || this.activeTool === 'arrow_left' || this.activeTool === 'arrow_right') { + let color = '#2962ff'; // Default blue for left/right + if (this.activeTool === 'arrow_up') color = '#26a69a'; // Green + if (this.activeTool === 'arrow_down') color = '#ef5350'; // Red + + const defs = this.defaults.arrow; 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' + direction: this.activeTool.replace('arrow_', ''), + color: color, + width: 2, // default thickness for solid arrows + style: defs.style, + opacity: defs.opacity, + text: defs.text, + textColor: defs.textColor, + fontSize: defs.fontSize, + bold: defs.bold, + italic: defs.italic, + alignVert: defs.alignVert, + alignHorz: defs.alignHorz }); this.setTool(null); } else if (this.activeTool === 'text') { @@ -773,21 +830,57 @@ export class DrawingManager { if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; } - if (d.type === 'trend_line' || d.type === 'ray') { + if (d.type === 'trend_line' || d.type === 'ray' || d.type === 'arrow_pointer') { const x1 = this.timeToX(d.p1.time); const y1 = series.priceToCoordinate(d.p1.price); const x2 = this.timeToX(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') { + + if (d.type === 'arrow_pointer') { const angle = Math.atan2(y2 - y1, x2 - x1); - ctx.lineTo(x1 + Math.cos(angle) * 10000, y1 + Math.sin(angle) * 10000); + const dist = Math.hypot(x2 - x1, y2 - y1); + + ctx.save(); + ctx.translate(x1, y1); + ctx.rotate(angle); + + // Fat arrow dimensions + const headLen = Math.min(dist * 0.5, 30 + (d.width || 2) * 5); + const headWidth = headLen * 1.2; + const tailStartW = (d.width || 2) * 2; + const tailEndW = headLen * 0.5; + const indent = headLen * 0.2; // Head indentation + + ctx.beginPath(); + // Top edge of tail + ctx.moveTo(0, -tailStartW/2); + ctx.lineTo(dist - headLen + indent, -tailEndW/2); + // Top edge of head + ctx.lineTo(dist - headLen, -headWidth/2); + // Tip + ctx.lineTo(dist, 0); + // Bottom edge of head + ctx.lineTo(dist - headLen, headWidth/2); + // Bottom edge of tail + ctx.lineTo(dist - headLen + indent, tailEndW/2); + ctx.lineTo(0, tailStartW/2); + ctx.closePath(); + + ctx.fillStyle = color; + ctx.fill(); + ctx.restore(); } else { - ctx.lineTo(x2, y2); + 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(); } - ctx.stroke(); // Render Text if present if (d.text) { @@ -825,9 +918,11 @@ export class DrawingManager { if (isSelected) { ctx.setLineDash([]); - 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.fillStyle = d.type === 'arrow_pointer' ? 'transparent' : '#ffffff'; + ctx.strokeStyle = d.type === 'arrow_pointer' ? color : d.color; + ctx.lineWidth = 2; + ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); if (d.type !== 'arrow_pointer') ctx.fill(); ctx.stroke(); + ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); if (d.type !== 'arrow_pointer') ctx.fill(); ctx.stroke(); } } } else if (d.type === 'rectangle') { @@ -1017,19 +1112,89 @@ export class DrawingManager { const x = this.timeToX(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.fillStyle = d.color; + ctx.beginPath(); + const baseSize = isSelected ? 20 : 15; + const w = baseSize * 1.5; // width of arrow head + const h = baseSize * 1.5; // height of arrow head + const tailW = baseSize * 0.8; // width of tail + const tailH = baseSize * 1.2; // length of tail + + ctx.save(); + ctx.translate(x, y); + + if (d.direction === 'up') ctx.rotate(0); + else if (d.direction === 'right') ctx.rotate(Math.PI / 2); + else if (d.direction === 'down') ctx.rotate(Math.PI); + else if (d.direction === 'left') ctx.rotate(-Math.PI / 2); + + // Draw a thick arrow pointing UP from the origin (x,y) + ctx.moveTo(0, -h/2 - tailH/2); // Tip of arrow + ctx.lineTo(w/2, h/2 - tailH/2); // Right corner of head + ctx.lineTo(tailW/2, h/2 - tailH/2); // Inner right corner + ctx.lineTo(tailW/2, h/2 + tailH/2); // Bottom right of tail + ctx.lineTo(-tailW/2, h/2 + tailH/2); // Bottom left of tail + ctx.lineTo(-tailW/2, h/2 - tailH/2); // Inner left corner + ctx.lineTo(-w/2, h/2 - tailH/2); // Left corner of head + ctx.closePath(); + ctx.fill(); - if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); } + if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; ctx.stroke(); } + ctx.restore(); + + // Render Text and store label position + let labelPos = null; + if (d.text) { + ctx.save(); + ctx.setLineDash([]); + const fontSize = d.fontSize || 14; + const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : ''); + const font = `${fontStyle}${fontSize}px Inter`; + ctx.font = font; + ctx.fillStyle = d.textColor || d.color; + + const alignHorz = d.alignHorz || 'center'; + ctx.textAlign = alignHorz; + ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'bottom' : 'top'); + + let textX = x; + let textY = y; + + // Default offset logic for arrows if align isn't strictly overriding + const offset = 25; + if (d.direction === 'up') textY += offset; + else if (d.direction === 'down') textY -= offset; + else if (d.direction === 'left') textX += offset; + else if (d.direction === 'right') textX -= offset; + + ctx.fillText(d.text, textX, textY); + + const metrics = ctx.measureText(d.text); + const labelWidth = metrics.width + 16; + const labelHeight = 24; + + let defaultLabelX = textX - labelWidth / 2; + if (alignHorz === 'left') defaultLabelX = textX - labelWidth; + else if (alignHorz === 'right') defaultLabelX = textX; + + let defaultLabelY = textY; + if (d.alignVert === 'top') defaultLabelY = textY; + else if (d.alignVert === 'bottom') defaultLabelY = textY - labelHeight; + else defaultLabelY = textY - labelHeight / 2; + + labelPos = { + x: defaultLabelX + (d.labelOffset?.x || 0), + y: defaultLabelY + (d.labelOffset?.y || 0), + width: labelWidth, + height: labelHeight + }; + ctx.restore(); + } + + // Store label position for hit detection + if (labelPos) { + d.labelPos = labelPos; + } } } else if (d.type === 'text') { const x = this.timeToX(d.time); @@ -1240,7 +1405,7 @@ export class DrawingManager { window.switchTLTab = (tab) => { this.activeTLTab = tab; - const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type); + const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type); document.getElementById('tlTabStyle').className = tab === 'style' ? 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium' : 'flex-1 py-2 text-center text-gray-400 hover:text-white'; @@ -1265,11 +1430,11 @@ export class DrawingManager { window.setTLThickness = (width) => this.applySettings('width', width); window.setTLStyle = (style) => this.applySettings('style', style); window.toggleTLBold = () => { - const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type) ? this.selectedDrawing.bold : this.defaults.trend_line.bold; + const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type) ? this.selectedDrawing.bold : this.defaults.trend_line.bold; this.applySettings('bold', !current); }; window.toggleTLItalic = () => { - const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type) ? this.selectedDrawing.italic : this.defaults.trend_line.italic; + const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type) ? this.selectedDrawing.italic : this.defaults.trend_line.italic; this.applySettings('italic', !current); }; @@ -1391,7 +1556,7 @@ export class DrawingManager { btn.setAttribute('data-active', parseInt(btn.dataset.style) === settings.style); }); - const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type); + const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type); const textColorBtn = document.getElementById('tlTextColorBtn'); const textPicker = document.getElementById('tlTextColorPicker'); @@ -1420,7 +1585,7 @@ export class DrawingManager { applySettings(key, value) { if (this.selectedDrawing) { if (key === 'bold' || key === 'italic' || key === 'fontSize' || key === 'text' || key === 'textColor' || key === 'alignVert' || key === 'alignHorz') { - const isLineWithText = ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type); + const isLineWithText = ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type); if (!isLineWithText) return; this.selectedDrawing[key] = value; } else { @@ -1440,6 +1605,8 @@ export class DrawingManager { this.defaults.horizontal_line[key] = value; } else if (drawingType === 'vertical_line') { this.defaults.vertical_line[key] = value; + } else if (drawingType === 'arrow' || drawingType === 'arrow_pointer') { + this.defaults.arrow[key] = value; } else { this.defaults.trend_line[key] = value; }