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

187 lines
7.5 KiB
TypeScript

import React, { useState } from 'react';
type Relationship = 'none' | 'vertical' | 'linear' | 'corresponding' | 'alt-interior' | 'same-side';
const InteractiveTransversal: React.FC = () => {
const [activeRel, setActiveRel] = useState<Relationship>('same-side');
// SVG Config
const width = 500;
const height = 300;
const line1Y = 100;
const line2Y = 200;
// Transversal passes through (200, 100) and (300, 200)
// Slope is 1 (45 degrees down-right)
const intersection1 = { x: 200, y: 100 };
const intersection2 = { x: 300, y: 200 };
// Angle Definitions (SVG y-down coordinates)
// 0 deg = Right, 90 deg = Down, 180 deg = Left, 270 deg = Up
// Transversal vector is (1, 1), angle is 45 deg.
// Opposite ray is 225 deg.
const angles = [
// Intersection 1 (Top)
{ id: 1, cx: intersection1.x, cy: intersection1.y, start: 180, end: 225, labelPos: 202.5, quadrant: 'TL' }, // Top-Left (Acute)
{ id: 2, cx: intersection1.x, cy: intersection1.y, start: 225, end: 360, labelPos: 292.5, quadrant: 'TR' }, // Top-Right (Obtuse)
{ id: 3, cx: intersection1.x, cy: intersection1.y, start: 0, end: 45, labelPos: 22.5, quadrant: 'BR' }, // Bottom-Right (Acute)
{ id: 4, cx: intersection1.x, cy: intersection1.y, start: 45, end: 180, labelPos: 112.5, quadrant: 'BL' }, // Bottom-Left (Obtuse)
// Intersection 2 (Bottom)
{ id: 5, cx: intersection2.x, cy: intersection2.y, start: 180, end: 225, labelPos: 202.5, quadrant: 'TL' },
{ id: 6, cx: intersection2.x, cy: intersection2.y, start: 225, end: 360, labelPos: 292.5, quadrant: 'TR' },
{ id: 7, cx: intersection2.x, cy: intersection2.y, start: 0, end: 45, labelPos: 22.5, quadrant: 'BR' },
{ id: 8, cx: intersection2.x, cy: intersection2.y, start: 45, end: 180, labelPos: 112.5, quadrant: 'BL' },
];
const getArcPath = (cx: number, cy: number, r: number, startDeg: number, endDeg: number) => {
// Convert to radians
const startRad = (startDeg * Math.PI) / 180;
const endRad = (endDeg * Math.PI) / 180;
const x1 = cx + r * Math.cos(startRad);
const y1 = cy + r * Math.sin(startRad);
const x2 = cx + r * Math.cos(endRad);
const y2 = cy + r * Math.sin(endRad);
const largeArc = (endDeg - startDeg) > 180 ? 1 : 0;
return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`;
};
const getLabelPos = (cx: number, cy: number, r: number, angleDeg: number) => {
const rad = (angleDeg * Math.PI) / 180;
return {
x: cx + r * Math.cos(rad),
y: cy + r * Math.sin(rad)
};
};
const getStyles = (id: number) => {
const base = { fill: 'transparent', stroke: 'transparent', label: 'text-slate-400' };
const highlightBlue = { fill: 'rgba(99, 102, 241, 0.3)', stroke: '#4f46e5', label: 'text-indigo-600 font-bold' };
const highlightPink = { fill: 'rgba(244, 63, 94, 0.3)', stroke: '#e11d48', label: 'text-rose-600 font-bold' };
switch (activeRel) {
case 'vertical':
// 1 & 3 are equal
if ([1, 3].includes(id)) return highlightBlue;
return base;
case 'linear':
// 1 & 2 are supplementary
if (id === 1) return highlightBlue;
if (id === 2) return highlightPink;
return base;
case 'corresponding':
// 2 & 6 are equal
if ([2, 6].includes(id)) return highlightBlue;
return base;
case 'alt-interior':
// 4 & 6 are equal
if ([4, 6].includes(id)) return highlightBlue;
return base;
case 'same-side':
// 3 & 6 are supplementary
if (id === 3) return highlightBlue;
if (id === 6) return highlightPink;
return base;
default:
return base;
}
};
const getDescription = () => {
switch (activeRel) {
case 'vertical': return "Vertical Angles are equal (e.g. ∠1 = ∠3)";
case 'linear': return "Linear Pairs sum to 180° (e.g. ∠1 + ∠2 = 180°)";
case 'corresponding': return "Corresponding Angles are equal (e.g. ∠2 = ∠6)";
case 'alt-interior': return "Alternate Interior Angles are equal (e.g. ∠4 = ∠6)";
case 'same-side': return "Same-Side Interior sum to 180° (e.g. ∠3 + ∠6 = 180°)";
default: return "Select a relationship to highlight";
}
};
const buttons: { id: Relationship, label: string }[] = [
{ id: 'vertical', label: 'Vertical Angles' },
{ id: 'linear', label: 'Linear Pair' },
{ id: 'corresponding', label: 'Corresponding' },
{ id: 'alt-interior', label: 'Alt. Interior' },
{ id: 'same-side', label: 'Same-Side Interior' },
];
return (
<div className="flex flex-col items-center bg-white p-8 rounded-xl shadow-sm border border-slate-200">
<div className="flex flex-wrap gap-2 justify-center mb-8">
{buttons.map(btn => (
<button
key={btn.id}
onClick={() => setActiveRel(activeRel === btn.id ? 'none' : btn.id)}
className={`px-4 py-2 rounded-full text-sm font-bold transition-all border ${
activeRel === btn.id
? 'bg-slate-900 text-white border-slate-900 shadow-md'
: 'bg-white text-slate-600 border-slate-200 hover:border-slate-400 hover:bg-slate-50'
}`}
>
{btn.label}
</button>
))}
</div>
<div className="relative w-full flex justify-center">
<div className="absolute top-0 left-0 w-full text-center">
<p className="text-slate-500 font-medium">{getDescription()}</p>
</div>
<svg width={width} height={height} className="mt-8 select-none">
<defs>
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="4" orient="auto">
<path d="M0,0 L0,8 L12,4 z" fill="#64748b" />
</marker>
</defs>
{/* Parallel Lines */}
<line x1="50" y1={line1Y} x2="450" y2={line1Y} stroke="#64748b" strokeWidth="3" markerEnd="url(#arrow)" />
<line x1="450" y1={line1Y} x2="50" y2={line1Y} stroke="#64748b" strokeWidth="3" markerEnd="url(#arrow)" />
<line x1="50" y1={line2Y} x2="450" y2={line2Y} stroke="#64748b" strokeWidth="3" markerEnd="url(#arrow)" />
<line x1="450" y1={line2Y} x2="50" y2={line2Y} stroke="#64748b" strokeWidth="3" markerEnd="url(#arrow)" />
{/* Transversal (infinite line simulation) */}
<line x1="100" y1="0" x2="400" y2="300" stroke="#0f172a" strokeWidth="3" strokeLinecap="round" />
{/* Angles */}
{angles.map((angle) => {
const styles = getStyles(angle.id);
const r = 35;
const labelPos = getLabelPos(angle.cx, angle.cy, r + 15, angle.labelPos);
return (
<g key={angle.id}>
<path
d={getArcPath(angle.cx, angle.cy, r, angle.start, angle.end)}
fill={styles.fill}
stroke={styles.stroke}
strokeWidth={styles.stroke === 'transparent' ? 0 : 2}
/>
<text
x={labelPos.x}
y={labelPos.y}
textAnchor="middle"
dominantBaseline="middle"
className={`text-sm select-none ${styles.label}`}
>
{angle.id}
</text>
</g>
);
})}
</svg>
</div>
</div>
);
};
export default InteractiveTransversal;