Geography
import React, { useState, useMemo } from 'react'; import { Plus, Minus, Equal, Eye, EyeOff, RotateCcw, AlertCircle, ArrowDown, ArrowRight, X } from 'lucide-react'; const App = () => { // State const [inputMode, setInputMode] = useState('improper'); // 'improper' or 'mixed' // Mixed Number State (Whole numbers) const [whole1, setWhole1] = useState(0); const [whole2, setWhole2] = useState(0); // Numerator/Denominator State - NOW INDEPENDENT const [num1, setNum1] = useState(1); const [denom1, setDenom1] = useState(2); const [num2, setNum2] = useState(1); const [denom2, setDenom2] = useState(3); const [operation, setOperation] = useState('add'); // 'add' or 'subtract' const [showAnswer, setShowAnswer] = useState(false); // --- Helper Math Functions --- const gcd = (a, b) => b === 0 ? a : gcd(b, a % b); const lcm = (a, b) => (a * b) / gcd(a, b); // --- Derived Values for Visualization --- // 1. Calculate Total Numerators (handling mixed numbers) const totalNum1 = inputMode === 'mixed' ? (whole1 * denom1) + num1 : num1; const totalNum2 = inputMode === 'mixed' ? (whole2 * denom2) + num2 : num2; // 2. Find Common Denominator & Multipliers const commonDenom = lcm(denom1, denom2); const multiplier1 = commonDenom / denom1; const multiplier2 = commonDenom / denom2; // 3. Calculate Scaled Numerators (Equivalent Fractions) const scaledNum1 = totalNum1 * multiplier1; const scaledNum2 = totalNum2 * multiplier2; // 4. Validation & Safety Checks // We strictly limit the common denominator to prevent SVG rendering crashes const isCommonDenomTooLarge = commonDenom > 64; const totalCircles1 = Math.ceil(scaledNum1 / commonDenom); const totalCircles2 = Math.ceil(scaledNum2 / commonDenom); const isTooComplex = isCommonDenomTooLarge || totalCircles1 > 15 || totalCircles2 > 15; const isValid = denom1 > 0 && denom2 > 0 && totalNum1 >= 0 && totalNum2 >= 0 && !isTooComplex; // 5. Calculate Final Result const resultTotalNum = operation === 'add' ? scaledNum1 + scaledNum2 : scaledNum1 - scaledNum2; const isNegative = resultTotalNum < 0; // Handlers const handleReset = () => { setNum1(1); setDenom1(2); setWhole1(0); setNum2(1); setDenom2(3); setWhole2(0); setOperation('add'); setShowAnswer(false); }; const toggleInputMode = () => { if (inputMode === 'improper') { // Improper -> Mixed setWhole1(Math.floor(num1 / denom1)); setNum1(num1 % denom1); setWhole2(Math.floor(num2 / denom2)); setNum2(num2 % denom2); setInputMode('mixed'); } else { // Mixed -> Improper setNum1((whole1 * denom1) + num1); setWhole1(0); setNum2((whole2 * denom2) + num2); setWhole2(0); setInputMode('improper'); } }; // Helper to simplify fraction const simplify = (n, d) => { if (d === 0) return { n: 0, d: 0 }; const common = gcd(Math.abs(n), Math.abs(d)); return { n: n / common, d: d / common }; }; const simplified = simplify(resultTotalNum, commonDenom); // Helper for Mixed Number Result Display const getMixedResult = () => { const w = Math.floor(resultTotalNum / commonDenom); const r = resultTotalNum % commonDenom; return { w, r }; }; const mixedResult = getMixedResult(); return (
Fraction Action
Add and subtract fractions with different denominators. See how we find a common denominator to solve the puzzle!
{/* Main Control Panel */}Numbers too complex to visualize!
The common denominator for {denom1} and {denom2} is {commonDenom}. Drawing a circle with {commonDenom} slices is very difficult to see. Try denominators like 2, 3, 4, 6, 8, or 12.
We can't {operation} these directly because the slices are different sizes. We need to find a common denominator (LCM) of {denom1} and {denom2}, which is {commonDenom}.
); }; // --- Sub Components --- const ConversionVisual = ({ originalNum, originalDenom, multiplier, commonDenom, scaledNum, color, textColor }) => { return (
) } const FractionInput = ({ label, isMixed, whole, setWhole, num, setNum, denom, setDenom, color, bgColor, borderColor }) => { return (
); }; // Single SVG Pie Chart Component const PieChart = ({ num, denom, color, size = 100 }) => { if (denom === 0) return null; const circlesNeeded = Math.ceil(num / denom) || 1; const charts = []; for (let i = 0; i < circlesNeeded; i++) { const slicesForThisCircle = Math.min(denom, num - (i * denom)); charts.push( ); } return (
); }; // The Combined Result Chart Logic const ResultPieChart = ({ num1, num2, denom, operation, size = 100 }) => { if (denom === 0) return null; const resultNum = operation === 'add' ? num1 + num2 : num1 - num2; const circlesNeeded = Math.ceil(Math.abs(resultNum) / denom) || 1; const charts = []; if (resultNum < 0) { return ; } for (let i = 0; i < circlesNeeded; i++) { const startCount = i * denom; const blueSlicesInThisCircle = Math.max(0, Math.min(denom, num1 - startCount)); let orangeSlicesInThisCircle = 0; if (operation === 'add') { const rangeStart = startCount; const rangeEnd = startCount + denom; const orangeStart = num1; const orangeEnd = num1 + num2; const overlapStart = Math.max(rangeStart, orangeStart); const overlapEnd = Math.min(rangeEnd, orangeEnd); orangeSlicesInThisCircle = Math.max(0, overlapEnd - overlapStart); } charts.push( ); } return (
); }; // Low-level SVG renderer for a single circle const SingleCircle = ({ filledSlices, totalSlices, color, size }) => { const center = size / 2; const radius = (size / 2) - 4; // padding const gridPaths = useMemo(() => { const paths = []; if (totalSlices === 1) return []; for (let i = 0; i < totalSlices; i++) { const angle = (i * 360) / totalSlices; const rad = (angle - 90) * (Math.PI / 180); const x = center + radius * Math.cos(rad); const y = center + radius * Math.sin(rad); paths.push( 30 ? 0.5 : 1} /> ); } return paths; }, [totalSlices, center, radius]); const renderFilled = () => { if (filledSlices <= 0) return null; if (filledSlices >= totalSlices) { return ; } const startAngle = -90; const endAngle = -90 + (filledSlices * 360 / totalSlices); const startRad = startAngle * (Math.PI / 180); const endRad = endAngle * (Math.PI / 180); const x1 = center + radius * Math.cos(startRad); const y1 = center + radius * Math.sin(startRad); const x2 = center + radius * Math.cos(endRad); const y2 = center + radius * Math.sin(endRad); const largeArcFlag = filledSlices > totalSlices / 2 ? 1 : 0; const pathData = [ `M ${center} ${center}`, `L ${x1} ${y1}`, `A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`, `Z` ].join(' '); return ; }; return ( {renderFilled()} {gridPaths} ); }; // Specialized circle that can show two colors (for addition visualization) const MixedSingleCircle = ({ totalSlices, blueCount, orangeCount, simpleFillCount, size }) => { const center = size / 2; const radius = (size / 2) - 4; const getSlicePath = (startIndex, count, colorClass) => { if (count <= 0) return null; if (count >= totalSlices) { return ; } const startAngle = -90 + (startIndex * 360 / totalSlices); const endAngle = startAngle + (count * 360 / totalSlices); const startRad = startAngle * (Math.PI / 180); const endRad = endAngle * (Math.PI / 180); const x1 = center + radius * Math.cos(startRad); const y1 = center + radius * Math.sin(startRad); const x2 = center + radius * Math.cos(endRad); const y2 = center + radius * Math.sin(endRad); const largeArcFlag = count > totalSlices / 2 ? 1 : 0; const pathData = [ `M ${center} ${center}`, `L ${x1} ${y1}`, `A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`, `Z` ].join(' '); return ; }; const gridPaths = useMemo(() => { const paths = []; if (totalSlices === 1) return []; for (let i = 0; i < totalSlices; i++) { const angle = (i * 360) / totalSlices; const rad = (angle - 90) * (Math.PI / 180); const x = center + radius * Math.cos(rad); const y = center + radius * Math.sin(rad); paths.push( 30 ? 0.5 : 1} className="opacity-50" /> ); } return paths; }, [totalSlices, center, radius]); return ( {simpleFillCount > 0 && getSlicePath(0, simpleFillCount, "fill-indigo-600")} {blueCount > 0 && getSlicePath(0, blueCount, "fill-blue-500")} {orangeCount > 0 && getSlicePath(blueCount, orangeCount, "fill-orange-500")} {gridPaths} ); }; export default App;
