split editor
This commit is contained in:
94
solution-player/README.md
Normal file
94
solution-player/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 🎬 Визуализатор решений - Гонки на бумаге
|
||||
|
||||
Веб-приложение для визуализации и анимации решений. Работает без сервера, просто откройте в браузере.
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
```bash
|
||||
# Запуск визуализатора
|
||||
./open-player.sh
|
||||
# или
|
||||
firefox index.html
|
||||
```
|
||||
|
||||
**Три простых шага:**
|
||||
1. Загрузите карту (📂 Загрузить карту) - файл с полем
|
||||
2. Загрузите решение (🎬 Загрузить решение) - файл с траекторией
|
||||
3. Нажмите ▶ Play и наблюдайте за движением
|
||||
|
||||
## 🎮 Управление
|
||||
|
||||
| Кнопка | Действие |
|
||||
|--------|----------|
|
||||
| ▶ Play | Автоматическое воспроизведение |
|
||||
| ⏸ Pause | Пауза |
|
||||
| ⏮ Reset | Сброс к началу |
|
||||
| ⏪ Back | Шаг назад |
|
||||
| ⏩ Forward | Шаг вперед |
|
||||
| Слайдер | Скорость 1x - 10x |
|
||||
|
||||
## 📊 Визуализация
|
||||
|
||||
- 🔵 **Синяя линия и точки** - пройденная траектория
|
||||
- 🔴 **Красный круг** - текущая позиция
|
||||
- ➡️ **Красная стрелка** - вектор скорости (направление и величина)
|
||||
- **Панель информации** - шаг, позиция (x, y), скорость (vx, vy), ускорение (ax, ay)
|
||||
|
||||
## 📄 Форматы файлов
|
||||
|
||||
### Формат карты
|
||||
```json
|
||||
{
|
||||
"map": [
|
||||
[5, 0, 0, 1, 0],
|
||||
[0, 1, 0, 1, 0],
|
||||
[0, 0, 2, 2, 4]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Типы ячеек:** 0-Дорога, 1-Камень, 2-Снег, 3-Лёд, 4-Чекпоинт, 5-Старт (обязательно!)
|
||||
|
||||
### Формат решения
|
||||
```json
|
||||
{
|
||||
"solution": [
|
||||
[1, 1],
|
||||
[1, 0],
|
||||
[0, 1]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Структура:** массив векторов ускорения `[[ax, ay], ...]` где `ax`, `ay` - целые числа (обычно от -1 до 1)
|
||||
|
||||
### Физика движения
|
||||
|
||||
На каждом шаге:
|
||||
```
|
||||
velocity += acceleration
|
||||
position += velocity
|
||||
```
|
||||
|
||||
**Пример:**
|
||||
```
|
||||
Шаг 0: pos=(0,0), vel=(0,0)
|
||||
Шаг 1: acc=(1,1) → vel=(1,1) → pos=(1,1)
|
||||
Шаг 2: acc=(1,0) → vel=(2,1) → pos=(3,2)
|
||||
```
|
||||
|
||||
## 🎯 Быстрый тест
|
||||
|
||||
1. Откройте `./open-player.sh`
|
||||
2. Загрузите карту `demo-with-start.json`
|
||||
3. Загрузите решение `demo-with-start-solution.json`
|
||||
4. Нажмите ▶ Play и экспериментируйте со скоростью
|
||||
|
||||
## 🔗 Связанные проекты
|
||||
|
||||
- [Редактор карт](../map-editor/) - для создания карт
|
||||
- [Основной проект](../README.md) - C# решатель на основе A*
|
||||
|
||||
---
|
||||
|
||||
**Технологии:** HTML5 Canvas, Vanilla JavaScript (ES6+), CSS3
|
||||
32
solution-player/demo-with-start-solution.json
Normal file
32
solution-player/demo-with-start-solution.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"solution": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
-1
|
||||
],
|
||||
[
|
||||
-2,
|
||||
-1
|
||||
],
|
||||
[
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
-1
|
||||
]
|
||||
]
|
||||
}
|
||||
20
solution-player/demo-with-start.json
Normal file
20
solution-player/demo-with-start.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"map": [
|
||||
[5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0, 0, 0, 2, 4, 2, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 1, 1, 0],
|
||||
[0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 2, 2, 2, 0],
|
||||
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 2, 0],
|
||||
[0, 0, 0, 0, 0, 3, 3, 3, 0, 0, 0, 2, 2, 2, 0],
|
||||
[0, 0, 0, 0, 0, 3, 1, 3, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 3, 3, 3, 0, 0, 0, 0, 0, 0, 4],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
]
|
||||
}
|
||||
|
||||
358
solution-player/index.html
Normal file
358
solution-player/index.html
Normal file
@@ -0,0 +1,358 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Визуализатор решений - Гонки на бумаге</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.control-group h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #495057;
|
||||
font-size: 1.1em;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 3px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #2196f3;
|
||||
border-radius: 8px;
|
||||
color: #0d47a1;
|
||||
}
|
||||
|
||||
.info strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#mapInput, #solutionInput {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.road { background: #f8f9fa; }
|
||||
.stone { background: #6c757d; }
|
||||
.snow { background: #e3f2fd; }
|
||||
.ice { background: #b3e5fc; }
|
||||
.checkpoint { background: #fff3cd; }
|
||||
.start { background: #d4edda; }
|
||||
|
||||
.playback-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.playback-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.playback-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.playback-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.speed-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.speed-control input[type="range"] {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.85em;
|
||||
color: #6c757d;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.visualization-panel {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.visualization-panel h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #495057;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎬 Визуализатор решений</h1>
|
||||
<p class="subtitle">Гонки на бумаге / Paper Racing</p>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<h3>📂 Загрузка файлов</h3>
|
||||
<div class="buttons">
|
||||
<button class="btn-primary" id="loadMapBtn" onclick="document.getElementById('mapInput').click()">📂 Загрузить карту</button>
|
||||
<button class="btn-success" id="loadSolutionBtn" onclick="document.getElementById('solutionInput').click()">🎬 Загрузить решение</button>
|
||||
</div>
|
||||
<input type="file" id="mapInput" accept=".json" onchange="loadMap(event)">
|
||||
<input type="file" id="solutionInput" accept=".json" onchange="loadSolution(event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="canvas-wrapper">
|
||||
<canvas id="mapCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div id="playbackControls" class="visualization-panel hidden">
|
||||
<h3>🎮 Управление воспроизведением</h3>
|
||||
|
||||
<div class="playback-controls">
|
||||
<button class="playback-btn" onclick="playVisualization()" id="playBtn">▶ Play</button>
|
||||
<button class="playback-btn" onclick="pauseVisualization()" id="pauseBtn" disabled>⏸ Pause</button>
|
||||
<button class="playback-btn" onclick="resetVisualization()">⏮ Reset</button>
|
||||
<button class="playback-btn" onclick="stepBackward()">⏪ Back</button>
|
||||
<button class="playback-btn" onclick="stepForward()">⏩ Forward</button>
|
||||
|
||||
<div class="speed-control">
|
||||
<label for="speedSlider">Скорость:</label>
|
||||
<input type="range" id="speedSlider" min="1" max="10" value="5" onchange="updateSpeed()">
|
||||
<span id="speedValue">5x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="stepInfo" class="step-info hidden">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Шаг</span>
|
||||
<span class="info-value" id="stepNumber">0 / 0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Позиция (x, y)</span>
|
||||
<span class="info-value" id="positionValue">(0, 0)</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Скорость (vx, vy)</span>
|
||||
<span class="info-value" id="velocityValue">(0, 0)</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Ускорение (ax, ay)</span>
|
||||
<span class="info-value" id="accelerationValue">(0, 0)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<strong>💡 Инструкция:</strong>
|
||||
• Сначала загрузите карту с точкой старта (тип 5)<br>
|
||||
• Затем загрузите файл решения с векторами ускорений<br>
|
||||
• Используйте кнопки управления для просмотра анимации<br>
|
||||
• 🔵 Синяя линия - пройденная траектория<br>
|
||||
• 🔴 Красный круг - текущая позиция<br>
|
||||
• ➡️ Красная стрелка - направление и скорость движения
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>📖 Легенда цветов</h3>
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color road"></div>
|
||||
<span>Дорога (0)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color stone"></div>
|
||||
<span>Камень (1)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color snow"></div>
|
||||
<span>Снег (2)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color ice"></div>
|
||||
<span>Лёд (3)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color checkpoint"></div>
|
||||
<span>Чекпоинт (4)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color start"></div>
|
||||
<span>Старт (5)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="player.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
40
solution-player/open-player.sh
Executable file
40
solution-player/open-player.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт для открытия визуализатора решений в браузере
|
||||
# Script to open solution player in browser
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HTML_FILE="${SCRIPT_DIR}/index.html"
|
||||
|
||||
echo "🎬 Открытие визуализатора решений..."
|
||||
echo "📂 Путь: ${HTML_FILE}"
|
||||
|
||||
# Пробуем открыть в различных браузерах
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open "${HTML_FILE}"
|
||||
echo "✓ Открыто в браузере по умолчанию"
|
||||
elif command -v firefox &> /dev/null; then
|
||||
firefox "${HTML_FILE}" &
|
||||
echo "✓ Открыто в Firefox"
|
||||
elif command -v chromium &> /dev/null; then
|
||||
chromium "${HTML_FILE}" &
|
||||
echo "✓ Открыто в Chromium"
|
||||
elif command -v google-chrome &> /dev/null; then
|
||||
google-chrome "${HTML_FILE}" &
|
||||
echo "✓ Открыто в Google Chrome"
|
||||
else
|
||||
echo "❌ Не найден браузер"
|
||||
echo "Откройте файл вручную: ${HTML_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "💡 Инструкция:"
|
||||
echo "1. Загрузите карту с точкой старта (файл map)"
|
||||
echo "2. Загрузите решение (файл solution)"
|
||||
echo "3. Используйте кнопки управления для просмотра"
|
||||
echo ""
|
||||
echo "📁 Примеры файлов:"
|
||||
echo " - demo-with-start.json (карта)"
|
||||
echo " - example-solution.json (решение)"
|
||||
|
||||
454
solution-player/player.js
Normal file
454
solution-player/player.js
Normal file
@@ -0,0 +1,454 @@
|
||||
// Конфигурация плеера
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user