import React, { useState, useRef, useEffect } from 'react'; // Helper to convert radians to degrees const toDeg = (rad: number) => (rad * 180) / Math.PI; const InteractiveTriangle: React.FC = () => { // Vertex B state (the draggable top vertex) // Default position forming a nice scalene triangle const [bPos, setBPos] = useState({ x: 120, y: 50 }); const [isDragging, setIsDragging] = useState(false); const [showProof, setShowProof] = useState(false); const svgRef = useRef(null); // Fixed vertices const A = { x: 50, y: 250 }; const C = { x: 300, y: 250 }; const D = { x: 450, y: 250 }; // Extension of base AC // Colors const colors = { A: { text: "text-indigo-600", stroke: "#4f46e5", fill: "rgba(79, 70, 229, 0.2)" }, B: { text: "text-emerald-600", stroke: "#059669", fill: "rgba(5, 150, 105, 0.2)" }, Ext: { text: "text-rose-600", stroke: "#e11d48", fill: "rgba(225, 29, 72, 0.2)" } }; // Drag logic useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isDragging || !svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); let x = e.clientX - rect.left; let y = e.clientY - rect.top; // Constraints x = Math.max(20, Math.min(x, 380)); y = Math.max(20, Math.min(y, 230)); // Keep B above the base (y < 250) setBPos({ x, y }); }; const handleMouseUp = () => setIsDragging(false); if (isDragging) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); } return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [isDragging]); // Calculations // SVG Coordinate system: Y is Down. // We use atan2(dy, dx) to get angles. // Angle of vector (dx, dy). // Angle of AB const angleAB_rad = Math.atan2(bPos.y - A.y, bPos.x - A.x); const angleAB_deg = toDeg(angleAB_rad); // usually negative (e.g. -60) // Angle of AC is 0. // Angle A (magnitude) const valA = Math.abs(angleAB_deg); // Angle of CB const angleCB_rad = Math.atan2(bPos.y - C.y, bPos.x - C.x); const angleCB_deg = toDeg(angleCB_rad); // usually negative (e.g. -120) // Angle of CA is 180. // Angle C Interior (magnitude) = 180 - abs(angleCB_deg) if y < C.y (which it is). const valC = 180 - Math.abs(angleCB_deg); // Angle B (Interior) const valB = 180 - valA - valC; // Exterior Angle (magnitude) // Between CD (0) and CB (angleCB_deg). // Ext = abs(angleCB_deg). const valExt = Math.abs(angleCB_deg); // Arc Generation Helper const getArcPath = (cx: number, cy: number, r: number, startDeg: number, endDeg: number) => { // SVG standard: degrees clockwise from X-axis. // Our atan2 returns degrees relative to X-axis (clockwise positive if Y down). // so we can use them directly. const startRad = (startDeg * Math.PI) / 180; const endRad = (endDeg * Math.PI) / 180; const x1 = cx + r * Math.cos(startRad); const y1 = cy + r * Math.sin(startRad); const x2 = cx + r * Math.cos(endRad); const y2 = cy + r * Math.sin(endRad); // Sweep flag: 0 if counter-clockwise, 1 if clockwise. // We want to draw from start to end. // If we go from negative angle (AB) to 0 (AC), difference is positive. const largeArc = Math.abs(endDeg - startDeg) > 180 ? 1 : 0; const sweep = endDeg > startDeg ? 1 : 0; return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} ${sweep} ${x2} ${y2} Z`; }; return (

Interactive Triangle

{/* Base Line Extension */} D {/* Angle Arcs */} {/* Angle A: from angleAB to 0 */} {Math.round(valA)}° {/* Angle B: from angle of BA to angle of BC */} {/* Angle of BA is angleAB + 180. Angle of BC is angleCB + 180. */} {/* Wait, B is center. */} {/* Vector BA: A - B. Angle = atan2(Ay - By, Ax - Bx). */} {/* Vector BC: C - B. Angle = atan2(Cy - By, Cx - Bx). */} {/* Label B slightly above vertex */} {Math.round(valB)}° {/* Exterior Angle: at C, from angleCB to 0 */} {/* If showing proof, split it */} {!showProof && ( )} {/* Proof Visuals */} {showProof && ( <> {/* Parallel Line CE. Angle same as AB: angleAB_deg */} E {/* Lower part of Ext (Corresponding to A) - From angleAB_deg to 0 */} {Math.round(valA)}° {/* Upper part of Ext (Alt Interior to B) - From angleCB_deg to angleAB_deg */} {Math.round(valB)}° )} {/* Label Ext if not split or just general label */} {!showProof && ( Ext {Math.round(valExt)}° )} {/* Triangle Lines */} {/* Vertices */} A C {/* Draggable B */} setIsDragging(true)} className="cursor-grab active:cursor-grabbing" > {/* Hit area */} B
Ext ({Math.round(valExt)}°) = A ({Math.round(valA)}°) + B ({Math.round(valB)}°)

{showProof ? "Notice how the parallel line 'transports' angle A and B to the exterior?" : "Drag vertex B to see the values update."}

); }; export default InteractiveTriangle;