455 lines
15 KiB
JavaScript
455 lines
15 KiB
JavaScript
// Конфигурация плеера
|
||
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 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() {
|
||
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 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 loadMap(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;
|
||
|
||
// Очищаем траекторию при загрузке новой карты
|
||
clearVisualization();
|
||
|
||
resizeCanvas();
|
||
drawMap();
|
||
|
||
document.getElementById('loadMapBtn').textContent = '✓ Карта загружена';
|
||
setTimeout(() => {
|
||
document.getElementById('loadMapBtn').textContent = '📂 Загрузить карту';
|
||
}, 2000);
|
||
|
||
alert('Карта успешно загружена!');
|
||
} catch (error) {
|
||
alert('Ошибка при загрузке карты: ' + error.message);
|
||
console.error('Ошибка загрузки:', error);
|
||
}
|
||
};
|
||
|
||
reader.readAsText(file);
|
||
event.target.value = '';
|
||
}
|
||
|
||
// Загрузка решения из 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('playbackControls').classList.remove('hidden');
|
||
document.getElementById('stepInfo').classList.remove('hidden');
|
||
|
||
// Обновляем информацию
|
||
updateStepInfo();
|
||
drawMap();
|
||
|
||
document.getElementById('loadSolutionBtn').textContent = '✓ Решение загружено';
|
||
setTimeout(() => {
|
||
document.getElementById('loadSolutionBtn').textContent = '🎬 Загрузить решение';
|
||
}, 2000);
|
||
|
||
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 stepBackward() {
|
||
if (!trajectory || trajectory.length === 0) return;
|
||
|
||
if (currentStep > 0) {
|
||
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('playbackControls').classList.add('hidden');
|
||
document.getElementById('stepInfo').classList.add('hidden');
|
||
|
||
drawMap();
|
||
}
|
||
|
||
// Инициализация при загрузке страницы
|
||
init();
|
||
|