import React, { useState, useRef } from 'react'; type Mode = 'chords' | 'secants'; interface Point { x: number; y: number; } const PowerOfPointWidget: React.FC = () => { const [mode, setMode] = useState('chords'); // -- Common State -- const svgRef = useRef(null); const isDragging = useRef(null); const center = { x: 200, y: 180 }; const radius = 100; // -- Chords Mode State -- // Store angles for points A, B, C, D on the circle const [chordAngles, setChordAngles] = useState({ a: 220, b: 40, // Chord 1 c: 140, d: 320 // Chord 2 }); // -- Secants Mode State -- // P is external point. // Secant 1 defined by angle theta1 (offset from center-P line) // Secant 2 defined by angle theta2 const [secantState, setSecantState] = useState({ px: 380, py: 180, // Point P theta1: 15, // Angle offset for secant 1 }); // --- Helper Math --- const getPosOnCircle = (deg: number) => ({ x: center.x + radius * Math.cos(deg * Math.PI / 180), y: center.y + radius * Math.sin(deg * Math.PI / 180) }); const dist = (p1: Point, p2: Point) => Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); const getIntersection = (p1: Point, p2: Point, p3: Point, p4: Point) => { // Line AB represented as a1x + b1y = c1 const a1 = p2.y - p1.y; const b1 = p1.x - p2.x; const c1 = a1 * p1.x + b1 * p1.y; // Line CD represented as a2x + b2y = c2 const a2 = p4.y - p3.y; const b2 = p3.x - p4.x; const c2 = a2 * p3.x + b2 * p3.y; const determinant = a1 * b2 - a2 * b1; if (Math.abs(determinant) < 0.001) return null; // Parallel const x = (b2 * c1 - b1 * c2) / determinant; const y = (a1 * c2 - a2 * c1) / determinant; // Check if inside circle if (dist({x,y}, center) > radius + 1) return null; return { x, y }; }; // --- Interaction Handlers --- const handleChordDrag = (e: React.MouseEvent, key: string) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); const dx = e.clientX - rect.left - center.x; const dy = e.clientY - rect.top - center.y; let deg = Math.atan2(dy, dx) * 180 / Math.PI; if (deg < 0) deg += 360; setChordAngles(prev => ({ ...prev, [key]: deg })); }; const handleSecantDrag = (e: React.MouseEvent) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; if (isDragging.current === 'P') { // Constrain P outside const dx = x - center.x; const dy = y - center.y; const d = Math.sqrt(dx*dx + dy*dy); if (d > radius + 20) { setSecantState(prev => ({...prev, px: x, py: y})); } else { const ang = Math.atan2(dy, dx); setSecantState(prev => ({ ...prev, px: center.x + (radius+20)*Math.cos(ang), py: center.y + (radius+20)*Math.sin(ang) })); } } else if (isDragging.current === 'SecantEnd') { // Calculate angle relative to PO line // Vector PO const pdx = center.x - secantState.px; const pdy = center.y - secantState.py; const poAngle = Math.atan2(pdy, pdx); // Vector PA (mouse to P) const mdx = x - secantState.px; const mdy = y - secantState.py; const mAngle = Math.atan2(mdy, mdx); let diff = (mAngle - poAngle) * 180 / Math.PI; // Normalize to -180 to 180 while (diff > 180) diff -= 360; while (diff < -180) diff += 360; // Clamp to hit circle. Max angle is asin(R/dist) const distPO = Math.sqrt(pdx*pdx + pdy*pdy); const maxAngle = Math.asin(radius/distPO) * 180 / Math.PI; // Clamp const clamped = Math.max(-maxAngle + 1, Math.min(maxAngle - 1, diff)); setSecantState(prev => ({...prev, theta1: clamped})); } }; // --- Render Helpers --- const renderChords = () => { const A = getPosOnCircle(chordAngles.a); const B = getPosOnCircle(chordAngles.b); const C = getPosOnCircle(chordAngles.c); const D = getPosOnCircle(chordAngles.d); const E = getIntersection(A, B, C, D); const valid = !!E; const ae = valid ? dist(A, E) : 0; const eb = valid ? dist(E, B) : 0; const ce = valid ? dist(C, E) : 0; const ed = valid ? dist(E, D) : 0; const points = [ { k: 'a', p: A, l: 'A', c: '#7c3aed' }, { k: 'b', p: B, l: 'B', c: '#7c3aed' }, { k: 'c', p: C, l: 'C', c: '#059669' }, { k: 'd', p: D, l: 'D', c: '#059669' } ]; return ( <> {/* Points */} {points.map((pt) => ( isDragging.current = pt.k} className="cursor-pointer hover:scale-110 transition-transform"> {pt.l} ))} {valid && ( <> E )} {/* Info Panel */}
{!valid ? (

Chords must intersect inside!

) : (
Purple Chord
{ae.toFixed(0)} × {eb.toFixed(0)} = {(ae*eb).toFixed(0)}
Green Chord
{ce.toFixed(0)} × {ed.toFixed(0)} = {(ce*ed).toFixed(0)}

AE · EB = CE · ED

)}
); }; const renderSecant = () => { const { px, py, theta1 } = secantState; // Calculate Tangent Point T (Upper) const dx = px - center.x; const dy = py - center.y; const distPO = Math.sqrt(dx*dx + dy*dy); const anglePO = Math.atan2(dy, dx); const angleOffset = Math.acos(radius/distPO); const tAngle = anglePO - angleOffset; const T = { x: center.x + radius * Math.cos(tAngle), y: center.y + radius * Math.sin(tAngle) }; const tangentLen = Math.sqrt(distPO*distPO - radius*radius); // Calculate Secant Intersection Points // Secant Line angle const secantAngle = anglePO + theta1 * Math.PI / 180; const vx = px - center.x; const vy = py - center.y; const cos = Math.cos(secantAngle); const sin = Math.sin(secantAngle); // t^2 + 2(V.D)t + (V^2 - R^2) = 0 const b = 2 * (vx * cos + vy * sin); const c = vx*vx + vy*vy - radius*radius; const det = b*b - 4*c; let A = {x:0, y:0}, B = {x:0, y:0}; let valid = false; if (det > 0) { const tFar = (-b - Math.sqrt(det)) / 2; const tNear = (-b + Math.sqrt(det)) / 2; // A is Near (External part) A = { x: px + tNear * cos, y: py + tNear * sin }; // B is Far (Whole secant endpoint) B = { x: px + tFar * cos, y: py + tFar * sin }; valid = true; } const distPA = valid ? dist({x:px, y:py}, A) : 0; const distPB = valid ? dist({x:px, y:py}, B) : 0; return ( <> {/* Tangent Line */} T {/* Secant Line (Draw full segment P to B) */} {valid && } {valid && ( <> {/* Point A (Near/External) */} A {/* Point B (Far/Whole) */} B )} {/* Point P */} isDragging.current = 'P'} className="cursor-grab active:cursor-grabbing"> P {/* Drag Handle for Secant Angle (at B, the far end) */} {valid && ( isDragging.current = 'SecantEnd'} /> )} {/* Info */}
Tangent² (PT²)
{tangentLen.toFixed(0)}² = {(tangentLen*tangentLen).toFixed(0)}
Secant (PA · PB)
{distPA.toFixed(0)} × {distPB.toFixed(0)} = {(distPA*distPB).toFixed(0)}

Tangent² = External × Whole

); }; const handleMouseMove = (e: React.MouseEvent) => { if (!isDragging.current) return; if (mode === 'chords') { // Check if dragging specific points if (['a','b','c','d'].includes(isDragging.current as string)) { handleChordDrag(e, isDragging.current as string); } } else { handleSecantDrag(e); } }; return (
isDragging.current = null} onMouseLeave={() => isDragging.current = null} > {/* Circle */} {mode === 'chords' ? renderChords() : renderSecant()}
{mode === 'chords' ? "Drag the colored points along the circle." : "Drag point P or the secant endpoint B." }
); }; export default PowerOfPointWidget;