feat(lessons): add lessons from client db
This commit is contained in:
179
src/components/lessons/TangentPropertiesWidget.tsx
Normal file
179
src/components/lessons/TangentPropertiesWidget.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user