179 lines
7.8 KiB
TypeScript
179 lines
7.8 KiB
TypeScript
import React, { useState, useRef } from 'react';
|
|
|
|
const TangentPropertiesWidget: React.FC = () => {
|
|
const [pointP, setPointP] = useState({ x: 350, y: 150 });
|
|
const isDragging = useRef(false);
|
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
|
|
const center = { x: 150, y: 150 };
|
|
const radius = 60;
|
|
|
|
// Interaction
|
|
const handleMouseMove = (e: React.MouseEvent) => {
|
|
if (!isDragging.current || !svgRef.current) return;
|
|
const rect = svgRef.current.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
// Constrain P to be outside the circle (distance > radius)
|
|
const dx = x - center.x;
|
|
const dy = y - center.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
// Min distance to keep things looking nice (radius + padding)
|
|
if (dist < radius + 20) {
|
|
const angle = Math.atan2(dy, dx);
|
|
setPointP({
|
|
x: center.x + (radius + 20) * Math.cos(angle),
|
|
y: center.y + (radius + 20) * Math.sin(angle)
|
|
});
|
|
} else {
|
|
setPointP({ x, y });
|
|
}
|
|
};
|
|
|
|
// Calculations
|
|
const dx = pointP.x - center.x;
|
|
const dy = pointP.y - center.y;
|
|
const distPO = Math.sqrt(dx * dx + dy * dy);
|
|
const anglePO = Math.atan2(dy, dx);
|
|
|
|
// Angle offset to tangent points
|
|
// cos(theta) = Adjacent / Hypotenuse = radius / distPO
|
|
const theta = Math.acos(radius / distPO);
|
|
|
|
const t1Angle = anglePO - theta;
|
|
const t2Angle = anglePO + theta;
|
|
|
|
const T1 = {
|
|
x: center.x + radius * Math.cos(t1Angle),
|
|
y: center.y + radius * Math.sin(t1Angle)
|
|
};
|
|
|
|
const T2 = {
|
|
x: center.x + radius * Math.cos(t2Angle),
|
|
y: center.y + radius * Math.sin(t2Angle)
|
|
};
|
|
|
|
const tangentLength = Math.sqrt(distPO * distPO - radius * radius);
|
|
|
|
// Right Angle Markers
|
|
const markerSize = 10;
|
|
const getRightAnglePath = (p: {x:number, y:number}, angle: number) => {
|
|
// angle is the angle of the radius. We need to go inwards and perpendicular
|
|
// Actually simpler: Vector from Center to T, and Vector T to P are perp.
|
|
// Let's just draw a small square aligned with radius
|
|
const rAngle = angle;
|
|
// Point on radius
|
|
const p1 = { x: p.x - markerSize * Math.cos(rAngle), y: p.y - markerSize * Math.sin(rAngle) };
|
|
// Point on tangent (towards P)
|
|
// Tangent is perpendicular to radius.
|
|
// We need to know if we go clockwise or counter clockwise.
|
|
// Vector T->P
|
|
const tpAngle = Math.atan2(pointP.y - p.y, pointP.x - p.x);
|
|
const p2 = { x: p.x + markerSize * Math.cos(tpAngle), y: p.y + markerSize * Math.sin(tpAngle) };
|
|
// Corner
|
|
const p3 = { x: p1.x + markerSize * Math.cos(tpAngle), y: p1.y + markerSize * Math.sin(tpAngle) };
|
|
|
|
return `M ${p1.x} ${p1.y} L ${p3.x} ${p3.y} L ${p2.x} ${p2.y}`;
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200 flex flex-col md:flex-row gap-8 items-center">
|
|
<div className="relative">
|
|
<svg
|
|
ref={svgRef}
|
|
width="400" height="300"
|
|
className="select-none cursor-default bg-slate-50 rounded-lg border border-slate-100"
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={() => isDragging.current = false}
|
|
onMouseLeave={() => isDragging.current = false}
|
|
>
|
|
{/* Circle */}
|
|
<circle cx={center.x} cy={center.y} r={radius} fill="white" stroke="#94a3b8" strokeWidth="2" />
|
|
<circle cx={center.x} cy={center.y} r="3" fill="#64748b" />
|
|
<text x={center.x - 15} y={center.y + 5} className="text-xs font-bold fill-slate-400">O</text>
|
|
|
|
{/* Radii */}
|
|
<line x1={center.x} y1={center.y} x2={T1.x} y2={T1.y} stroke="#cbd5e1" strokeWidth="2" strokeDasharray="4,4" />
|
|
<line x1={center.x} y1={center.y} x2={T2.x} y2={T2.y} stroke="#cbd5e1" strokeWidth="2" strokeDasharray="4,4" />
|
|
|
|
{/* Tangents */}
|
|
<line x1={pointP.x} y1={pointP.y} x2={T1.x} y2={T1.y} stroke="#7c3aed" strokeWidth="3" />
|
|
<line x1={pointP.x} y1={pointP.y} x2={T2.x} y2={T2.y} stroke="#7c3aed" strokeWidth="3" />
|
|
|
|
{/* Right Angle Markers */}
|
|
<path d={getRightAnglePath(T1, t1Angle)} stroke="#64748b" fill="transparent" strokeWidth="1" />
|
|
<path d={getRightAnglePath(T2, t2Angle)} stroke="#64748b" fill="transparent" strokeWidth="1" />
|
|
|
|
{/* Points */}
|
|
<circle cx={T1.x} cy={T1.y} r="5" fill="#7c3aed" />
|
|
<text x={T1.x + (T1.x - center.x)*0.2} y={T1.y + (T1.y - center.y)*0.2} className="text-xs font-bold fill-violet-700">A</text>
|
|
|
|
<circle cx={T2.x} cy={T2.y} r="5" fill="#7c3aed" />
|
|
<text x={T2.x + (T2.x - center.x)*0.2} y={T2.y + (T2.y - center.y)*0.2} className="text-xs font-bold fill-violet-700">B</text>
|
|
|
|
{/* External Point P */}
|
|
<g
|
|
onMouseDown={() => isDragging.current = true}
|
|
className="cursor-grab active:cursor-grabbing"
|
|
>
|
|
<circle cx={pointP.x} cy={pointP.y} r="15" fill="transparent" />
|
|
<circle cx={pointP.x} cy={pointP.y} r="6" fill="#f43f5e" stroke="white" strokeWidth="2" />
|
|
<text x={pointP.x + 10} y={pointP.y} className="text-sm font-bold fill-rose-600">P</text>
|
|
</g>
|
|
|
|
{/* Length Labels (Midpoints) */}
|
|
<rect
|
|
x={(pointP.x + T1.x)/2 - 15} y={(pointP.y + T1.y)/2 - 10}
|
|
width="30" height="20" rx="4" fill="white" stroke="#e2e8f0"
|
|
/>
|
|
<text x={(pointP.x + T1.x)/2} y={(pointP.y + T1.y)/2 + 4} textAnchor="middle" className="text-xs font-bold fill-violet-600">
|
|
{Math.round(tangentLength)}
|
|
</text>
|
|
|
|
<rect
|
|
x={(pointP.x + T2.x)/2 - 15} y={(pointP.y + T2.y)/2 - 10}
|
|
width="30" height="20" rx="4" fill="white" stroke="#e2e8f0"
|
|
/>
|
|
<text x={(pointP.x + T2.x)/2} y={(pointP.y + T2.y)/2 + 4} textAnchor="middle" className="text-xs font-bold fill-violet-600">
|
|
{Math.round(tangentLength)}
|
|
</text>
|
|
</svg>
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-6">
|
|
<div className="bg-violet-50 p-4 rounded-xl border border-violet-100">
|
|
<h4 className="font-bold text-violet-900 mb-2 flex items-center gap-2">
|
|
<span className="bg-violet-200 text-xs px-2 py-0.5 rounded-full text-violet-800">Rule 1</span>
|
|
Equal Tangents
|
|
</h4>
|
|
<p className="text-sm text-violet-800 mb-2">
|
|
Tangents from the same external point are always congruent.
|
|
</p>
|
|
<p className="font-mono text-lg font-bold text-violet-600 bg-white p-2 rounded border border-violet-100 text-center">
|
|
PA = PB = {Math.round(tangentLength)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200">
|
|
<h4 className="font-bold text-slate-700 mb-2 flex items-center gap-2">
|
|
<span className="bg-slate-200 text-xs px-2 py-0.5 rounded-full text-slate-600">Rule 2</span>
|
|
Perpendicular Radius
|
|
</h4>
|
|
<p className="text-sm text-slate-600">
|
|
The radius to the point of tangency is always perpendicular to the tangent line.
|
|
</p>
|
|
<div className="flex gap-4 mt-2 justify-center">
|
|
<span className="text-xs font-bold bg-white px-2 py-1 rounded border border-slate-200">∠OAP = 90°</span>
|
|
<span className="text-xs font-bold bg-white px-2 py-1 rounded border border-slate-200">∠OBP = 90°</span>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xs text-center text-slate-400">Drag point <strong>P</strong> to verify!</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TangentPropertiesWidget; |