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> | |