Files
100prisoners/src/components/BoxTable.tsx

129 lines
4.7 KiB
TypeScript

import {classNames, range} from "@/utils/helpers";
import BoxGraphic from "@/components/BoxGraphic";
import Xarrow from "react-xarrows";
import Chance from "chance";
import {useMemo, useState} from "react";
const chance = new Chance();
const colors = [
"#EF4444",
"#F97316",
"#F59E0B",
"#EAB308",
"#84CC16",
"#22C55E",
"#10B981",
"#14B8A6",
"#06B6D4",
"#0EA5E9",
"#3B82F6",
"#6366F1",
"#8B5CF6",
"#A855F7",
"#D946EF",
"#EC4899",
"#F43F5E",
]
const smartShuffle = (n: number[]): number[] => {
let shuffled: number[] | null = null;
while (shuffled == null || shuffled.some((v, i) => v == i + 1)) {
shuffled = chance.shuffle(n)
}
return shuffled;
}
const BoxTable = () => {
const [count, setCount] = useState<number>(16);
const {sources, boxes, boxesBySource} = useMemo(() => {
const sources: number[] = range(1, count);
const destinations: number[] = smartShuffle(sources);
const boxes: [number, number][] = sources.map((e, i) => [e, destinations[i] as number]);
const boxesBySource: Record<number, number> = Object.fromEntries(boxes);
return {sources, destinations, boxes, boxesBySource};
}, [count])
const extractLoop = (start: number): number[] => {
const results: number[] = [];
results.push(start);
while (true) {
const last = results[results.length - 1] as number;
const next = boxesBySource[last] as number;
if (next == start) break;
results.push(next)
}
return results;
}
const [filteredBoxes, setFilteredBoxes] = useState<Record<number, boolean>>(Object.fromEntries(
sources.map(n => [n, false])
));
const [enabledLines, setEnabledLines] = useState<Record<number, boolean>>(Object.fromEntries(
sources.map(n => [n, false])
));
const [isFiltered, setFiltered] = useState(false);
const toggleLines = (from: number) => {
hideLine(from);
const newState = !isFiltered;
setFiltered(newState);
showLine(from, newState);
const loop = Object.fromEntries(extractLoop(from).map(n => [n, false]));
setFilteredBoxes((prev) => {
const outsideLoop = Object.fromEntries(Object.entries(prev).map(([n]) => [n, newState]))
return {...outsideLoop, ...loop};
})
}
const showLine = (from: number, showLess: boolean | null = null) => {
const loop = Object.fromEntries(extractLoop(from).slice(0, (showLess ?? isFiltered) ? 15 : 3).map(n => [n, true]));
setEnabledLines((prev) => ({...prev, ...loop}))
}
const hideLine = (from: number) => {
const loop = Object.fromEntries(extractLoop(from).slice(0, isFiltered ? 15 : 3).map(n => [n, false]));
setEnabledLines((prev) => ({...prev, ...loop}))
}
const getColor = (seed: any) => {
const chance = Chance(seed);
return chance.pickone(colors);
}
const columns = Math.ceil(Math.sqrt(boxes.length));
return (
<div className="flex flex-col items-center justify-center w-full">
<div className="grid gap-x-4 gap-y-1 max-w-full"
style={{gridTemplateColumns: `repeat(${Math.max(3, columns)}, minmax(0, 1fr))`}}>
{boxes.map(([source, destination]) =>
<div key={source}
className={classNames("col-span-1 aspect-square", isFiltered && filteredBoxes[source] ? "opacity-10 pointer-events-none" : "")}
onClick={() => toggleLines(source)} onMouseEnter={() => showLine(source)}
onMouseLeave={() => hideLine(source)}>
<div
className="box flex items-center justify-center aspect-square relative">
<div
className="text flex items-center justify-center w-full h-full text-[150%] absolute pt-[30%] cursor-pointer">{destination}</div>
<BoxGraphic id={source.toString()}
className="w-full transition-all cursor-pointer relative z-30">
{source}
</BoxGraphic>
{enabledLines[source] ?
<Xarrow path="smooth" color={getColor(source)} start={source.toString()}
end={destination.toString()}
zIndex={50 + source} divContainerProps={{className: "arrow"}}/> : null}
</div>
</div>
)}
</div>
</div>
);
}
export default BoxTable;