Files
edbridge-scholars/src/components/lessons/SimilarityTestsWidget.tsx
2026-03-12 02:39:34 +06:00

646 lines
20 KiB
TypeScript

import React, { useState, useRef } 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 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;