feat(lessons): add lessons from client db
This commit is contained in:
232
src/components/lessons/InteractiveSectorWidget.tsx
Normal file
232
src/components/lessons/InteractiveSectorWidget.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
|
||||
const InteractiveSectorWidget: React.FC = () => {
|
||||
const [angle, setAngle] = useState(60); // degrees
|
||||
const [radius, setRadius] = useState(120); // pixels
|
||||
const isDragging = useRef<"angle" | "radius" | null>(null);
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
|
||||
const cx = 200;
|
||||
const cy = 200;
|
||||
const maxRadius = 160;
|
||||
|
||||
// Calculate Handle Position
|
||||
const rad = (angle * Math.PI) / 180;
|
||||
const hx = cx + radius * Math.cos(-rad); // SVG Y is down, so -rad for standard math "up" rotation behavior if we want counter-clockwise from East
|
||||
const hy = cy + radius * Math.sin(-rad);
|
||||
|
||||
// For the arc path
|
||||
// Start point is (cx + r, cy) [0 degrees]
|
||||
// End point is (hx, hy)
|
||||
const largeArc = angle > 180 ? 1 : 0;
|
||||
// Sweep flag 0 because we are using -rad (counter clockwise visual in SVG)
|
||||
const pathData = `
|
||||
M ${cx} ${cy}
|
||||
L ${cx + radius} ${cy}
|
||||
A ${radius} ${radius} 0 ${largeArc} 0 ${hx} ${hy}
|
||||
Z
|
||||
`;
|
||||
|
||||
// Interaction
|
||||
const handleInteraction = (e: React.MouseEvent) => {
|
||||
if (!svgRef.current || !isDragging.current) return;
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left;
|
||||
const my = e.clientY - rect.top;
|
||||
|
||||
const dx = mx - cx;
|
||||
const dy = my - cy;
|
||||
|
||||
if (isDragging.current === "angle") {
|
||||
let deg = Math.atan2(-dy, dx) * (180 / Math.PI); // -dy to correct for SVG coords
|
||||
if (deg < 0) deg += 360;
|
||||
setAngle(Math.round(deg));
|
||||
} else if (isDragging.current === "radius") {
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
setRadius(Math.max(50, Math.min(maxRadius, dist)));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200 flex flex-col md:flex-row items-center gap-8">
|
||||
<div className="relative select-none">
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="400"
|
||||
height="400"
|
||||
onMouseMove={handleInteraction}
|
||||
onMouseUp={() => (isDragging.current = null)}
|
||||
onMouseLeave={() => (isDragging.current = null)}
|
||||
className="cursor-crosshair"
|
||||
>
|
||||
{/* Full Circle Ghost */}
|
||||
<circle
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={radius}
|
||||
stroke="#e2e8f0"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
strokeDasharray="4,4"
|
||||
/>
|
||||
|
||||
{/* Sector */}
|
||||
<path
|
||||
d={pathData}
|
||||
fill="rgba(249, 115, 22, 0.2)"
|
||||
stroke="#f97316"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
|
||||
{/* Radius Handle Line (Baseline) */}
|
||||
<line
|
||||
x1={cx}
|
||||
y1={cy}
|
||||
x2={cx + radius}
|
||||
y2={cy}
|
||||
stroke="#cbd5e1"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
|
||||
{/* Radius Drag Handle (on baseline) */}
|
||||
<circle
|
||||
cx={cx + radius}
|
||||
cy={cy}
|
||||
r={8}
|
||||
fill="#94a3b8"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
className="cursor-ew-resize hover:fill-slate-600 shadow-sm"
|
||||
onMouseDown={() => (isDragging.current = "radius")}
|
||||
/>
|
||||
|
||||
{/* Angle Drag Handle (on arc) */}
|
||||
<circle
|
||||
cx={hx}
|
||||
cy={hy}
|
||||
r={10}
|
||||
fill="#f97316"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
className="cursor-pointer hover:scale-110 transition-transform shadow-md"
|
||||
onMouseDown={() => (isDragging.current = "angle")}
|
||||
/>
|
||||
|
||||
{/* Angle Text */}
|
||||
<text
|
||||
x={cx + 20}
|
||||
y={cy - 10}
|
||||
className="text-xs font-bold fill-orange-600"
|
||||
>
|
||||
{angle}°
|
||||
</text>
|
||||
|
||||
{/* Radius Text */}
|
||||
<text
|
||||
x={cx + radius / 2}
|
||||
y={cy + 15}
|
||||
textAnchor="middle"
|
||||
className="text-xs font-bold fill-slate-400"
|
||||
>
|
||||
r = {Math.round(radius / 10)}
|
||||
</text>
|
||||
|
||||
{/* Center */}
|
||||
<circle cx={cx} cy={cy} r={4} fill="#64748b" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full space-y-6">
|
||||
<div className="bg-orange-50 border border-orange-100 p-4 rounded-xl">
|
||||
<h3 className="text-orange-900 font-bold mb-2 flex items-center gap-2">
|
||||
<span className="p-1 bg-orange-200 rounded text-xs">INPUT</span>
|
||||
Parameters
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-orange-700 uppercase font-bold mb-1">
|
||||
Angle (θ): {angle}°
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="360"
|
||||
value={angle}
|
||||
onChange={(e) => setAngle(parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-orange-200 rounded-lg appearance-none cursor-pointer accent-orange-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-orange-700 uppercase font-bold mb-1">
|
||||
Radius (r): {Math.round(radius / 10)}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max={maxRadius}
|
||||
value={radius}
|
||||
onChange={(e) => setRadius(parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-orange-200 rounded-lg appearance-none cursor-pointer accent-orange-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-white border border-slate-200 rounded-xl shadow-sm">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm font-bold text-slate-600">
|
||||
Fraction of Circle
|
||||
</span>
|
||||
<span className="font-mono text-orange-600 font-bold">
|
||||
{angle}/360 ≈ {(angle / 360).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 rounded-full h-2">
|
||||
<div
|
||||
className="bg-orange-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${(angle / 360) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-white border border-slate-200 rounded-xl shadow-sm">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase">
|
||||
Arc Length
|
||||
</span>
|
||||
<div className="font-mono text-lg text-slate-800 mt-1">
|
||||
2π({Math.round(radius / 10)}) ×{" "}
|
||||
<span className="text-orange-600">
|
||||
{(angle / 360).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-bold text-xl text-slate-900 mt-1">
|
||||
= {(2 * Math.PI * (radius / 10) * (angle / 360)).toFixed(1)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-white border border-slate-200 rounded-xl shadow-sm">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase">
|
||||
Sector Area
|
||||
</span>
|
||||
<div className="font-mono text-lg text-slate-800 mt-1">
|
||||
π({Math.round(radius / 10)})² ×{" "}
|
||||
<span className="text-orange-600">
|
||||
{(angle / 360).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-bold text-xl text-slate-900 mt-1">
|
||||
={" "}
|
||||
{(Math.PI * Math.pow(radius / 10, 2) * (angle / 360)).toFixed(
|
||||
1,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InteractiveSectorWidget;
|
||||
Reference in New Issue
Block a user