import type { ResonanceData } from './types'; const GRID_COLOR = '#475569'; // slate-600 const POINT_COLOR = '#2dd4bf'; // teal-400 const BG_COLOR = '#1e293b'; // slate-800 const BORDER_COLOR = '#334155'; // slate-700 const TEXT_COLOR = '#94a3b8'; // slate-400 /** * Draw the Smith chart grid on a 2D canvas. * Uses normalized impedance: z = Z/Z0 where Z0=50. * Smith chart maps z to reflection coefficient Gamma = (z-1)/(z+1). */ export function drawSmithChart(canvas: HTMLCanvasElement, resonance?: ResonanceData): void { const ctx = canvas.getContext('2d'); if (!ctx) return; const w = canvas.width; const h = canvas.height; const cx = w / 2; const cy = h / 2; const R = (Math.min(w, h) / 2) * 0.85; // chart radius in pixels // Clear ctx.fillStyle = BG_COLOR; ctx.fillRect(0, 0, w, h); // Border ctx.strokeStyle = BORDER_COLOR; ctx.lineWidth = 1; ctx.strokeRect(0, 0, w, h); ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, R, 0, 2 * Math.PI); ctx.clip(); // Constant R circles ctx.strokeStyle = GRID_COLOR; ctx.lineWidth = 0.5; const rValues = [0, 0.2, 0.5, 1, 2, 5]; for (const r of rValues) { // Circle center at ((r/(r+1))*R + cx, cy), radius R/(r+1) const circR = R / (r + 1); const circX = cx + (r / (r + 1)) * R; ctx.beginPath(); ctx.arc(circX, cy, circR, 0, 2 * Math.PI); ctx.stroke(); } // Constant X arcs const xValues = [0.2, 0.5, 1, 2, 5]; for (const x of xValues) { // Positive X arc (inductive, above center) drawXArc(ctx, cx, cy, R, x); // Negative X arc (capacitive, below center) drawXArc(ctx, cx, cy, R, -x); } // Horizontal center line ctx.beginPath(); ctx.moveTo(cx - R, cy); ctx.lineTo(cx + R, cy); ctx.stroke(); ctx.restore(); // Outer circle ctx.strokeStyle = GRID_COLOR; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(cx, cy, R, 0, 2 * Math.PI); ctx.stroke(); // Title ctx.fillStyle = TEXT_COLOR; ctx.font = "10px 'Inter', sans-serif"; ctx.textAlign = 'center'; ctx.fillText('Smith Chart', cx, h - 4); // Plot impedance point if we have resonance data if (resonance) { const z_real = resonance.impedance_real / 50; const z_imag = resonance.impedance_imag / 50; // Reflection coefficient: Gamma = (z-1)/(z+1) where z = z_real + j*z_imag const denom_r = (z_real + 1) * (z_real + 1) + z_imag * z_imag; const gamma_real = ((z_real * z_real + z_imag * z_imag - 1)) / denom_r; const gamma_imag = (2 * z_imag) / denom_r; const px = cx + gamma_real * R; const py = cy - gamma_imag * R; // y-axis inverted in canvas // Draw point ctx.fillStyle = POINT_COLOR; ctx.beginPath(); ctx.arc(px, py, 4, 0, 2 * Math.PI); ctx.fill(); // Outline ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(px, py, 4, 0, 2 * Math.PI); ctx.stroke(); // Label ctx.fillStyle = POINT_COLOR; ctx.font = "bold 9px 'Inter', sans-serif"; ctx.textAlign = 'left'; const label = `${resonance.impedance_real.toFixed(0)}${resonance.impedance_imag >= 0 ? '+' : ''}${resonance.impedance_imag.toFixed(0)}j`; ctx.fillText(label, px + 7, py + 3); } } function drawXArc( ctx: CanvasRenderingContext2D, cx: number, cy: number, R: number, x: number ): void { const arcR = R / Math.abs(x); const centerX = cx + R; const centerY = x > 0 ? cy - arcR : cy + arcR; // We need to clip the arc to within the unit circle. // Use many small segments and only draw those inside. const steps = 100; ctx.beginPath(); let drawing = false; for (let i = 0; i <= steps; i++) { const t = (i / steps) * Math.PI; const angle = x > 0 ? -Math.PI / 2 + t : Math.PI / 2 - t; const px = centerX + arcR * Math.cos(angle); const py = centerY + arcR * Math.sin(angle); // Check if inside unit circle const dx = px - cx; const dy = py - cy; if (dx * dx + dy * dy <= R * R * 1.01) { if (!drawing) { ctx.moveTo(px, py); drawing = true; } else { ctx.lineTo(px, py); } } else { drawing = false; } } ctx.stroke(); }