231 lines
7.0 KiB
TypeScript
231 lines
7.0 KiB
TypeScript
import React, { useState } from "react";
|
|
|
|
const RadicalSolutionWidget: React.FC = () => {
|
|
// Equation: sqrt(x) = x - k
|
|
const [k, setK] = useState(2);
|
|
|
|
// Intersection logic
|
|
// x = (x-k)^2 => x = x^2 - 2kx + k^2 => x^2 - (2k+1)x + k^2 = 0
|
|
// Roots via quadratic formula
|
|
const a = 1;
|
|
const b = -(2 * k + 1);
|
|
const c = k * k;
|
|
const disc = b * b - 4 * a * c;
|
|
|
|
let solutions: number[] = [];
|
|
if (disc >= 0) {
|
|
const x1 = (-b + Math.sqrt(disc)) / (2 * a);
|
|
const x2 = (-b - Math.sqrt(disc)) / (2 * a);
|
|
solutions = [x1, x2].filter((val) => val >= 0); // Domain x>=0
|
|
}
|
|
|
|
// Check validity against original equation sqrt(x) = x - k
|
|
const validSolutions = solutions.filter(
|
|
(x) => Math.abs(Math.sqrt(x) - (x - k)) < 0.01,
|
|
);
|
|
const extraneousSolutions = solutions.filter(
|
|
(x) => Math.abs(Math.sqrt(x) - (x - k)) >= 0.01,
|
|
);
|
|
|
|
// Vis
|
|
|
|
const height = 300;
|
|
const range = 10;
|
|
const scale = 25;
|
|
const toPx = (v: number, isY = false) =>
|
|
isY ? height - v * scale - 20 : v * scale + 20;
|
|
|
|
const pathSqrt = () => {
|
|
let d = "";
|
|
for (let x = 0; x <= range; x += 0.1) {
|
|
d += d
|
|
? ` L ${toPx(x)} ${toPx(Math.sqrt(x), true)}`
|
|
: `M ${toPx(x)} ${toPx(Math.sqrt(x), true)}`;
|
|
}
|
|
return d;
|
|
};
|
|
|
|
const pathLine = () => {
|
|
// y = x - k
|
|
const x1 = 0;
|
|
const y1 = -k;
|
|
const x2 = range;
|
|
const y2 = range - k;
|
|
return `M ${toPx(x1)} ${toPx(y1, true)} L ${toPx(x2)} ${toPx(y2, true)}`;
|
|
};
|
|
|
|
// Phantom parabola path (x = y^2) - representing the squared equation
|
|
// This includes y = -sqrt(x)
|
|
const pathPhantom = () => {
|
|
let d = "";
|
|
for (let x = 0; x <= range; x += 0.1) {
|
|
d += d
|
|
? ` L ${toPx(x)} ${toPx(-Math.sqrt(x), true)}`
|
|
: `M ${toPx(x)} ${toPx(-Math.sqrt(x), true)}`;
|
|
}
|
|
return d;
|
|
};
|
|
|
|
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">
|
|
<div className="w-full md:w-1/3 space-y-6">
|
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200">
|
|
<div className="text-xs font-bold text-slate-400 uppercase mb-2">
|
|
Equation
|
|
</div>
|
|
<div className="font-mono text-lg font-bold text-slate-800">
|
|
√x = x - {k}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase">
|
|
Shift Line (k) = {k}
|
|
</label>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="6"
|
|
step="0.5"
|
|
value={k}
|
|
onChange={(e) => setK(parseFloat(e.target.value))}
|
|
className="w-full h-2 bg-slate-200 rounded-lg accent-indigo-600 mt-2"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div className="p-3 bg-emerald-50 rounded border border-emerald-100">
|
|
<div className="text-xs font-bold text-emerald-700 uppercase mb-1">
|
|
Valid Solutions
|
|
</div>
|
|
<div className="font-mono text-sm font-bold text-emerald-900">
|
|
{validSolutions.length > 0
|
|
? validSolutions.map((n) => `x = ${n.toFixed(2)}`).join(", ")
|
|
: "None"}
|
|
</div>
|
|
</div>
|
|
<div className="p-3 bg-rose-50 rounded border border-rose-100">
|
|
<div className="text-xs font-bold text-rose-700 uppercase mb-1">
|
|
Extraneous Solutions
|
|
</div>
|
|
<div className="font-mono text-sm font-bold text-rose-900">
|
|
{extraneousSolutions.length > 0
|
|
? extraneousSolutions
|
|
.map((n) => `x = ${n.toFixed(2)}`)
|
|
.join(", ")
|
|
: "None"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xs text-slate-400 leading-relaxed">
|
|
The <span className="text-rose-400 font-bold">extraneous</span>{" "}
|
|
solution is a real intersection for the <em>squared</em> equation
|
|
(the phantom curve), but not for the original radical.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex-1 flex justify-center">
|
|
<div className="relative w-[300px] h-[300px] bg-white border border-slate-200 rounded-xl overflow-hidden">
|
|
<svg width="100%" height="100%" viewBox="0 0 300 300">
|
|
{/* Grid */}
|
|
<defs>
|
|
<pattern
|
|
id="grid-rad"
|
|
width="25"
|
|
height="25"
|
|
patternUnits="userSpaceOnUse"
|
|
>
|
|
<path
|
|
d="M 25 0 L 0 0 0 25"
|
|
fill="none"
|
|
stroke="#f8fafc"
|
|
strokeWidth="1"
|
|
/>
|
|
</pattern>
|
|
</defs>
|
|
<rect width="100%" height="100%" fill="url(#grid-rad)" />
|
|
|
|
{/* Axes */}
|
|
<line
|
|
x1="20"
|
|
y1="0"
|
|
x2="20"
|
|
y2="300"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
<line
|
|
x1="0"
|
|
y1={toPx(0, true)}
|
|
x2="300"
|
|
y2={toPx(0, true)}
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
|
|
{/* Phantom -sqrt(x) */}
|
|
<path
|
|
d={pathPhantom()}
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
strokeDasharray="4,4"
|
|
/>
|
|
|
|
{/* Real sqrt(x) */}
|
|
<path
|
|
d={pathSqrt()}
|
|
fill="none"
|
|
stroke="#4f46e5"
|
|
strokeWidth="3"
|
|
/>
|
|
|
|
{/* Line x-k */}
|
|
<path
|
|
d={pathLine()}
|
|
fill="none"
|
|
stroke="#64748b"
|
|
strokeWidth="2"
|
|
/>
|
|
|
|
{/* Points */}
|
|
{validSolutions.map((x) => (
|
|
<circle
|
|
key={`v-${x}`}
|
|
cx={toPx(x)}
|
|
cy={toPx(Math.sqrt(x), true)}
|
|
r="5"
|
|
fill="#10b981"
|
|
stroke="white"
|
|
strokeWidth="2"
|
|
/>
|
|
))}
|
|
{extraneousSolutions.map((x) => (
|
|
<circle
|
|
key={`e-${x}`}
|
|
cx={toPx(x)}
|
|
cy={toPx(-Math.sqrt(x), true)}
|
|
r="5"
|
|
fill="#f43f5e"
|
|
stroke="white"
|
|
strokeWidth="2"
|
|
/>
|
|
))}
|
|
</svg>
|
|
<div className="absolute top-2 right-2 text-xs font-bold text-indigo-600">
|
|
y = √x
|
|
</div>
|
|
<div className="absolute bottom-10 right-2 text-xs font-bold text-slate-500">
|
|
y = x - {k}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RadicalSolutionWidget;
|