// Конфигурация редактора const CELL_SIZE = 30; const GRID_COLOR = '#dee2e6'; const COLORS = { 0: '#f8f9fa', // Дорога 1: '#6c757d', // Камень 2: '#e3f2fd', // Снег 3: '#b3e5fc', // Лёд 4: '#fff3cd', // Чекпоинт 5: '#d4edda' // Старт }; // Состояние редактора let width = 15; let height = 15; let map = []; let selectedType = 0; let isDrawing = false; // Состояние визуализации let solution = null; let trajectory = []; let currentStep = 0; let isPlaying = false; let playbackSpeed = 5; let playbackInterval = null; let startPosition = null; // Canvas элементы const canvas = document.getElementById('mapCanvas'); const ctx = canvas.getContext('2d'); // Инициализация function init() { width = parseInt(document.getElementById('width').value); height = parseInt(document.getElementById('height').value); initMap(); resizeCanvas(); drawMap(); } // Инициализация карты function initMap() { map = Array(height).fill(null).map(() => Array(width).fill(0)); } // Изменение размера canvas function resizeCanvas() { canvas.width = width * CELL_SIZE; canvas.height = height * CELL_SIZE; } // Отрисовка карты function drawMap() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Рисуем ячейки for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const cellType = map[y][x]; ctx.fillStyle = COLORS[cellType]; ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); } } // Рисуем сетку ctx.strokeStyle = GRID_COLOR; ctx.lineWidth = 1; // Вертикальные линии for (let x = 0; x <= width; x++) { ctx.beginPath(); ctx.moveTo(x * CELL_SIZE, 0); ctx.lineTo(x * CELL_SIZE, height * CELL_SIZE); ctx.stroke(); } // Горизонтальные линии for (let y = 0; y <= height; y++) { ctx.beginPath(); ctx.moveTo(0, y * CELL_SIZE); ctx.lineTo(width * CELL_SIZE, y * CELL_SIZE); ctx.stroke(); } // Рисуем маркеры для чекпоинтов и старта ctx.font = 'bold 16px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { if (map[y][x] === 4) { ctx.fillStyle = '#856404'; ctx.fillText('C', x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2); } else if (map[y][x] === 5) { ctx.fillStyle = '#155724'; ctx.fillText('S', x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2); } } } // Рисуем визуализацию траектории, если она есть if (trajectory.length > 0) { drawTrajectory(); } } // Рисование траектории решения function drawTrajectory() { if (!trajectory || trajectory.length === 0) return; // Рисуем все предыдущие позиции как след ctx.strokeStyle = '#667eea'; ctx.lineWidth = 3; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); for (let i = 0; i <= currentStep && i < trajectory.length; i++) { const pos = trajectory[i]; const screenX = pos.x * CELL_SIZE + CELL_SIZE / 2; const screenY = pos.y * CELL_SIZE + CELL_SIZE / 2; if (i === 0) { ctx.moveTo(screenX, screenY); } else { ctx.lineTo(screenX, screenY); } } ctx.stroke(); // Рисуем точки на каждом шаге for (let i = 0; i <= currentStep && i < trajectory.length; i++) { const pos = trajectory[i]; const screenX = pos.x * CELL_SIZE + CELL_SIZE / 2; const screenY = pos.y * CELL_SIZE + CELL_SIZE / 2; ctx.beginPath(); ctx.arc(screenX, screenY, 4, 0, Math.PI * 2); ctx.fillStyle = '#667eea'; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke(); } // Рисуем текущую позицию большим кругом if (currentStep < trajectory.length) { const current = trajectory[currentStep]; const screenX = current.x * CELL_SIZE + CELL_SIZE / 2; const screenY = current.y * CELL_SIZE + CELL_SIZE / 2; // Пульсирующий эффект ctx.beginPath(); ctx.arc(screenX, screenY, 10, 0, Math.PI * 2); ctx.fillStyle = '#f5576c'; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 3; ctx.stroke(); // Стрелка направления скорости if (current.vx !== 0 || current.vy !== 0) { const arrowLen = 20; const angle = Math.atan2(current.vy, current.vx); const endX = screenX + Math.cos(angle) * arrowLen; const endY = screenY + Math.sin(angle) * arrowLen; ctx.beginPath(); ctx.moveTo(screenX, screenY); ctx.lineTo(endX, endY); ctx.strokeStyle = '#f5576c'; ctx.lineWidth = 3; ctx.stroke(); // Наконечник стрелки const headLen = 8; const headAngle = Math.PI / 6; ctx.beginPath(); ctx.moveTo(endX, endY); ctx.lineTo( endX - headLen * Math.cos(angle - headAngle), endY - headLen * Math.sin(angle - headAngle) ); ctx.moveTo(endX, endY); ctx.lineTo( endX - headLen * Math.cos(angle + headAngle), endY - headLen * Math.sin(angle + headAngle) ); ctx.stroke(); } } } // Выбор типа ячейки function selectCellType(type) { selectedType = type; // Обновляем UI document.querySelectorAll('.cell-type').forEach(el => { el.classList.remove('active'); }); document.querySelector(`[data-type="${type}"]`).classList.add('active'); } // Получение координат ячейки по клику function getCellCoords(event) { const rect = canvas.getBoundingClientRect(); const x = Math.floor((event.clientX - rect.left) / CELL_SIZE); const y = Math.floor((event.clientY - rect.top) / CELL_SIZE); if (x >= 0 && x < width && y >= 0 && y < height) { return { x, y }; } return null; } // Установка типа ячейки function setCellType(x, y) { if (x >= 0 && x < width && y >= 0 && y < height) { map[y][x] = selectedType; drawMap(); } } // Обработчики событий мыши canvas.addEventListener('mousedown', (e) => { isDrawing = true; const coords = getCellCoords(e); if (coords) { setCellType(coords.x, coords.y); } }); canvas.addEventListener('mousemove', (e) => { if (isDrawing) { const coords = getCellCoords(e); if (coords) { setCellType(coords.x, coords.y); } } }); canvas.addEventListener('mouseup', () => { isDrawing = false; }); canvas.addEventListener('mouseleave', () => { isDrawing = false; }); // Изменение размера карты function resizeMap() { const newWidth = parseInt(document.getElementById('width').value); const newHeight = parseInt(document.getElementById('height').value); if (newWidth < 5 || newWidth > 100 || newHeight < 5 || newHeight > 100) { alert('Размеры должны быть от 5 до 100'); return; } const newMap = Array(newHeight).fill(null).map(() => Array(newWidth).fill(0)); // Копируем существующие данные const minHeight = Math.min(height, newHeight); const minWidth = Math.min(width, newWidth); for (let y = 0; y < minHeight; y++) { for (let x = 0; x < minWidth; x++) { newMap[y][x] = map[y][x]; } } width = newWidth; height = newHeight; map = newMap; resizeCanvas(); drawMap(); } // Очистка карты function clearMap() { if (confirm('Вы уверены, что хотите очистить всю карту?')) { initMap(); drawMap(); } } // Экспорт карты в JSON function exportMap() { const data = { map: map }; const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `racing-map-${width}x${height}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Также выводим в консоль для быстрого копирования console.log('Экспортированная карта:', json); alert('Карта экспортирована! JSON также выведен в консоль браузера (F12)'); } // Импорт карты из JSON function importMap(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); if (!data.map || !Array.isArray(data.map)) { throw new Error('Неверный формат: отсутствует массив map'); } const newMap = data.map; // Валидация if (!newMap.every(row => Array.isArray(row))) { throw new Error('Неверный формат: map должен быть двумерным массивом'); } const newHeight = newMap.length; const newWidth = newMap[0].length; if (newHeight < 5 || newHeight > 100 || newWidth < 5 || newWidth > 100) { throw new Error('Размеры карты должны быть от 5 до 100'); } if (!newMap.every(row => row.length === newWidth)) { throw new Error('Все строки должны иметь одинаковую длину'); } // Проверка значений ячеек const validValues = [0, 1, 2, 3, 4, 5]; for (let y = 0; y < newHeight; y++) { for (let x = 0; x < newWidth; x++) { if (!validValues.includes(newMap[y][x])) { throw new Error(`Недопустимое значение ячейки: ${newMap[y][x]} на позиции [${y}][${x}]`); } } } // Импортируем карту height = newHeight; width = newWidth; map = newMap; // Обновляем UI document.getElementById('width').value = width; document.getElementById('height').value = height; resizeCanvas(); drawMap(); alert('Карта успешно импортирована!'); } catch (error) { alert('Ошибка при импорте: ' + error.message); console.error('Ошибка импорта:', error); } }; reader.readAsText(file); // Сброс input для возможности повторного импорта того же файла event.target.value = ''; } // Обработчики изменения размеров document.getElementById('width').addEventListener('keypress', (e) => { if (e.key === 'Enter') resizeMap(); }); document.getElementById('height').addEventListener('keypress', (e) => { if (e.key === 'Enter') resizeMap(); }); // ============================================ // Функции визуализации решения // ============================================ // Поиск стартовой позиции на карте function findStartPosition() { for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { if (map[y][x] === 5) { return { x, y }; } } } return null; } // Симуляция траектории на основе векторов ускорений function simulateTrajectory(accelerations, start) { const traj = []; let x = start.x; let y = start.y; let vx = 0; let vy = 0; // Начальная позиция traj.push({ x, y, vx, vy, ax: 0, ay: 0 }); // Применяем каждое ускорение for (let i = 0; i < accelerations.length; i++) { const [ax, ay] = accelerations[i]; // Обновляем скорость vx += ax; vy += ay; // Обновляем позицию x += vx; y += vy; traj.push({ x, y, vx, vy, ax, ay }); } return traj; } // Загрузка решения из JSON function loadSolution(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); if (!data.solution || !Array.isArray(data.solution)) { throw new Error('Неверный формат: отсутствует массив solution'); } // Проверяем, что это массив массивов из двух чисел if (!data.solution.every(acc => Array.isArray(acc) && acc.length === 2)) { throw new Error('Неверный формат: solution должен быть массивом [[ax, ay], ...]'); } // Находим стартовую позицию startPosition = findStartPosition(); if (!startPosition) { throw new Error('На карте не найдена точка старта (тип 5)'); } solution = data.solution; trajectory = simulateTrajectory(solution, startPosition); currentStep = 0; // Показываем панель визуализации document.getElementById('visualizationPanel').classList.remove('hidden'); document.getElementById('clearVisBtn').disabled = false; // Обновляем информацию updateStepInfo(); drawMap(); alert(`Решение загружено! ${solution.length} шагов.`); } catch (error) { alert('Ошибка при загрузке решения: ' + error.message); console.error('Ошибка загрузки:', error); } }; reader.readAsText(file); event.target.value = ''; } // Обновление информации о текущем шаге function updateStepInfo() { if (!trajectory || trajectory.length === 0) return; const current = trajectory[currentStep]; document.getElementById('stepNumber').textContent = `${currentStep} / ${trajectory.length - 1}`; document.getElementById('positionValue').textContent = `(${current.x}, ${current.y})`; document.getElementById('velocityValue').textContent = `(${current.vx}, ${current.vy})`; document.getElementById('accelerationValue').textContent = `(${current.ax}, ${current.ay})`; } // Воспроизведение визуализации function playVisualization() { if (!trajectory || trajectory.length === 0) return; isPlaying = true; document.getElementById('playBtn').disabled = true; document.getElementById('pauseBtn').disabled = false; playbackInterval = setInterval(() => { if (currentStep < trajectory.length - 1) { currentStep++; updateStepInfo(); drawMap(); } else { pauseVisualization(); } }, 1000 / playbackSpeed); } // Пауза воспроизведения function pauseVisualization() { isPlaying = false; document.getElementById('playBtn').disabled = false; document.getElementById('pauseBtn').disabled = true; if (playbackInterval) { clearInterval(playbackInterval); playbackInterval = null; } } // Сброс визуализации function resetVisualization() { pauseVisualization(); currentStep = 0; updateStepInfo(); drawMap(); } // Шаг вперед function stepForward() { if (!trajectory || trajectory.length === 0) return; if (currentStep < trajectory.length - 1) { currentStep++; updateStepInfo(); drawMap(); } } // Обновление скорости воспроизведения function updateSpeed() { playbackSpeed = parseInt(document.getElementById('speedSlider').value); document.getElementById('speedValue').textContent = `${playbackSpeed}x`; // Если воспроизведение идет, перезапускаем с новой скоростью if (isPlaying) { pauseVisualization(); playVisualization(); } } // Очистка визуализации function clearVisualization() { pauseVisualization(); solution = null; trajectory = []; currentStep = 0; startPosition = null; document.getElementById('visualizationPanel').classList.add('hidden'); document.getElementById('clearVisBtn').disabled = true; drawMap(); } // Инициализация при загрузке страницы init();