173 lines
8.1 KiB
TypeScript
173 lines
8.1 KiB
TypeScript
import React, { useState, useRef } from 'react';
|
|
|
|
const InequalityRegionWidget: React.FC = () => {
|
|
// State for Inequalities: y > or < mx + b
|
|
// isGreater: true for >=, false for <=
|
|
const [ineq1, setIneq1] = useState({ m: 1, b: 1, isGreater: true });
|
|
const [ineq2, setIneq2] = useState({ m: -0.5, b: -2, isGreater: false });
|
|
|
|
const [testPoint, setTestPoint] = useState({ x: 0, y: 0 });
|
|
const isDragging = useRef(false);
|
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
|
|
// Viewport
|
|
const range = 10;
|
|
const size = 300;
|
|
const scale = size / (range * 2);
|
|
const center = size / 2;
|
|
|
|
// Helpers
|
|
const toPx = (val: number, isY = false) => {
|
|
return isY ? center - val * scale : center + val * scale;
|
|
};
|
|
|
|
const fromPx = (px: number, isY = false) => {
|
|
return isY ? (center - px) / scale : (px - center) / scale;
|
|
};
|
|
|
|
// Generate polygon points for shading
|
|
const getRegionPoints = (m: number, b: number, isGreater: boolean) => {
|
|
const xMin = -range;
|
|
const xMax = range;
|
|
const yAtMin = m * xMin + b;
|
|
const yAtMax = m * xMax + b;
|
|
|
|
// y limit is the top (range) or bottom (-range) of the graph
|
|
const limitY = isGreater ? range : -range;
|
|
|
|
const p1 = { x: xMin, y: yAtMin };
|
|
const p2 = { x: xMax, y: yAtMax };
|
|
const p3 = { x: xMax, y: limitY };
|
|
const p4 = { x: xMin, y: limitY };
|
|
|
|
return `${toPx(p1.x)},${toPx(p1.y, true)} ${toPx(p2.x)},${toPx(p2.y, true)} ${toPx(p3.x)},${toPx(p3.y, true)} ${toPx(p4.x)},${toPx(p4.y, true)}`;
|
|
};
|
|
|
|
const getLinePath = (m: number, b: number) => {
|
|
const x1 = -range;
|
|
const y1 = m * x1 + b;
|
|
const x2 = range;
|
|
const y2 = m * x2 + b;
|
|
return `M ${toPx(x1)} ${toPx(y1, true)} L ${toPx(x2)} ${toPx(y2, true)}`;
|
|
};
|
|
|
|
// Interaction
|
|
const handleInteraction = (e: React.MouseEvent) => {
|
|
if (!svgRef.current) return;
|
|
const rect = svgRef.current.getBoundingClientRect();
|
|
const x = fromPx(e.clientX - rect.left);
|
|
const y = fromPx(e.clientY - rect.top, true);
|
|
// Clamp
|
|
const cx = Math.max(-range, Math.min(range, x));
|
|
const cy = Math.max(-range, Math.min(range, y));
|
|
setTestPoint({ x: cx, y: cy });
|
|
};
|
|
|
|
// Logic Check
|
|
const check1 = ineq1.isGreater ? testPoint.y >= ineq1.m * testPoint.x + ineq1.b : testPoint.y <= ineq1.m * testPoint.x + ineq1.b;
|
|
const check2 = ineq2.isGreater ? testPoint.y >= ineq2.m * testPoint.x + ineq2.b : testPoint.y <= ineq2.m * testPoint.x + ineq2.b;
|
|
const isSolution = check1 && check2;
|
|
|
|
return (
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
|
|
<div className="flex flex-col md:flex-row gap-8">
|
|
|
|
{/* Controls */}
|
|
<div className="w-full md:w-1/3 space-y-6">
|
|
{/* Inequality 1 */}
|
|
<div className={`p-4 rounded-lg border ${check1 ? 'bg-indigo-50 border-indigo-200' : 'bg-slate-50 border-slate-200'}`}>
|
|
<div className="flex justify-between items-center mb-2">
|
|
<span className="font-bold text-indigo-800 text-sm">Region 1 (Blue)</span>
|
|
<button
|
|
onClick={() => setIneq1(p => ({...p, isGreater: !p.isGreater}))}
|
|
className="text-xs bg-white border border-indigo-200 px-2 py-1 rounded font-bold text-indigo-600"
|
|
>
|
|
{ineq1.isGreater ? 'y ≥ ...' : 'y ≤ ...'}
|
|
</button>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<div className="flex justify-between text-xs text-slate-500"><span>Slope</span><span>{ineq1.m}</span></div>
|
|
<input type="range" min="-4" max="4" step="0.5" value={ineq1.m} onChange={e => setIneq1({...ineq1, m: parseFloat(e.target.value)})} className="w-full h-1 bg-indigo-200 rounded accent-indigo-600"/>
|
|
</div>
|
|
<div>
|
|
<div className="flex justify-between text-xs text-slate-500"><span>Y-Int</span><span>{ineq1.b}</span></div>
|
|
<input type="range" min="-8" max="8" step="1" value={ineq1.b} onChange={e => setIneq1({...ineq1, b: parseFloat(e.target.value)})} className="w-full h-1 bg-indigo-200 rounded accent-indigo-600"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Inequality 2 */}
|
|
<div className={`p-4 rounded-lg border ${check2 ? 'bg-rose-50 border-rose-200' : 'bg-slate-50 border-slate-200'}`}>
|
|
<div className="flex justify-between items-center mb-2">
|
|
<span className="font-bold text-rose-800 text-sm">Region 2 (Red)</span>
|
|
<button
|
|
onClick={() => setIneq2(p => ({...p, isGreater: !p.isGreater}))}
|
|
className="text-xs bg-white border border-rose-200 px-2 py-1 rounded font-bold text-rose-600"
|
|
>
|
|
{ineq2.isGreater ? 'y ≥ ...' : 'y ≤ ...'}
|
|
</button>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<div className="flex justify-between text-xs text-slate-500"><span>Slope</span><span>{ineq2.m}</span></div>
|
|
<input type="range" min="-4" max="4" step="0.5" value={ineq2.m} onChange={e => setIneq2({...ineq2, m: parseFloat(e.target.value)})} className="w-full h-1 bg-rose-200 rounded accent-rose-600"/>
|
|
</div>
|
|
<div>
|
|
<div className="flex justify-between text-xs text-slate-500"><span>Y-Int</span><span>{ineq2.b}</span></div>
|
|
<input type="range" min="-8" max="8" step="1" value={ineq2.b} onChange={e => setIneq2({...ineq2, b: parseFloat(e.target.value)})} className="w-full h-1 bg-rose-200 rounded accent-rose-600"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={`p-3 rounded-lg text-center font-bold text-sm border-2 transition-colors ${isSolution ? 'bg-emerald-100 border-emerald-400 text-emerald-800' : 'bg-slate-100 border-slate-300 text-slate-500'}`}>
|
|
Test Point: ({testPoint.x.toFixed(1)}, {testPoint.y.toFixed(1)}) <br/>
|
|
{isSolution ? "SOLUTION FOUND" : "NOT A SOLUTION"}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Graph */}
|
|
<div className="flex-1 flex justify-center">
|
|
<div className="relative w-[300px] h-[300px] bg-white border border-slate-200 rounded-xl overflow-hidden cursor-crosshair">
|
|
<svg
|
|
ref={svgRef}
|
|
width="300" height="300" viewBox="0 0 300 300"
|
|
onMouseDown={(e) => { isDragging.current = true; handleInteraction(e); }}
|
|
onMouseMove={(e) => { if(isDragging.current) handleInteraction(e); }}
|
|
onMouseUp={() => isDragging.current = false}
|
|
onMouseLeave={() => isDragging.current = false}
|
|
>
|
|
<defs>
|
|
<pattern id="grid-ineq" width="15" height="15" patternUnits="userSpaceOnUse">
|
|
<path d="M 15 0 L 0 0 0 15" fill="none" stroke="#f8fafc" strokeWidth="1"/>
|
|
</pattern>
|
|
</defs>
|
|
<rect width="100%" height="100%" fill="url(#grid-ineq)" />
|
|
|
|
{/* Axes */}
|
|
<line x1="0" y1={center} x2={size} y2={center} stroke="#cbd5e1" strokeWidth="2" />
|
|
<line x1={center} y1="0" x2={center} y2={size} stroke="#cbd5e1" strokeWidth="2" />
|
|
|
|
{/* Region 1 */}
|
|
<polygon points={getRegionPoints(ineq1.m, ineq1.b, ineq1.isGreater)} fill="rgba(99, 102, 241, 0.2)" />
|
|
<path d={getLinePath(ineq1.m, ineq1.b)} stroke="#4f46e5" strokeWidth="2" />
|
|
|
|
{/* Region 2 */}
|
|
<polygon points={getRegionPoints(ineq2.m, ineq2.b, ineq2.isGreater)} fill="rgba(225, 29, 72, 0.2)" />
|
|
<path d={getLinePath(ineq2.m, ineq2.b)} stroke="#e11d48" strokeWidth="2" />
|
|
|
|
{/* Test Point */}
|
|
<circle
|
|
cx={toPx(testPoint.x)} cy={toPx(testPoint.y, true)} r="6"
|
|
fill={isSolution ? "#10b981" : "#64748b"} stroke="white" strokeWidth="2" className="shadow-sm"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default InequalityRegionWidget; |