Add Cube Coloring interactive
This commit is contained in:
parent
7a166fc8d0
commit
9a127a5e22
7 changed files with 1160 additions and 22 deletions
793
package-lock.json
generated
793
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -39,6 +39,8 @@
|
|||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@react-three/drei": "^9.122.0",
|
||||
"@react-three/fiber": "^8.18.0",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
|
|
@ -61,6 +63,7 @@
|
|||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.160.1",
|
||||
"vaul": "^0.9.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import ParityBitsGamePage from './pages/ParityBitsGamePage';
|
|||
import CrapsGamePage from './pages/CrapsGamePage';
|
||||
import NeighborSumAvoidancePage from './pages/NeighborSumAvoidancePage';
|
||||
import BurnsidesLemmaPage from './pages/BurnsidesLemmaPage';
|
||||
import CubeColoringPage from './pages/CubeColoringPage';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
|
@ -85,6 +86,7 @@ const App = () => (
|
|||
<Route path="/themes/discrete-math/graphs" element={<Graphs />} />
|
||||
<Route path="/themes/discrete-math/graphs/neighbor-sum-avoidance" element={<NeighborSumAvoidancePage />} />
|
||||
<Route path="/themes/discrete-math/structures/burnsides-lemma" element={<BurnsidesLemmaPage />} />
|
||||
<Route path="/themes/discrete-math/structures/cube-coloring" element={<CubeColoringPage />} />
|
||||
<Route path="/themes/social-choice" element={<SocialChoice />} />
|
||||
<Route path="/themes/advanced-algorithms" element={<AdvancedAlgorithms />} />
|
||||
<Route path="/themes/data-structures" element={<DataStructures />} />
|
||||
|
|
|
|||
345
src/components/CubeColoring.tsx
Normal file
345
src/components/CubeColoring.tsx
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import SocialShare from './SocialShare';
|
||||
import { Box, RotateCw, ArrowRightLeft } from 'lucide-react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
interface CubeColoringProps {
|
||||
showSocialShare?: boolean;
|
||||
shareUrl?: string;
|
||||
}
|
||||
|
||||
const colorPalette = [
|
||||
'#ef4444', // red
|
||||
'#3b82f6', // blue
|
||||
'#22c55e', // green
|
||||
'#eab308', // yellow
|
||||
'#a855f7', // purple
|
||||
];
|
||||
|
||||
// Face indices: 0=top, 1=bottom, 2=front, 3=back, 4=right, 5=left
|
||||
const faceNames = ['Top', 'Bottom', 'Front', 'Back', 'Right', 'Left'];
|
||||
|
||||
// Rotation definitions: each rotation maps face indices [0,1,2,3,4,5] to new positions
|
||||
const rotations = [
|
||||
{ name: 'Identity', permutation: [0, 1, 2, 3, 4, 5], description: 'No rotation' },
|
||||
{ name: 'Y-90° (vertical)', permutation: [0, 1, 4, 5, 3, 2], description: 'Front→Right→Back→Left' },
|
||||
{ name: 'Y-180°', permutation: [0, 1, 3, 2, 5, 4], description: 'Front↔Back, Right↔Left' },
|
||||
{ name: 'Y-270°', permutation: [0, 1, 5, 4, 2, 3], description: 'Front→Left→Back→Right' },
|
||||
{ name: 'X-90° (horizontal)', permutation: [2, 3, 1, 0, 4, 5], description: 'Top→Front→Bottom→Back' },
|
||||
{ name: 'X-180°', permutation: [1, 0, 3, 2, 4, 5], description: 'Top↔Bottom, Front↔Back' },
|
||||
{ name: 'X-270°', permutation: [3, 2, 0, 1, 4, 5], description: 'Top→Back→Bottom→Front' },
|
||||
{ name: 'Z-90° (front-back)', permutation: [4, 5, 2, 3, 1, 0], description: 'Top→Right→Bottom→Left' },
|
||||
{ name: 'Z-180°', permutation: [1, 0, 2, 3, 5, 4], description: 'Top↔Bottom, Right↔Left' },
|
||||
{ name: 'Z-270°', permutation: [5, 4, 2, 3, 0, 1], description: 'Top→Left→Bottom→Right' },
|
||||
];
|
||||
|
||||
interface PaintableCubeProps {
|
||||
colors: string[];
|
||||
onFaceClick: (faceIndex: number) => void;
|
||||
}
|
||||
|
||||
function PaintableCube({ colors, onFaceClick }: PaintableCubeProps) {
|
||||
const meshRef = useRef<THREE.Mesh>(null);
|
||||
|
||||
useFrame(() => {
|
||||
if (meshRef.current) {
|
||||
meshRef.current.rotation.y += 0.005;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<boxGeometry args={[2, 2, 2]} />
|
||||
{colors.map((color, i) => (
|
||||
<meshStandardMaterial key={i} attach={`material-${i}`} color={color} />
|
||||
))}
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
function PermutationCube({ permutation }: { permutation: number[] }) {
|
||||
const meshRef = useRef<THREE.Mesh>(null);
|
||||
|
||||
useFrame(() => {
|
||||
if (meshRef.current) {
|
||||
meshRef.current.rotation.y += 0.005;
|
||||
}
|
||||
});
|
||||
|
||||
// Create materials based on permutation
|
||||
const baseMaterials = [
|
||||
new THREE.MeshStandardMaterial({ color: '#ef4444' }), // red - top
|
||||
new THREE.MeshStandardMaterial({ color: '#3b82f6' }), // blue - bottom
|
||||
new THREE.MeshStandardMaterial({ color: '#22c55e' }), // green - front
|
||||
new THREE.MeshStandardMaterial({ color: '#eab308' }), // yellow - back
|
||||
new THREE.MeshStandardMaterial({ color: '#a855f7' }), // purple - right
|
||||
new THREE.MeshStandardMaterial({ color: '#f97316' }), // orange - left
|
||||
];
|
||||
|
||||
// Apply permutation: materials[i] shows the color that should be at position i
|
||||
const permutedMaterials = permutation.map(originalIndex => baseMaterials[originalIndex]);
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<boxGeometry args={[2, 2, 2]} />
|
||||
{permutedMaterials.map((material, i) => (
|
||||
<primitive key={i} object={material} attach={`material-${i}`} />
|
||||
))}
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
const CubeColoring = ({ showSocialShare = false, shareUrl }: CubeColoringProps) => {
|
||||
const [faceColors, setFaceColors] = useState<string[]>(Array(6).fill(colorPalette[0]));
|
||||
const [selectedColor, setSelectedColor] = useState(colorPalette[0]);
|
||||
const [selectedPermutation, setSelectedPermutation] = useState<number[]>([0, 1, 2, 3, 4, 5]);
|
||||
const [selectedRotation, setSelectedRotation] = useState(0);
|
||||
|
||||
const handleFaceClick = (faceIndex: number) => {
|
||||
const newColors = [...faceColors];
|
||||
newColors[faceIndex] = selectedColor;
|
||||
setFaceColors(newColors);
|
||||
};
|
||||
|
||||
const handlePermutationChange = (position: number, value: string) => {
|
||||
const newPerm = [...selectedPermutation];
|
||||
newPerm[position] = parseInt(value);
|
||||
setSelectedPermutation(newPerm);
|
||||
};
|
||||
|
||||
const permutationToRotation = () => {
|
||||
const match = rotations.findIndex(r =>
|
||||
r.permutation.every((val, idx) => val === selectedPermutation[idx])
|
||||
);
|
||||
return match >= 0 ? rotations[match] : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Box className="w-6 h-6" />
|
||||
Burnside's Lemma: Cube Coloring
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
How many distinct cubes can you make by painting each face in one of 5 colors?
|
||||
Two cubes are the same if one can be rotated to look like the other.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-8">
|
||||
{/* 3D Cube Arena */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Paint Your Cube</h3>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="h-[300px] border border-border rounded-lg bg-muted/20">
|
||||
<Canvas camera={{ position: [4, 4, 4] }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[10, 10, 5]} intensity={1} />
|
||||
<PaintableCube colors={faceColors} onFaceClick={handleFaceClick} />
|
||||
<OrbitControls enableZoom={true} />
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Select Color</label>
|
||||
<div className="flex gap-2">
|
||||
{colorPalette.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
onClick={() => setSelectedColor(color)}
|
||||
className={`w-12 h-12 rounded-lg border-2 transition-all ${
|
||||
selectedColor === color ? 'border-foreground scale-110' : 'border-border'
|
||||
}`}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Face Colors</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{faceNames.map((name, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 p-2 bg-muted/50 rounded">
|
||||
<div
|
||||
className="w-6 h-6 rounded border border-border"
|
||||
style={{ backgroundColor: faceColors[idx] }}
|
||||
/>
|
||||
<span className="text-sm">{name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Permutation to Rotation */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<ArrowRightLeft className="w-5 h-5" />
|
||||
Permutation → Rotation
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card className="bg-muted/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Pick a Permutation</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Choose where each face (0-5) goes
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{selectedPermutation.map((value, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="w-20">
|
||||
Face {idx}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<Select
|
||||
value={value.toString()}
|
||||
onValueChange={(v) => handlePermutationChange(idx, v)}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[0, 1, 2, 3, 4, 5].map((n) => (
|
||||
<SelectItem key={n} value={n.toString()}>
|
||||
{n}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-xs text-muted-foreground">({faceNames[idx]})</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-muted/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Corresponding Rotation</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Visualize the rotation
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{permutationToRotation() ? (
|
||||
<>
|
||||
<div className="text-center space-y-2">
|
||||
<Badge className="text-lg px-4 py-2">
|
||||
{permutationToRotation()!.name}
|
||||
</Badge>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{permutationToRotation()!.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-[200px] border border-border rounded-lg bg-background/50">
|
||||
<Canvas camera={{ position: [4, 4, 4] }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[10, 10, 5]} intensity={1} />
|
||||
<PermutationCube permutation={selectedPermutation} />
|
||||
<OrbitControls enableZoom={true} />
|
||||
</Canvas>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>Invalid permutation</p>
|
||||
<p className="text-xs mt-1">Each number 0-5 must appear exactly once</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rotation to Permutation */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<RotateCw className="w-5 h-5" />
|
||||
Rotation → Permutation
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card className="bg-muted/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Corresponding Permutation</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
How faces move with this rotation
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{rotations[selectedRotation].permutation.map((target, source) => (
|
||||
<div key={source} className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="w-20">
|
||||
Face {source}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<Badge className="w-20">Face {target}</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{faceNames[source]} → {faceNames[target]}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-muted/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Pick a Rotation</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Select a cube rotation
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Select
|
||||
value={selectedRotation.toString()}
|
||||
onValueChange={(v) => setSelectedRotation(parseInt(v))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{rotations.map((rot, idx) => (
|
||||
<SelectItem key={idx} value={idx.toString()}>
|
||||
{rot.name} - {rot.description}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="h-[200px] border border-border rounded-lg bg-background/50">
|
||||
<Canvas camera={{ position: [4, 4, 4] }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[10, 10, 5]} intensity={1} />
|
||||
<PermutationCube permutation={rotations[selectedRotation].permutation} />
|
||||
<OrbitControls enableZoom={true} />
|
||||
</Canvas>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{showSocialShare && (
|
||||
<SocialShare
|
||||
url={shareUrl || window.location.href}
|
||||
title="Burnside's Lemma - Cube Coloring"
|
||||
description="Explore cube rotations and permutations with Burnside's Lemma"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CubeColoring;
|
||||
|
|
@ -261,6 +261,15 @@ const allInteractives: Interactive[] = [
|
|||
path: '/themes/discrete-math/structures/burnsides-lemma',
|
||||
theme: 'Discrete Math',
|
||||
dateAdded: '2025-01-22'
|
||||
},
|
||||
{
|
||||
id: 'cube-coloring',
|
||||
title: "Burnside's Lemma: Cube Coloring",
|
||||
description: 'Explore cube rotations and their correspondence with permutations. Paint a 3D cube and understand the rotational symmetry group.',
|
||||
tags: ['group-theory', 'symmetry', 'cube', 'rotations', 'permutations', 'algebra', '3d'],
|
||||
path: '/themes/discrete-math/structures/cube-coloring',
|
||||
theme: 'Discrete Math',
|
||||
dateAdded: '2025-01-22'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
17
src/pages/CubeColoringPage.tsx
Normal file
17
src/pages/CubeColoringPage.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import Layout from '@/components/Layout';
|
||||
import CubeColoring from '@/components/CubeColoring';
|
||||
|
||||
const CubeColoringPage = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<CubeColoring
|
||||
showSocialShare={true}
|
||||
shareUrl={window.location.href}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CubeColoringPage;
|
||||
|
|
@ -5,6 +5,7 @@ import { Badge } from '@/components/ui/badge';
|
|||
import Layout from '@/components/Layout';
|
||||
import { useState } from 'react';
|
||||
import BurnsidesLemma from '@/components/BurnsidesLemma';
|
||||
import CubeColoring from '@/components/CubeColoring';
|
||||
|
||||
interface Interactive {
|
||||
id: string;
|
||||
|
|
@ -24,6 +25,14 @@ const interactives: Interactive[] = [
|
|||
path: '/themes/discrete-math/structures/burnsides-lemma',
|
||||
component: BurnsidesLemma
|
||||
},
|
||||
{
|
||||
id: 'cube-coloring',
|
||||
title: "Burnside's Lemma: Cube Coloring",
|
||||
description: 'Explore cube rotations and their correspondence with permutations. Paint a 3D cube and understand the rotational symmetry group.',
|
||||
tags: ['group-theory', 'symmetry', 'cube', 'rotations', 'permutations', 'algebra', '3d'],
|
||||
path: '/themes/discrete-math/structures/cube-coloring',
|
||||
component: CubeColoring
|
||||
},
|
||||
];
|
||||
|
||||
const Structures = () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue