<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0, user-scalable=no”>
<title>3D Maze Mapper Deluxe / Solver</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #050505;
font-family: ‘Segoe UI’, Roboto, Helvetica, Arial, sans-serif;
color: #ffffff;
touch-action: none;
}
#canvas-container {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
#ui-layer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
z-index: 2;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: max(20px, env(safe-area-inset-bottom));
background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
}
.controls {
pointer-events: auto;
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
padding: 10px;
width: 95%;
max-width: 600px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
label {
font-size: 0.7rem;
text-transform: uppercase;
color: #888;
font-weight: bold;
margin-left: 2px;
}
select, button {
background: rgba(40, 40, 50, 0.9);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 12px;
border-radius: 12px;
font-size: 0.9rem;
outline: none;
backdrop-filter: blur(5px);
-webkit-appearance: none;
}
button {
font-weight: 700;
text-transform: uppercase;
cursor: pointer;
transition: transform 0.1s, background 0.2s;
display: flex;
align-items: center;
justify-content: center;
min-width: 80px;
}
button:active {
transform: scale(0.96);
}
/* Toggle Button State */
button.active {
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
border-color: #fff;
}
#header {
position: absolute;
top: 20px;
left: 0;
width: 100%;
text-align: center;
z-index: 2;
pointer-events: none;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
}
h1 {
margin: 0;
font-size: 1.8rem;
font-weight: 800;
letter-spacing: 1px;
}
p {
margin: 5px 0 0 0;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
<script src=”https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js”></script>
</head>
<body>
<div id=”header”>
<h1 id=”titleText”>MAZE SOLVER</h1>
<p>Drag to Rotate • Pinch to Zoom</p>
</div>
<div id=”canvas-container”></div>
<div id=”ui-layer”>
<div class=”controls”>
<div class=”control-group”>
<label>Shape</label>
<select id=”shapeSelect”>
<option value=”cube”>Cube</option>
<option value=”sphere”>Sphere</option>
<option value=”cylinder”>Cylinder</option>
<option value=”torus”>Donut</option>
<option value=”knot” selected>Pretzel</option>
<option value=”dodecahedron”>D20</option>
<option value=”cone”>Cone</option>
</select>
</div>
<div class=”control-group”>
<label>Theme</label>
<select id=”themeSelect”>
<option value=”cyberpunk”>Cyberpunk</option>
<option value=”matrix”>Matrix</option>
<option value=”magma”>Magma</option>
<option value=”ice”>Ice</option>
<option value=”gold”>Gold</option>
</select>
</div>
<div class=”control-group”>
<label>Complexity</label>
<select id=”difficultySelect”>
<option value=”15″>Easy</option>
<option value=”25″ selected>Medium</option>
<option value=”45″>Hard</option>
<option value=”65″>Extreme</option>
</select>
</div>
<div class=”control-group”>
<label>Action</label>
<div style=”display:flex; gap:5px;”>
<button id=”regenerateBtn” style=”flex:1;”>New</button>
<button id=”solveBtn” style=”flex:1;”>Solve</button>
</div>
</div>
</div>
</div>
<script>
// — CONFIGURATION —
const THEMES = {
cyberpunk: { bg: ‘#050505’, path: ‘#00ffcc’, wall: ‘#000000’, glow: ‘#00ffcc’, solve: ‘#ff00ff’ },
matrix: { bg: ‘#000000’, path: ‘#00cc00’, wall: ‘#001100’, glow: ‘#00ff00’, solve: ‘#ffffff’ },
magma: { bg: ‘#1a0000’, path: ‘#ff3300’, wall: ‘#2a0000’, glow: ‘#ff4400’, solve: ‘#00ffff’ },
ice: { bg: ‘#000510’, path: ‘#0066cc’, wall: ‘#001122’, glow: ‘#0088ff’, solve: ‘#ffcc00’ },
gold: { bg: ‘#1a1000’, path: ‘#cca000’, wall: ‘#2a2000’, glow: ‘#ffaa00’, solve: ‘#9933ff’ }
};
let currentSettings = {
shape: ‘knot’,
theme: ‘cyberpunk’,
difficulty: 25,
showSolution: false
};
// Global State for Maze Data
let mazeData = {
grid: [],
solution: [],
cols: 0,
rows: 0
};
// — 1. MAZE ALGORITHMS —
// Generates the Grid and the Solution Path
function generateMazeData(cols, rows) {
// Ensure odd dimensions
if (cols % 2 === 0) cols++;
if (rows % 2 === 0) rows++;
// Initialize Grid (1 = Wall, 0 = Path)
let grid = Array(rows).fill().map(() => Array(cols).fill(1));
// Recursive Backtracking to carve maze
const directions = [[0,-2], [2,0], [0,2], [-2,0]];
function carve(x, y) {
grid[y][x] = 0;
const dirs = […directions].sort(() => Math.random() – 0.5);
for (let [dx, dy] of dirs) {
const nx = x + dx, ny = y + dy;
if (nx > 0 && nx < cols-1 && ny > 0 && ny < rows-1 && grid[ny][nx] === 1) {
grid[y + dy/2][x + dx/2] = 0;
carve(nx, ny);
}
}
}
// Start carving from top-left
carve(1, 1);
// Force end point at bottom right
grid[rows-2][cols-2] = 0;
// Solve it immediately
const solution = solveMaze(grid, cols, rows);
return { grid, solution, cols, rows };
}
function solveMaze(grid, cols, rows) {
const start = {x: 1, y: 1};
const end = {x: cols-2, y: rows-2};
let stack = [start];
let visited = new Set();
let parentMap = new Map();
visited.add(`${start.x},${start.y}`);
// DFS Solver
while (stack.length > 0) {
let current = stack.pop();
if (current.x === end.x && current.y === end.y) {
// Reconstruct path
let path = [];
let currKey = `${end.x},${end.y}`;
while (currKey) {
const [cx, cy] = currKey.split(‘,’).map(Number);
path.push({x: cx, y: cy});
currKey = parentMap.get(currKey);
}
return path.reverse();
}
// Check neighbors (Up, Right, Down, Left)
const neighbors = [
{x: current.x, y: current.y – 1},
{x: current.x + 1, y: current.y},
{x: current.x, y: current.y + 1},
{x: current.x – 1, y: current.y}
];
for (let n of neighbors) {
if (n.x >= 0 && n.x < cols && n.y >= 0 && n.y < rows && grid[n.y][n.x] === 0) {
const key = `${n.x},${n.y}`;
if (!visited.has(key)) {
visited.add(key);
parentMap.set(key, `${current.x},${current.y}`);
stack.push(n);
}
}
}
}
return []; // No path found
}
// Draws the current Maze Data to a Canvas
function drawMazeToCanvas(size, themeName, showSol) {
const { grid, solution, cols, rows } = mazeData;
const theme = THEMES[themeName];
const canvas = document.createElement(‘canvas’);
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext(‘2d’);
const cellW = size / cols;
const cellH = size / rows;
// 1. Draw Background (Walls)
ctx.fillStyle = theme.wall;
ctx.fillRect(0, 0, size, size);
// 2. Draw Paths
ctx.shadowBlur = 0; // Disable shadow for base path for crispness
ctx.fillStyle = theme.path;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
if (grid[y][x] === 0) {
ctx.fillRect(x * cellW, y * cellH, cellW + 1, cellH + 1);
}
}
}
// 3. Draw Glow Overlay (Optimization: Draw paths again with blur)
ctx.shadowBlur = 15;
ctx.shadowColor = theme.glow;
ctx.fillStyle = theme.path;
// Only draw a subset or simpler representation for glow if performance needed
// But for textures, we can afford it once per generation.
// 4. Draw Solution (if enabled)
if (showSol && solution.length > 0) {
ctx.beginPath();
ctx.strokeStyle = theme.solve;
ctx.lineWidth = cellW * 0.4; // 40% of cell width
ctx.lineCap = ’round’;
ctx.lineJoin = ’round’;
// Override shadow for the solution line to make it pop
ctx.shadowColor = theme.solve;
ctx.shadowBlur = 10;
// Move to start center
const startX = solution[0].x * cellW + cellW/2;
const startY = solution[0].y * cellH + cellH/2;
ctx.moveTo(startX, startY);
for (let i = 1; i < solution.length; i++) {
const px = solution[i].x * cellW + cellW/2;
const py = solution[i].y * cellH + cellH/2;
ctx.lineTo(px, py);
}
ctx.stroke();
}
// 5. Draw Border
ctx.shadowBlur = 0;
ctx.strokeStyle = theme.path;
ctx.lineWidth = cellW;
ctx.strokeRect(0, 0, size, size);
return canvas;
}
// — 2. THREE.JS SCENE SETUP —
const container = document.getElementById(‘canvas-container’);
let scene, camera, renderer, mesh, material;
let texture;
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
let autoRotate = true;
let ambientLight, pointLight;
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.02);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 3.5;
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// Lighting
ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(ambientLight);
pointLight = new THREE.PointLight(0xffffff, 1.5, 50);
pointLight.position.set(5, 5, 10);
scene.add(pointLight);
// Input Listeners
const canvas = renderer.domElement;
canvas.addEventListener(‘mousedown’, startDrag);
canvas.addEventListener(‘mousemove’, onDrag);
canvas.addEventListener(‘mouseup’, endDrag);
canvas.addEventListener(‘touchstart’, (e) => startDrag(e.touches[0]), {passive: false});
canvas.addEventListener(‘touchmove’, (e) => { e.preventDefault(); onDrag(e.touches[0]); }, {passive: false});
canvas.addEventListener(‘touchend’, endDrag);
// Initial Maze Generation
generateNewMaze();
updateVisualization();
window.addEventListener(‘resize’, onWindowResize);
requestAnimationFrame(animate);
}
function createGeometry(type) {
switch(type) {
case ‘cube’: return new THREE.BoxGeometry(1.8, 1.8, 1.8);
case ‘sphere’: return new THREE.SphereGeometry(1.4, 64, 64);
case ‘cylinder’: return new THREE.CylinderGeometry(1, 1, 2.5, 32);
case ‘cone’: return new THREE.ConeGeometry(1.2, 2.5, 32);
case ‘torus’: return new THREE.TorusGeometry(1.2, 0.5, 30, 60);
case ‘knot’: return new THREE.TorusKnotGeometry(1, 0.3, 128, 32);
case ‘dodecahedron’: return new THREE.DodecahedronGeometry(1.3);
default: return new THREE.BoxGeometry(1.5, 1.5, 1.5);
}
}
// 1. Generate Logical Data
function generateNewMaze() {
const difficulty = parseInt(currentSettings.difficulty);
mazeData = generateMazeData(difficulty, difficulty);
}
// 2. Render Texture & Mesh based on Data + Settings
function updateVisualization() {
const theme = THEMES[currentSettings.theme];
// UI Styling
document.body.style.backgroundColor = theme.bg;
document.querySelector(‘h1’).style.textShadow = `0 0 15px ${theme.glow}`;
document.querySelector(‘h1’).style.color = theme.path;
const btns = document.querySelectorAll(‘button’);
btns.forEach(b => {
b.style.backgroundColor = theme.path;
b.style.color = (theme.bg === ‘#000000’ || theme.bg === ‘#050505’) ? ‘black’ : ‘white’;
});
// Toggle Solve Button styling
const solveBtn = document.getElementById(‘solveBtn’);
if (currentSettings.showSolution) {
solveBtn.style.backgroundColor = theme.solve;
solveBtn.style.color = ‘black’;
solveBtn.innerText = “Hide”;
} else {
solveBtn.innerText = “Solve”;
}
scene.fog.color.set(theme.bg);
renderer.setClearColor(theme.bg);
pointLight.color.set(theme.glow);
// Cleanup
if (mesh) {
scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();
if(texture) texture.dispose();
}
// Draw Texture
const mazeCanvas = drawMazeToCanvas(1024, currentSettings.theme, currentSettings.showSolution);
texture = new THREE.CanvasTexture(mazeCanvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
if (currentSettings.shape === ‘torus’) texture.repeat.set(6, 2);
else if (currentSettings.shape === ‘knot’) texture.repeat.set(8, 1);
else if (currentSettings.shape === ‘cylinder’) texture.repeat.set(2, 1);
else texture.repeat.set(1, 1);
material = new THREE.MeshStandardMaterial({
map: texture,
emissive: 0xffffff,
emissiveMap: texture,
emissiveIntensity: 0.9,
roughness: 0.3,
metalness: 0.6,
flatShading: currentSettings.shape === ‘dodecahedron’
});
// Adjust emissive color for solution contrast
// We set the base emissive to white so the texture colors (which include the solution color) pop correctly
mesh = new THREE.Mesh(createGeometry(currentSettings.shape), material);
scene.add(mesh);
if (!isDragging) autoRotate = true;
}
// — 3. EVENT HANDLERS —
function startDrag(e) {
isDragging = true;
autoRotate = false;
previousMousePosition = { x: e.clientX, y: e.clientY };
}
function onDrag(e) {
if (!isDragging || !mesh) return;
const delta = {
x: e.clientX – previousMousePosition.x,
y: e.clientY – previousMousePosition.y
};
mesh.rotation.y += delta.x * 0.005;
mesh.rotation.x += delta.y * 0.005;
previousMousePosition = { x: e.clientX, y: e.clientY };
}
function endDrag() {
isDragging = false;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
if (mesh && autoRotate && !isDragging) {
mesh.rotation.x += 0.0015;
mesh.rotation.y += 0.0025;
}
renderer.render(scene, camera);
}
// Buttons
document.getElementById(‘regenerateBtn’).addEventListener(‘click’, () => {
currentSettings.showSolution = false; // Reset solver on new maze
generateNewMaze();
updateVisualization();
});
document.getElementById(‘solveBtn’).addEventListener(‘click’, () => {
currentSettings.showSolution = !currentSettings.showSolution;
updateVisualization(); // Re-render texture without regenerating grid
});
// Dropdowns
document.getElementById(‘shapeSelect’).addEventListener(‘change’, (e) => {
currentSettings.shape = e.target.value;
updateVisualization();
});
document.getElementById(‘themeSelect’).addEventListener(‘change’, (e) => {
currentSettings.theme = e.target.value;
updateVisualization();
});
document.getElementById(‘difficultySelect’).addEventListener(‘change’, (e) => {
currentSettings.difficulty = e.target.value;
generateNewMaze(); // Difficulty requires new grid
updateVisualization();
});
// Init
init();
</script>
</body>
</html>
