Files
edbridge-scholars/src/components/lessons/SimilarityTestsWidget.tsx
2026-03-01 20:24:14 +06:00

300 lines
16 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
type Mode = 'AA' | 'SAS' | 'SSS';
const SimilarityTestsWidget: React.FC = () => {
const [mode, setMode] = useState<Mode>('AA');
const [scale, setScale] = useState(1.5);
// Store Vertex B's position relative to A (x offset, y height)
// A is at (40, 220). SVG Y is down.
const [vertexB, setVertexB] = useState({ x: 40, y: 100 });
const isDragging = useRef(false);
const svgRef = useRef<SVGSVGElement>(null);
// Triangle 1 (ABC) - Fixed base AC
const A = { x: 40, y: 220 };
const C = { x: 120, y: 220 }; // Base length = 80
// Calculate B in SVG coordinates based on state
// vertexB.y is the height (upwards), so we subtract from A.y
const B = { x: A.x + vertexB.x, y: A.y - vertexB.y };
// Calculate lengths and angles for T1
const dist = (p1: {x:number, y:number}, p2: {x:number, y:number}) => Math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2);
const c1 = dist(A, B); // side c (opp C) - Side AB
const a1 = dist(B, C); // side a (opp A) - Side BC
const b1 = dist(A, C); // side b (opp B) - Side AC (Base)
const getAngle = (a: number, b: number, c: number) => {
return Math.acos((b**2 + c**2 - a**2) / (2 * b * c)) * (180 / Math.PI);
};
const angleA = getAngle(a1, b1, c1);
const angleB = getAngle(b1, a1, c1);
// const angleC = getAngle(c1, a1, b1);
// Triangle 2 (DEF) - Scaled version of ABC
// Start D with enough margin. Max width of T1 is ~100-140.
// Let's place D at x=240.
const D = { x: 240, y: 220 };
// F is horizontal from D by scaled base length
const F = { x: D.x + b1 * scale, y: D.y };
// E is scaled vector AB from D
const vecAB = { x: B.x - A.x, y: B.y - A.y };
const E = {
x: D.x + vecAB.x * scale,
y: D.y + vecAB.y * scale
};
// 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;
// Constraints for B relative to A
// Keep B within reasonable bounds to prevent breaking the layout
// Base is 40 to 120. B.x can range from 0 to 140?
const newX = x - A.x;
const height = A.y - y;
// Clamp
const clampedX = Math.max(-20, Math.min(100, newX));
const clampedH = Math.max(40, Math.min(180, height));
setVertexB({ x: clampedX, y: clampedH });
};
const angleColor = "#6366f1"; // Indigo
const sideColor = "#059669"; // Emerald
// Helper: draw filled angle wedge + labelled badge at a vertex
const angleC = 180 - angleA - angleB;
const renderAngle = (
vx: number, vy: number,
p1x: number, p1y: number,
p2x: number, p2y: number,
deg: number,
r = 28
) => {
const d1 = Math.atan2(p1y - vy, p1x - vx);
const d2 = Math.atan2(p2y - vy, p2x - vx);
const sx = vx + r * Math.cos(d1), sy = vy + r * Math.sin(d1);
const ex = vx + r * Math.cos(d2), ey = vy + r * Math.sin(d2);
const cross = (p1x - vx) * (p2y - vy) - (p1y - vy) * (p2x - vx);
const sweep = cross > 0 ? 1 : 0;
let diff = d2 - d1;
while (diff > Math.PI) diff -= 2 * Math.PI;
while (diff < -Math.PI) diff += 2 * Math.PI;
const mid = d1 + diff / 2;
const lr = r + 18;
const lx = vx + lr * Math.cos(mid), ly = vy + lr * Math.sin(mid);
const txt = `${Math.round(deg)}°`;
return (
<g>
<path d={`M ${vx} ${vy} L ${sx} ${sy} A ${r} ${r} 0 0 ${sweep} ${ex} ${ey} Z`} fill={angleColor} fillOpacity={0.12} stroke={angleColor} strokeWidth={2} />
<rect x={lx - 18} y={ly - 10} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={angleColor} strokeWidth={0.8} />
<text x={lx} y={ly + 5} textAnchor="middle" fill={angleColor} fontSize="13" fontWeight="bold" fontFamily="system-ui">{txt}</text>
</g>
);
};
return (
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
<div className="flex flex-col sm:flex-row gap-4 justify-between items-center mb-6">
<div className="flex bg-slate-100 p-1 rounded-lg overflow-x-auto max-w-full">
{(['AA', 'SAS', 'SSS'] as Mode[]).map(m => (
<button
key={m}
onClick={() => setMode(m)}
className={`px-4 py-2 rounded-md text-sm font-bold transition-all whitespace-nowrap ${
mode === m
? 'bg-white text-rose-600 shadow-sm'
: 'text-slate-500 hover:text-rose-600'
}`}
>
{m}
</button>
))}
</div>
<div className="flex items-center gap-3 bg-slate-50 px-4 py-2 rounded-lg border border-slate-200">
<span className="text-xs font-bold text-slate-400 uppercase">Scale (k)</span>
<input
type="range" min="0.5" max="2.5" step="0.1"
value={scale}
onChange={e => setScale(parseFloat(e.target.value))}
className="w-24 h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-rose-600"
/>
<span className="font-mono font-bold text-rose-600 text-sm w-12 text-right">{scale.toFixed(1)}x</span>
</div>
</div>
<div className="relative border border-slate-100 rounded-lg bg-slate-50 mb-6 overflow-hidden flex justify-center">
<svg
ref={svgRef}
width="550" height="280"
className="cursor-default select-none"
onMouseMove={handleMouseMove}
onMouseUp={() => isDragging.current = false}
onMouseLeave={() => isDragging.current = false}
>
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#e2e8f0" strokeWidth="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
{/* Triangle 1 (ABC) */}
<path d={`M ${A.x} ${A.y} L ${B.x} ${B.y} L ${C.x} ${C.y} Z`} fill="rgba(255, 255, 255, 0.8)" stroke="#334155" strokeWidth="2" />
{/* Vertices T1 */}
<circle cx={A.x} cy={A.y} r="4" fill="#334155" />
<text x={A.x - 16} y={A.y + 14} fontWeight="bold" fill="#334155" fontSize="14">A</text>
<circle cx={C.x} cy={C.y} r="4" fill="#334155" />
<text x={C.x + 8} y={C.y + 14} fontWeight="bold" fill="#334155" fontSize="14">C</text>
{/* Draggable B */}
<g onMouseDown={() => isDragging.current = true} className="cursor-grab active:cursor-grabbing">
<circle cx={B.x} cy={B.y} r="20" fill="transparent" /> {/* Hit area */}
<circle cx={B.x} cy={B.y} r="7" fill="#f43f5e" stroke="white" strokeWidth="2" />
<text x={B.x} y={B.y - 16} textAnchor="middle" fontWeight="bold" fill="#f43f5e" fontSize="14">B</text>
</g>
{/* Triangle 2 (DEF) */}
<path d={`M ${D.x} ${D.y} L ${E.x} ${E.y} L ${F.x} ${F.y} Z`} fill="rgba(255, 255, 255, 0.8)" stroke="#334155" strokeWidth="2" />
<circle cx={D.x} cy={D.y} r="4" fill="#334155" />
<text x={D.x - 16} y={D.y + 14} fontWeight="bold" fill="#334155" fontSize="14">D</text>
<circle cx={F.x} cy={F.y} r="4" fill="#334155" />
<text x={F.x + 8} y={F.y + 14} fontWeight="bold" fill="#334155" fontSize="14">F</text>
<circle cx={E.x} cy={E.y} r="4" fill="#334155" />
<text x={E.x} y={E.y - 16} textAnchor="middle" fontWeight="bold" fill="#334155" fontSize="14">E</text>
{/* Visual Overlays based on Mode */}
{mode === 'AA' && (
<>
{/* Angle A and D (base-left) */}
{renderAngle(A.x, A.y, C.x, C.y, B.x, B.y, angleA)}
{renderAngle(D.x, D.y, F.x, F.y, E.x, E.y, angleA)}
{/* Angle B and E (apex) */}
{renderAngle(B.x, B.y, A.x, A.y, C.x, C.y, angleB)}
{renderAngle(E.x, E.y, D.x, D.y, F.x, F.y, angleB)}
</>
)}
{mode === 'SAS' && (
<>
{/* Included Angle A and D */}
{renderAngle(A.x, A.y, C.x, C.y, B.x, B.y, angleA)}
{renderAngle(D.x, D.y, F.x, F.y, E.x, E.y, angleA)}
{/* Side labels with background badges */}
{/* Side AB / DE */}
<rect x={(A.x + B.x)/2 - 24} y={(A.y + B.y)/2 - 12} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(A.x + B.x)/2 - 6} y={(A.y + B.y)/2 + 3} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(c1)}</text>
<rect x={(D.x + E.x)/2 - 24} y={(D.y + E.y)/2 - 12} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(D.x + E.x)/2 - 6} y={(D.y + E.y)/2 + 3} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(c1 * scale)}</text>
{/* Side AC / DF */}
<rect x={(A.x + C.x)/2 - 18} y={A.y + 4} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(A.x + C.x)/2} y={A.y + 18} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(b1)}</text>
<rect x={(D.x + F.x)/2 - 18} y={D.y + 4} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(D.x + F.x)/2} y={D.y + 18} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(b1 * scale)}</text>
</>
)}
{mode === 'SSS' && (
<>
{/* Side AB / DE */}
<rect x={(A.x + B.x)/2 - 24} y={(A.y + B.y)/2 - 12} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(A.x + B.x)/2 - 6} y={(A.y + B.y)/2 + 3} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(c1)}</text>
<rect x={(D.x + E.x)/2 - 24} y={(D.y + E.y)/2 - 12} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(D.x + E.x)/2 - 6} y={(D.y + E.y)/2 + 3} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(c1 * scale)}</text>
{/* Side AC / DF */}
<rect x={(A.x + C.x)/2 - 18} y={A.y + 4} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(A.x + C.x)/2} y={A.y + 18} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(b1)}</text>
<rect x={(D.x + F.x)/2 - 18} y={D.y + 4} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(D.x + F.x)/2} y={D.y + 18} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(b1 * scale)}</text>
{/* Side BC / EF */}
<rect x={(B.x + C.x)/2 + 2} y={(B.y + C.y)/2 - 12} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(B.x + C.x)/2 + 20} y={(B.y + C.y)/2 + 3} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(a1)}</text>
<rect x={(E.x + F.x)/2 + 2} y={(E.y + F.y)/2 - 12} width={36} height={20} rx={5} fill="white" fillOpacity={0.92} stroke={sideColor} strokeWidth={0.8} />
<text x={(E.x + F.x)/2 + 20} y={(E.y + F.y)/2 + 3} fill={sideColor} fontSize="13" fontWeight="bold" textAnchor="middle">{Math.round(a1 * scale)}</text>
</>
)}
</svg>
</div>
<div className="bg-rose-50 border border-rose-100 rounded-lg p-4 text-rose-900">
<h4 className="font-bold mb-2 flex items-center gap-2 text-lg">
<span className="w-3 h-3 rounded-full bg-rose-500"></span>
{mode === 'AA' && "Angle-Angle (AA) Similarity"}
{mode === 'SAS' && "Side-Angle-Side (SAS) Similarity"}
{mode === 'SSS' && "Side-Side-Side (SSS) Similarity"}
</h4>
<div className="text-sm font-mono space-y-2">
{mode === 'AA' && (
<>
<p className="leading-relaxed">If two angles of one triangle are equal to two angles of another triangle, then the triangles are similar.</p>
<div className="flex gap-8 mt-2">
<div>
<span className="text-xs font-bold text-rose-400 uppercase">First Angle</span>
<p className="font-bold text-lg">A = D = {Math.round(angleA)}°</p>
</div>
<div>
<span className="text-xs font-bold text-rose-400 uppercase">Second Angle</span>
<p className="font-bold text-lg">B = E = {Math.round(angleB)}°</p>
</div>
</div>
</>
)}
{mode === 'SAS' && (
<>
<p className="leading-relaxed">If two sides are proportional and the included angles are equal, the triangles are similar.</p>
<div className="grid grid-cols-2 gap-4 mt-2">
<div className="bg-white p-2 rounded border border-rose-100">
<p className="text-xs text-rose-500 font-bold uppercase">Side Ratio (c)</p>
<p>DE / AB = {(c1*scale).toFixed(0)} / {c1.toFixed(0)} = <strong>{scale.toFixed(1)}</strong></p>
</div>
<div className="bg-white p-2 rounded border border-rose-100">
<p className="text-xs text-rose-500 font-bold uppercase">Side Ratio (b)</p>
<p>DF / AC = {(b1*scale).toFixed(0)} / {b1.toFixed(0)} = <strong>{scale.toFixed(1)}</strong></p>
</div>
</div>
<p className="mt-2 font-bold text-rose-800">Included Angle: A = D = {Math.round(angleA)}°</p>
</>
)}
{mode === 'SSS' && (
<>
<p className="leading-relaxed">If the corresponding sides of two triangles are proportional, then the triangles are similar.</p>
<p className="bg-white inline-block px-2 py-1 rounded border border-rose-100 font-bold text-rose-600 mb-2">Scale Factor k = {scale.toFixed(1)}</p>
<div className="grid grid-cols-3 gap-2 text-center text-xs">
<div className="bg-white p-1 rounded">
DE/AB = {scale.toFixed(1)}
</div>
<div className="bg-white p-1 rounded">
EF/BC = {scale.toFixed(1)}
</div>
<div className="bg-white p-1 rounded">
DF/AC = {scale.toFixed(1)}
</div>
</div>
</>
)}
</div>
<p className="text-xs text-rose-400 mt-4 border-t border-rose-100 pt-2">
Drag vertex <strong>B</strong> on the first triangle to explore different shapes!
</p>
</div>
</div>
);
};
export default SimilarityTestsWidget;