Initial commit: Echoes of the Ashes - Telegram RPG Bot
This commit is contained in:
603
web-map/map.js
Normal file
603
web-map/map.js
Normal file
@@ -0,0 +1,603 @@
|
||||
// Map visualization with pan, zoom, and interactive features
|
||||
let canvas, ctx;
|
||||
let mapData = { locations: [], connections: [], interactables: [], spawn_tables: {} };
|
||||
let viewOffset = { x: 0, y: 0 };
|
||||
let viewScale = 1.0;
|
||||
let isDragging = false;
|
||||
let lastMouse = { x: 0, y: 0 };
|
||||
let showLabels = true;
|
||||
let selectedLocation = null;
|
||||
|
||||
// Visual settings
|
||||
const gridSize = 100; // pixels per game unit
|
||||
const nodeRadius = 12;
|
||||
const colors = {
|
||||
background: '#0a0a1a',
|
||||
grid: '#1a1a2e',
|
||||
connection: '#3a3a6a',
|
||||
nodeSafe: '#4fc3f7',
|
||||
nodeLowDanger: '#ffa726',
|
||||
nodeMediumDanger: '#ff7043',
|
||||
nodeHighDanger: '#e53935',
|
||||
nodeSelected: '#00ff88',
|
||||
text: '#ffffff',
|
||||
label: '#e0e0e0'
|
||||
};
|
||||
|
||||
// Initialize
|
||||
window.addEventListener('load', async () => {
|
||||
canvas = document.getElementById('mapCanvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
// Load map data
|
||||
await loadMapData();
|
||||
|
||||
// Event listeners
|
||||
canvas.addEventListener('mousedown', onMouseDown);
|
||||
canvas.addEventListener('mousemove', onMouseMove);
|
||||
canvas.addEventListener('mouseup', onMouseUp);
|
||||
canvas.addEventListener('mouseleave', onMouseUp);
|
||||
canvas.addEventListener('wheel', onWheel);
|
||||
canvas.addEventListener('click', onClick);
|
||||
|
||||
// Touch event listeners for mobile
|
||||
canvas.addEventListener('touchstart', onTouchStart, { passive: false });
|
||||
canvas.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
canvas.addEventListener('touchend', onTouchEnd);
|
||||
|
||||
document.getElementById('zoomIn').addEventListener('click', () => zoom(1.2));
|
||||
document.getElementById('zoomOut').addEventListener('click', () => zoom(0.8));
|
||||
document.getElementById('resetView').addEventListener('click', resetView);
|
||||
document.getElementById('toggleLabels').addEventListener('click', toggleLabels);
|
||||
|
||||
// Image modal setup
|
||||
setupImageModal();
|
||||
|
||||
draw();
|
||||
});
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = canvas.clientWidth;
|
||||
canvas.height = canvas.clientHeight;
|
||||
draw();
|
||||
}
|
||||
|
||||
async function loadMapData() {
|
||||
try {
|
||||
const response = await fetch('/map_data.json');
|
||||
mapData = await response.json();
|
||||
centerView();
|
||||
updateStatistics();
|
||||
draw();
|
||||
} catch (error) {
|
||||
console.error('Failed to load map data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatistics() {
|
||||
document.getElementById('totalLocations').textContent = mapData.locations.length;
|
||||
document.getElementById('totalConnections').textContent = mapData.connections.length;
|
||||
document.getElementById('totalInteractables').textContent = mapData.interactables.length;
|
||||
|
||||
const uniqueEnemies = new Set();
|
||||
Object.values(mapData.spawn_tables).forEach(enemies => {
|
||||
enemies.forEach(enemy => uniqueEnemies.add(enemy.npc_id));
|
||||
});
|
||||
document.getElementById('totalEnemies').textContent = uniqueEnemies.size;
|
||||
}
|
||||
|
||||
function worldToScreen(x, y) {
|
||||
return {
|
||||
x: (x * gridSize * viewScale) + viewOffset.x + canvas.width / 2,
|
||||
y: (-y * gridSize * viewScale) + viewOffset.y + canvas.height / 2
|
||||
};
|
||||
}
|
||||
|
||||
function screenToWorld(sx, sy) {
|
||||
return {
|
||||
x: ((sx - canvas.width / 2 - viewOffset.x) / gridSize) / viewScale,
|
||||
y: -((sy - canvas.height / 2 - viewOffset.y) / gridSize) / viewScale
|
||||
};
|
||||
}
|
||||
|
||||
function centerView() {
|
||||
if (mapData.locations.length === 0) return;
|
||||
|
||||
// Calculate bounds
|
||||
let minX = Infinity, maxX = -Infinity;
|
||||
let minY = Infinity, maxY = -Infinity;
|
||||
|
||||
mapData.locations.forEach(loc => {
|
||||
minX = Math.min(minX, loc.x);
|
||||
maxX = Math.max(maxX, loc.x);
|
||||
minY = Math.min(minY, loc.y);
|
||||
maxY = Math.max(maxY, loc.y);
|
||||
});
|
||||
|
||||
const centerX = (minX + maxX) / 2;
|
||||
const centerY = (minY + maxY) / 2;
|
||||
|
||||
viewOffset.x = -centerX * gridSize * viewScale;
|
||||
viewOffset.y = centerY * gridSize * viewScale;
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
viewScale = 1.0;
|
||||
centerView();
|
||||
draw();
|
||||
}
|
||||
|
||||
function toggleLabels() {
|
||||
showLabels = !showLabels;
|
||||
draw();
|
||||
}
|
||||
|
||||
function zoom(factor) {
|
||||
const oldScale = viewScale;
|
||||
viewScale *= factor;
|
||||
viewScale = Math.max(0.3, Math.min(3.0, viewScale));
|
||||
|
||||
const scaleDiff = viewScale - oldScale;
|
||||
viewOffset.x -= (canvas.width / 2) * scaleDiff / oldScale;
|
||||
viewOffset.y -= (canvas.height / 2) * scaleDiff / oldScale;
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
function onMouseDown(e) {
|
||||
isDragging = true;
|
||||
lastMouse = { x: e.clientX, y: e.clientY };
|
||||
canvas.style.cursor = 'grabbing';
|
||||
}
|
||||
|
||||
function onMouseMove(e) {
|
||||
if (isDragging) {
|
||||
const dx = e.clientX - lastMouse.x;
|
||||
const dy = e.clientY - lastMouse.y;
|
||||
viewOffset.x += dx;
|
||||
viewOffset.y += dy;
|
||||
lastMouse = { x: e.clientX, y: e.clientY };
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
isDragging = false;
|
||||
canvas.style.cursor = 'grab';
|
||||
}
|
||||
|
||||
// Touch event handlers for mobile
|
||||
let lastTouch = { x: 0, y: 0 };
|
||||
let touchStartTime = 0;
|
||||
let lastTouchDistance = 0;
|
||||
|
||||
function onTouchStart(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.touches.length === 1) {
|
||||
// Single touch - pan
|
||||
isDragging = true;
|
||||
const touch = e.touches[0];
|
||||
lastTouch = { x: touch.clientX, y: touch.clientY };
|
||||
lastMouse = { x: touch.clientX, y: touch.clientY };
|
||||
touchStartTime = Date.now();
|
||||
} else if (e.touches.length === 2) {
|
||||
// Two touches - pinch zoom
|
||||
isDragging = false;
|
||||
const touch1 = e.touches[0];
|
||||
const touch2 = e.touches[1];
|
||||
lastTouchDistance = Math.sqrt(
|
||||
Math.pow(touch2.clientX - touch1.clientX, 2) +
|
||||
Math.pow(touch2.clientY - touch1.clientY, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onTouchMove(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.touches.length === 1 && isDragging) {
|
||||
// Single touch - pan
|
||||
const touch = e.touches[0];
|
||||
const dx = touch.clientX - lastTouch.x;
|
||||
const dy = touch.clientY - lastTouch.y;
|
||||
viewOffset.x += dx;
|
||||
viewOffset.y += dy;
|
||||
lastTouch = { x: touch.clientX, y: touch.clientY };
|
||||
draw();
|
||||
} else if (e.touches.length === 2) {
|
||||
// Two touches - pinch zoom
|
||||
const touch1 = e.touches[0];
|
||||
const touch2 = e.touches[1];
|
||||
const newDistance = Math.sqrt(
|
||||
Math.pow(touch2.clientX - touch1.clientX, 2) +
|
||||
Math.pow(touch2.clientY - touch1.clientY, 2)
|
||||
);
|
||||
|
||||
if (lastTouchDistance > 0) {
|
||||
const zoomFactor = newDistance / lastTouchDistance;
|
||||
|
||||
// Calculate center point between two touches
|
||||
const centerX = (touch1.clientX + touch2.clientX) / 2;
|
||||
const centerY = (touch1.clientY + touch2.clientY) / 2;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = centerX - rect.left;
|
||||
const mouseY = centerY - rect.top;
|
||||
|
||||
const worldBefore = screenToWorld(mouseX, mouseY);
|
||||
zoom(zoomFactor);
|
||||
const worldAfter = screenToWorld(mouseX, mouseY);
|
||||
|
||||
viewOffset.x += (worldAfter.x - worldBefore.x) * gridSize * viewScale;
|
||||
viewOffset.y -= (worldAfter.y - worldBefore.y) * gridSize * viewScale;
|
||||
}
|
||||
|
||||
lastTouchDistance = newDistance;
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
function onTouchEnd(e) {
|
||||
if (e.touches.length === 0) {
|
||||
isDragging = false;
|
||||
lastTouchDistance = 0;
|
||||
|
||||
// Check if it was a quick tap (< 200ms) for click action
|
||||
const touchDuration = Date.now() - touchStartTime;
|
||||
if (touchDuration < 200 && e.changedTouches.length === 1) {
|
||||
const touch = e.changedTouches[0];
|
||||
// Simulate click event
|
||||
const clickEvent = {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
};
|
||||
onClick(clickEvent);
|
||||
}
|
||||
} else if (e.touches.length === 1) {
|
||||
// One finger remaining, reset for pan
|
||||
const touch = e.touches[0];
|
||||
lastTouch = { x: touch.clientX, y: touch.clientY };
|
||||
isDragging = true;
|
||||
lastTouchDistance = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function onWheel(e) {
|
||||
e.preventDefault();
|
||||
const zoomSpeed = 0.001;
|
||||
const delta = -e.deltaY * zoomSpeed;
|
||||
const factor = 1 + delta;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
const worldBefore = screenToWorld(mouseX, mouseY);
|
||||
zoom(factor);
|
||||
const worldAfter = screenToWorld(mouseX, mouseY);
|
||||
|
||||
viewOffset.x += (worldAfter.x - worldBefore.x) * gridSize * viewScale;
|
||||
viewOffset.y -= (worldAfter.y - worldBefore.y) * gridSize * viewScale;
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
if (isDragging) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
const worldPos = screenToWorld(mouseX, mouseY);
|
||||
|
||||
// Find clicked location
|
||||
for (const location of mapData.locations) {
|
||||
const dist = Math.sqrt(
|
||||
Math.pow(location.x - worldPos.x, 2) +
|
||||
Math.pow(location.y - worldPos.y, 2)
|
||||
);
|
||||
|
||||
if (dist < 0.3) { // Click threshold
|
||||
selectedLocation = location;
|
||||
showLocationInfo(location);
|
||||
draw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDangerLevel(locationId) {
|
||||
const spawns = mapData.spawn_tables[locationId];
|
||||
if (!spawns || spawns.length === 0) return 'safe';
|
||||
|
||||
// Calculate average enemy strength
|
||||
let totalXP = 0;
|
||||
spawns.forEach(enemy => {
|
||||
totalXP += enemy.xp_reward * (enemy.spawn_weight / 100);
|
||||
});
|
||||
|
||||
if (totalXP < 20) return 'low';
|
||||
if (totalXP < 40) return 'medium';
|
||||
return 'high';
|
||||
}
|
||||
|
||||
function getNodeColor(locationId) {
|
||||
const danger = getDangerLevel(locationId);
|
||||
switch (danger) {
|
||||
case 'safe': return colors.nodeSafe;
|
||||
case 'low': return colors.nodeLowDanger;
|
||||
case 'medium': return colors.nodeMediumDanger;
|
||||
case 'high': return colors.nodeHighDanger;
|
||||
default: return colors.nodeSafe;
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Clear canvas
|
||||
ctx.fillStyle = colors.background;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw grid
|
||||
drawGrid();
|
||||
|
||||
// Draw connections
|
||||
ctx.lineWidth = 2 * viewScale;
|
||||
mapData.connections.forEach(conn => {
|
||||
const from = mapData.locations.find(l => l.id === conn.from);
|
||||
const to = mapData.locations.find(l => l.id === conn.to);
|
||||
|
||||
if (from && to) {
|
||||
const fromScreen = worldToScreen(from.x, from.y);
|
||||
const toScreen = worldToScreen(to.x, to.y);
|
||||
|
||||
ctx.strokeStyle = colors.connection;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(fromScreen.x, fromScreen.y);
|
||||
ctx.lineTo(toScreen.x, toScreen.y);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw distance label
|
||||
if (showLabels && viewScale > 0.6) {
|
||||
const midX = (fromScreen.x + toScreen.x) / 2;
|
||||
const midY = (fromScreen.y + toScreen.y) / 2;
|
||||
const stamina = Math.ceil(conn.distance * 3);
|
||||
|
||||
ctx.fillStyle = 'rgba(42, 42, 74, 0.9)';
|
||||
ctx.fillRect(midX - 20, midY - 10, 40, 20);
|
||||
|
||||
ctx.fillStyle = colors.label;
|
||||
ctx.font = `${10 * viewScale}px sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(`${stamina}⚡`, midX, midY);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Draw locations
|
||||
mapData.locations.forEach(location => {
|
||||
const screen = worldToScreen(location.x, location.y);
|
||||
const radius = nodeRadius * viewScale;
|
||||
|
||||
// Node circle
|
||||
const isSelected = selectedLocation && selectedLocation.id === location.id;
|
||||
ctx.fillStyle = isSelected ? colors.nodeSelected : getNodeColor(location.id);
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = isSelected ? 3 * viewScale : 2 * viewScale;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(screen.x, screen.y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// Interactable indicator
|
||||
if (location.interactable_count > 0) {
|
||||
ctx.fillStyle = '#ffd700';
|
||||
ctx.font = `${8 * viewScale}px sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(location.interactable_count, screen.x, screen.y);
|
||||
}
|
||||
|
||||
// Label
|
||||
if (showLabels) {
|
||||
ctx.fillStyle = colors.label;
|
||||
ctx.font = `${12 * viewScale}px sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(location.name, screen.x, screen.y + radius + 5);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawGrid() {
|
||||
ctx.strokeStyle = colors.grid;
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
const step = gridSize * viewScale;
|
||||
const startX = (viewOffset.x % step) - step;
|
||||
const startY = (viewOffset.y % step) - step;
|
||||
|
||||
for (let x = startX; x < canvas.width; x += step) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, canvas.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
for (let y = startY; y < canvas.height; y += step) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(canvas.width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function showLocationInfo(location) {
|
||||
// Location details
|
||||
const locationHTML = `
|
||||
${location.image_path ?
|
||||
`<img src="/${location.image_path}" class="location-image" onerror="this.style.display='none';" />` :
|
||||
`<div class="image-placeholder">🗺️</div>`
|
||||
}
|
||||
<h3>${location.name}</h3>
|
||||
<div class="description">${location.description}</div>
|
||||
<p><strong>Coordinates:</strong> (${location.x}, ${location.y})</p>
|
||||
<p><strong>Interactables:</strong> ${location.interactable_count}</p>
|
||||
<h3>🧭 Connections</h3>
|
||||
<div class="connections">
|
||||
${mapData.connections
|
||||
.filter(c => c.from === location.id)
|
||||
.map(c => {
|
||||
const dest = mapData.locations.find(l => l.id === c.to);
|
||||
const stamina = Math.ceil(c.distance * 3);
|
||||
return `<div class="connection-item">
|
||||
<strong>${c.direction.toUpperCase()}</strong><br>
|
||||
${dest ? dest.name : c.to}<br>
|
||||
${stamina}⚡ stamina
|
||||
</div>`;
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('locationInfo').innerHTML = locationHTML;
|
||||
|
||||
// Add click handler to location image
|
||||
setTimeout(() => {
|
||||
const locationImg = document.querySelector('.location-image');
|
||||
if (locationImg) {
|
||||
locationImg.addEventListener('click', () => openImageModal(locationImg.src, location.name));
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// Interactables
|
||||
const locationInteractables = mapData.interactables.filter(i => i.location_id === location.id);
|
||||
if (locationInteractables.length > 0) {
|
||||
const interactablesHTML = `
|
||||
<div class="interactable-list">
|
||||
${locationInteractables.map(inter => `
|
||||
<div class="interactable-card">
|
||||
<div class="interactable-header">
|
||||
<div class="interactable-icon">
|
||||
${inter.image_path ?
|
||||
`<img src="/${inter.image_path}" class="interactable-image" onerror="this.outerHTML='<div class=\\'interactable-image-placeholder\\'>📦</div>';" />` :
|
||||
`<div class="interactable-image-placeholder">📦</div>`
|
||||
}
|
||||
</div>
|
||||
<div class="interactable-name">${inter.name}</div>
|
||||
</div>
|
||||
${inter.actions.map(action => `
|
||||
<div class="action-item">
|
||||
<div class="action-header">${action.label} (${action.stamina_cost}⚡)</div>
|
||||
${action.outcomes.map(outcome => `
|
||||
<div class="outcome-item outcome-${outcome.type}">
|
||||
<strong>${outcome.type}:</strong> ${outcome.text}
|
||||
${Object.keys(outcome.items).length > 0 ?
|
||||
`<br>Items: ${Object.entries(outcome.items).map(([id, qty]) => `${id} x${qty}`).join(', ')}` : ''}
|
||||
${outcome.damage > 0 ? `<br>⚠️ Damage: ${outcome.damage} HP` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('interactablesInfo').innerHTML = interactablesHTML;
|
||||
|
||||
// Add click handlers to interactable images
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.interactable-image').forEach(img => {
|
||||
img.addEventListener('click', () => openImageModal(img.src, img.alt || 'Interactable'));
|
||||
});
|
||||
}, 0);
|
||||
} else {
|
||||
document.getElementById('interactablesInfo').innerHTML = '<p class="no-data">No interactables at this location</p>';
|
||||
}
|
||||
|
||||
// Enemies
|
||||
const enemies = mapData.spawn_tables[location.id];
|
||||
if (enemies && enemies.length > 0) {
|
||||
const enemiesHTML = `
|
||||
<p><strong>Encounter Rate:</strong> ${enemies[0].encounter_rate}% when traveling</p>
|
||||
<div class="enemy-list">
|
||||
${enemies.map(enemy => `
|
||||
<div class="enemy-card">
|
||||
<div class="enemy-header">
|
||||
<div class="enemy-icon">
|
||||
${enemy.image_url ?
|
||||
`<img src="/${enemy.image_url}" class="enemy-image" onerror="this.outerHTML='<div class=\\'enemy-image-placeholder\\'>${enemy.emoji}</div>';" />` :
|
||||
`<div class="enemy-image-placeholder">${enemy.emoji}</div>`
|
||||
}
|
||||
</div>
|
||||
<div class="enemy-name">${enemy.name}</div>
|
||||
<div>${enemy.spawn_chance}%</div>
|
||||
</div>
|
||||
<div class="enemy-stats">
|
||||
<div class="stat-item">❤️ HP: ${enemy.hp_range[0]}-${enemy.hp_range[1]}</div>
|
||||
<div class="stat-item">⚔️ DMG: ${enemy.damage_range[0]}-${enemy.damage_range[1]}</div>
|
||||
<div class="stat-item">⭐ XP: ${enemy.xp_reward}</div>
|
||||
<div class="stat-item">🎲 Weight: ${enemy.spawn_weight}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('enemiesInfo').innerHTML = enemiesHTML;
|
||||
|
||||
// Add click handlers to enemy images
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.enemy-image').forEach(img => {
|
||||
img.addEventListener('click', () => openImageModal(img.src, img.alt || 'Enemy'));
|
||||
});
|
||||
}, 0);
|
||||
} else {
|
||||
document.getElementById('enemiesInfo').innerHTML = '<p class="no-data">✅ Safe zone - no enemies spawn here</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Image Modal Functions
|
||||
function setupImageModal() {
|
||||
const modal = document.getElementById('imageModal');
|
||||
const closeBtn = document.querySelector('.image-modal-close');
|
||||
|
||||
// Close modal when clicking close button, backdrop, or pressing Escape
|
||||
closeBtn.addEventListener('click', closeImageModal);
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeImageModal();
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
||||
closeImageModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openImageModal(imageSrc, imageTitle) {
|
||||
const modal = document.getElementById('imageModal');
|
||||
const modalImg = document.getElementById('modalImage');
|
||||
const modalInfo = document.getElementById('modalInfo');
|
||||
|
||||
modalImg.src = imageSrc;
|
||||
modalImg.alt = imageTitle;
|
||||
modalInfo.textContent = imageTitle + ' - Click anywhere to close';
|
||||
modal.classList.add('active');
|
||||
|
||||
// Prevent body scrolling when modal is open
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeImageModal() {
|
||||
const modal = document.getElementById('imageModal');
|
||||
modal.classList.remove('active');
|
||||
|
||||
// Re-enable body scrolling
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
Reference in New Issue
Block a user