import React, { useState, useRef, useEffect } from 'react'; const SPECIAL_ANGLES = [0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330, 360]; const UnitCircleWidget: React.FC = () => { const [angle, setAngle] = useState(45); // Degrees const [snap, setSnap] = useState(true); // Snap to special angles const svgRef = useRef(null); const isDragging = useRef(false); const radius = 140; const center = { x: 200, y: 200 }; const handleInteraction = (clientX: number, clientY: number) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); const dx = clientX - rect.left - center.x; const dy = clientY - rect.top - center.y; // Calculate angle from 0 to 360 let rad = Math.atan2(-dy, dx); if (rad < 0) rad += 2 * Math.PI; let deg = (rad * 180) / Math.PI; if (snap) { const nearest = SPECIAL_ANGLES.reduce((prev, curr) => Math.abs(curr - deg) < Math.abs(prev - deg) ? curr : prev ); if (Math.abs(nearest - deg) < 15) { deg = nearest; } } if (deg > 360) deg = 360; setAngle(Math.round(deg)); }; const handleMouseDown = (e: React.MouseEvent) => { isDragging.current = true; handleInteraction(e.clientX, e.clientY); }; const handleMouseMove = (e: React.MouseEvent) => { if (isDragging.current) { handleInteraction(e.clientX, e.clientY); } }; const handleMouseUp = () => { isDragging.current = false; }; useEffect(() => { const upHandler = () => isDragging.current = false; window.addEventListener('mouseup', upHandler); return () => window.removeEventListener('mouseup', upHandler); }, []); const rad = (angle * Math.PI) / 180; const x = Math.cos(rad); const y = Math.sin(rad); const px = center.x + radius * x; const py = center.y - radius * y; const getExactValue = (val: number) => { if (Math.abs(val) < 0.01) return "0"; if (Math.abs(val - 0.5) < 0.01) return "1/2"; if (Math.abs(val + 0.5) < 0.01) return "-1/2"; if (Math.abs(val - Math.sqrt(2)/2) < 0.01) return "√2/2"; if (Math.abs(val + Math.sqrt(2)/2) < 0.01) return "-√2/2"; if (Math.abs(val - Math.sqrt(3)/2) < 0.01) return "√3/2"; if (Math.abs(val + Math.sqrt(3)/2) < 0.01) return "-√3/2"; if (Math.abs(val - 1) < 0.01) return "1"; if (Math.abs(val + 1) < 0.01) return "-1"; return val.toFixed(3); }; const getRadianLabel = (deg: number) => { // Removed Record type annotation to prevent parsing error const map: any = { 0: "0", 30: "π/6", 45: "π/4", 60: "π/3", 90: "π/2", 120: "2π/3", 135: "3π/4", 150: "5π/6", 180: "π", 210: "7π/6", 225: "5π/4", 240: "4π/3", 270: "3π/2", 300: "5π/3", 315: "7π/4", 330: "11π/6", 360: "2π" }; if (map[deg]) return map[deg]; return ((deg * Math.PI) / 180).toFixed(2); }; const cosStr = getExactValue(x); const sinStr = getExactValue(y); const getAngleColor = () => { if (angle < 90) return "text-emerald-600"; if (angle < 180) return "text-indigo-600"; if (angle < 270) return "text-amber-600"; return "text-rose-600"; }; return (
{SPECIAL_ANGLES.map(a => { const rTick = (a * Math.PI) / 180; const x1 = 200 + (radius - 5) * Math.cos(rTick); const y1 = 200 - (radius - 5) * Math.sin(rTick); const x2 = 200 + radius * Math.cos(rTick); const y2 = 200 - radius * Math.sin(rTick); return ; })} {angle > 0 && ( 180 ? 1 : 0} 0 ${200 + 30*Math.cos(rad)} ${200 - 30*Math.sin(rad)}`} fill="none" stroke="#0f172a" strokeWidth="1.5" /> )} = 0 ? 15 : -10)} textAnchor="middle" className="text-xs font-bold fill-indigo-600">cos = 0 ? 10 : -10)} y={200 - (200 - py)/2} textAnchor={x >= 0 ? "start" : "end"} className="text-xs font-bold fill-rose-600">sin = 0 ? 280 : 40}, ${y >= 0 ? 40 : 360})`}> ({cosStr}, {sinStr})

Current Angle

{Math.round(angle)}° = {getRadianLabel(angle)} rad

Common Angles

{[0, 30, 45, 60, 90, 180, 270].map(a => ( ))}
Cosine (x)
{cosStr}
adj / hyp
Sine (y)
{sinStr}
opp / hyp
Tangent (sin/cos)
{Math.abs(x) < 0.001 ? "Undefined" : getExactValue(y/x)}

Pro tip: On the SAT, memorize the values for 30°, 45°, and 60°!

); }; export default UnitCircleWidget;