Spaces:
Running
Running
| <html> | |
| <head> | |
| <title>Isometric SVG Grid with Sound and Responsive Layout</title> | |
| <style> | |
| html, body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| height: 100%; | |
| width: 100%; | |
| background-color: #333; | |
| } | |
| svg { | |
| display: block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <svg id="svgGrid" width="100%" height="100%"></svg> | |
| <script> | |
| (function() { | |
| const svgNS = "http://www.w3.org/2000/svg"; | |
| const svg = document.getElementById('svgGrid'); | |
| const gridSize = 20; // 20x20 grid | |
| const tileSize = 40; // Base size for each tile | |
| const tiles = []; | |
| let lastClicked = null; // Keep track of the last clicked tile | |
| let pathTiles = []; // Tiles in the current path | |
| // Create AudioContext for playing sound | |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| // Function to play click sound | |
| function playClickSound() { | |
| const oscillator = audioContext.createOscillator(); | |
| const gainNode = audioContext.createGain(); | |
| oscillator.connect(gainNode); | |
| gainNode.connect(audioContext.destination); | |
| oscillator.type = 'sine'; | |
| oscillator.frequency.setValueAtTime(440, audioContext.currentTime); // A4 note | |
| gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); | |
| oscillator.start(); | |
| oscillator.stop(audioContext.currentTime + 0.1); // Play for 0.1 seconds | |
| } | |
| // Initialize the grid | |
| initGrid(); | |
| // Add event listener for window resize | |
| window.addEventListener('resize', onWindowResize); | |
| function initGrid() { | |
| // Remove any existing tiles from the SVG | |
| while (svg.firstChild) { | |
| svg.removeChild(svg.firstChild); | |
| } | |
| // Initialize tiles array | |
| for (let y = 0; y < gridSize; y++) { | |
| tiles[y] = tiles[y] || []; | |
| for (let x = 0; x < gridSize; x++) { | |
| const tileData = tiles[y][x] || { | |
| x: x, | |
| y: y, | |
| element: null, | |
| defaultColor: '#ccc', | |
| isPath: false, | |
| isClicked: false, // Indicates if the tile has been clicked | |
| f: 0, | |
| g: 0, | |
| h: 0, | |
| parent: null, | |
| }; | |
| // Create the polygon element if it doesn't exist | |
| if (!tileData.element) { | |
| const polygon = document.createElementNS(svgNS, 'polygon'); | |
| polygon.setAttribute('fill', tileData.defaultColor); | |
| polygon.setAttribute('stroke', '#999'); | |
| polygon.setAttribute('stroke-width', '1'); | |
| polygon.style.cursor = 'pointer'; | |
| // Randomly spawn obstacles (approximately 1 in 3 tiles) | |
| if (Math.random() < 1 / 3) { | |
| const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'); | |
| tileData.defaultColor = randomColor; | |
| tileData.isClicked = true; // Mark tile as clicked (obstacle) | |
| polygon.setAttribute('fill', randomColor); | |
| } | |
| // Add event listener for interactivity | |
| polygon.addEventListener('click', function() { | |
| // Play sound | |
| playClickSound(); | |
| const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'); | |
| tileData.defaultColor = randomColor; | |
| polygon.setAttribute('fill', randomColor); | |
| // Highlight the path from the last clicked tile | |
| if (lastClicked) { | |
| clearPath(); // Clear previous path | |
| resetTiles(); // Reset tile properties | |
| // Mark the current tile as the end tile | |
| const startTile = lastClicked; | |
| const endTile = tileData; | |
| // Temporarily set isClicked to false for pathfinding | |
| const startWasClicked = startTile.isClicked; | |
| const endWasClicked = endTile.isClicked; | |
| startTile.isClicked = false; | |
| endTile.isClicked = false; | |
| const path = findPathAStar(startTile, endTile); | |
| if (path.length > 0) { | |
| animatePath(path); | |
| } else { | |
| alert('No path found!'); | |
| } | |
| // After pathfinding, restore isClicked status | |
| startTile.isClicked = startWasClicked; | |
| endTile.isClicked = endWasClicked; | |
| } | |
| // Mark the tile as clicked after pathfinding | |
| tileData.isClicked = true; | |
| lastClicked = tileData; // Update last clicked tile | |
| }); | |
| tileData.element = polygon; | |
| svg.appendChild(polygon); | |
| } | |
| tiles[y][x] = tileData; | |
| } | |
| } | |
| // Update positions of tiles | |
| updateTilePositions(); | |
| } | |
| function updateTilePositions() { | |
| const width = window.innerWidth; | |
| const height = window.innerHeight; | |
| // Adjust tileSize to fit the screen | |
| const scaleX = width / (gridSize * tileSize); | |
| const scaleY = height / (gridSize * tileSize / 2); | |
| const scale = Math.min(scaleX, scaleY); | |
| const adjustedTileSize = tileSize * scale; | |
| // Calculate grid dimensions | |
| const gridWidth = gridSize * adjustedTileSize; | |
| const gridHeight = gridSize * adjustedTileSize / 2; | |
| // Calculate offsets to center the grid | |
| const offsetX = (width - gridWidth) / 2; | |
| const offsetY = (height - gridHeight) / 2; | |
| // Origin point for the grid | |
| const originX = offsetX + gridWidth / 2; | |
| const originY = offsetY; | |
| for (let y = 0; y < gridSize; y++) { | |
| for (let x = 0; x < gridSize; x++) { | |
| const tileData = tiles[y][x]; | |
| // Calculate the position of each tile | |
| const isoX = (x - y) * (adjustedTileSize / 2); | |
| const isoY = (x + y) * (adjustedTileSize / 4); | |
| // Coordinates for the diamond shape | |
| const points = [ | |
| { x: originX + isoX, y: originY + isoY }, | |
| { x: originX + isoX + adjustedTileSize / 2, y: originY + isoY + adjustedTileSize / 4 }, | |
| { x: originX + isoX, y: originY + isoY + adjustedTileSize / 2 }, | |
| { x: originX + isoX - adjustedTileSize / 2, y: originY + isoY + adjustedTileSize / 4 }, | |
| ]; | |
| tileData.element.setAttribute('points', points.map(p => `${p.x},${p.y}`).join(' ')); | |
| } | |
| } | |
| } | |
| function onWindowResize() { | |
| updateTilePositions(); | |
| } | |
| // Function to reset tile properties before pathfinding | |
| function resetTiles() { | |
| for (let y = 0; y < gridSize; y++) { | |
| for (let x = 0; x < gridSize; x++) { | |
| const tile = tiles[y][x]; | |
| tile.f = 0; | |
| tile.g = 0; | |
| tile.h = 0; | |
| tile.parent = null; | |
| } | |
| } | |
| } | |
| // Function to find the shortest path using A* algorithm | |
| function findPathAStar(start, end) { | |
| const openList = []; | |
| const closedList = []; | |
| openList.push(start); | |
| while (openList.length > 0) { | |
| // Find the tile with the lowest f value | |
| let lowestIndex = 0; | |
| for (let i = 0; i < openList.length; i++) { | |
| if (openList[i].f < openList[lowestIndex].f) { | |
| lowestIndex = i; | |
| } | |
| } | |
| const currentTile = openList[lowestIndex]; | |
| // If we've reached the end tile, reconstruct the path | |
| if (currentTile === end) { | |
| const path = []; | |
| let curr = currentTile; | |
| path.push(curr); // Include the end tile | |
| while (curr.parent) { | |
| curr = curr.parent; | |
| path.push(curr); | |
| } | |
| path.reverse(); | |
| return path; | |
| } | |
| // Move current tile from open to closed list | |
| openList.splice(lowestIndex, 1); | |
| closedList.push(currentTile); | |
| // Get neighbors | |
| const neighbors = getNeighbors(currentTile); | |
| for (let neighbor of neighbors) { | |
| if (closedList.includes(neighbor) || (neighbor.isClicked && neighbor !== end)) { | |
| // Ignore the neighbor which is already evaluated or clicked (except the end tile) | |
| continue; | |
| } | |
| const tentative_gScore = currentTile.g + 1; | |
| if (!openList.includes(neighbor)) { | |
| // Discover a new node | |
| neighbor.g = tentative_gScore; | |
| neighbor.h = heuristic(neighbor, end); | |
| neighbor.f = neighbor.g + neighbor.h; | |
| neighbor.parent = currentTile; | |
| openList.push(neighbor); | |
| } else if (tentative_gScore < neighbor.g) { | |
| // This is a better path | |
| neighbor.g = tentative_gScore; | |
| neighbor.f = neighbor.g + neighbor.h; | |
| neighbor.parent = currentTile; | |
| } | |
| } | |
| } | |
| // No path found | |
| return []; | |
| } | |
| // Heuristic function (Manhattan distance) | |
| function heuristic(a, b) { | |
| return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); | |
| } | |
| // Function to get neighbors of a tile | |
| function getNeighbors(tile) { | |
| const neighbors = []; | |
| const dirs = [ | |
| { x: 0, y: -1 }, // Up | |
| { x: 1, y: 0 }, // Right | |
| { x: 0, y: 1 }, // Down | |
| { x: -1, y: 0 }, // Left | |
| // Uncomment below for diagonal movement | |
| // { x: -1, y: -1 }, | |
| // { x: 1, y: -1 }, | |
| // { x: 1, y: 1 }, | |
| // { x: -1, y: 1 }, | |
| ]; | |
| for (let dir of dirs) { | |
| const nx = tile.x + dir.x; | |
| const ny = tile.y + dir.y; | |
| if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) { | |
| neighbors.push(tiles[ny][nx]); | |
| } | |
| } | |
| return neighbors; | |
| } | |
| // Function to animate the path | |
| function animatePath(path) { | |
| pathTiles = path; | |
| let index = 0; | |
| function highlightNextTile() { | |
| if (index < path.length) { | |
| const tile = path[index]; | |
| // Skip tiles that were clicked (they keep their color) | |
| if (!tile.isClicked || tile === path[0] || tile === path[path.length - 1]) { | |
| tile.element.setAttribute('fill', '#888'); // Highlight color | |
| tile.isPath = true; | |
| } | |
| index++; | |
| setTimeout(highlightNextTile, 100); // Adjust the delay as needed | |
| } | |
| } | |
| highlightNextTile(); | |
| } | |
| // Function to clear the previous path | |
| function clearPath() { | |
| pathTiles.forEach(tile => { | |
| // Only reset tiles that are not the clicked ones | |
| if (!tile.isClicked || tile === pathTiles[0] || tile === pathTiles[pathTiles.length - 1]) { | |
| tile.element.setAttribute('fill', tile.defaultColor); | |
| tile.isPath = false; | |
| } | |
| }); | |
| pathTiles = []; | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |