import React, { useState, useRef } from "react"; type Mode = "AA" | "SAS" | "SSS"; const SimilarityTestsWidget: React.FC = () => { const [mode, setMode] = useState("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(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 ( {txt} ); }; return (
{(["AA", "SAS", "SSS"] as Mode[]).map((m) => ( ))}
Scale (k) setScale(parseFloat(e.target.value))} className="w-24 h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-rose-600" /> {scale.toFixed(1)}x
(isDragging.current = false)} onMouseLeave={() => (isDragging.current = false)} > {/* Triangle 1 (ABC) */} {/* Vertices T1 */} A C {/* Draggable B */} (isDragging.current = true)} className="cursor-grab active:cursor-grabbing" > {" "} {/* Hit area */} B {/* Triangle 2 (DEF) */} D F E {/* 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 */} {Math.round(c1)} {Math.round(c1 * scale)} {/* Side AC / DF */} {Math.round(b1)} {Math.round(b1 * scale)} )} {mode === "SSS" && ( <> {/* Side AB / DE */} {Math.round(c1)} {Math.round(c1 * scale)} {/* Side AC / DF */} {Math.round(b1)} {Math.round(b1 * scale)} {/* Side BC / EF */} {Math.round(a1)} {Math.round(a1 * scale)} )}

{mode === "AA" && "Angle-Angle (AA) Similarity"} {mode === "SAS" && "Side-Angle-Side (SAS) Similarity"} {mode === "SSS" && "Side-Side-Side (SSS) Similarity"}

{mode === "AA" && ( <>

If two angles of one triangle are equal to two angles of another triangle, then the triangles are similar.

First Angle

∠A = ∠D = {Math.round(angleA)}°

Second Angle

∠B = ∠E = {Math.round(angleB)}°

)} {mode === "SAS" && ( <>

If two sides are proportional and the included angles are equal, the triangles are similar.

Side Ratio (c)

DE / AB = {(c1 * scale).toFixed(0)} / {c1.toFixed(0)} ={" "} {scale.toFixed(1)}

Side Ratio (b)

DF / AC = {(b1 * scale).toFixed(0)} / {b1.toFixed(0)} ={" "} {scale.toFixed(1)}

Included Angle: ∠A = ∠D = {Math.round(angleA)}°

)} {mode === "SSS" && ( <>

If the corresponding sides of two triangles are proportional, then the triangles are similar.

Scale Factor k = {scale.toFixed(1)}

DE/AB = {scale.toFixed(1)}
EF/BC = {scale.toFixed(1)}
DF/AC = {scale.toFixed(1)}
)}

Drag vertex B on the first triangle to explore different shapes!

); }; export default SimilarityTestsWidget;