split editor
This commit is contained in:
133
QUICKSTART-WEB.md
Normal file
133
QUICKSTART-WEB.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# 🚀 Быстрый старт - Веб-инструменты
|
||||
|
||||
## Два независимых приложения
|
||||
|
||||
### 🗺️ Редактор карт
|
||||
```bash
|
||||
cd map-editor
|
||||
./open-editor.sh
|
||||
```
|
||||
**Для чего:** Создание и редактирование карт
|
||||
|
||||
### 🎬 Визуализатор решений
|
||||
```bash
|
||||
cd solution-player
|
||||
./open-player.sh
|
||||
```
|
||||
**Для чего:** Просмотр анимации решений
|
||||
|
||||
---
|
||||
|
||||
## 📖 Полный цикл работы
|
||||
|
||||
### Шаг 1: Создайте карту
|
||||
```bash
|
||||
cd map-editor
|
||||
./open-editor.sh
|
||||
```
|
||||
1. Нарисуйте трассу
|
||||
2. Добавьте точку старта (S, тип 5)
|
||||
3. Добавьте чекпоинты (C, тип 4)
|
||||
4. Нажмите "📥 Экспорт JSON"
|
||||
5. Сохраните как `my-map.json`
|
||||
|
||||
### Шаг 2: Решите карту
|
||||
```bash
|
||||
cd ..
|
||||
dotnet run --project racing-astar.csproj maps/my-map.json
|
||||
```
|
||||
Решение сохранится в `solution.json`
|
||||
|
||||
### Шаг 3: Визуализируйте
|
||||
```bash
|
||||
cd solution-player
|
||||
./open-player.sh
|
||||
```
|
||||
1. Нажмите "📂 Загрузить карту" → выберите `my-map.json`
|
||||
2. Нажмите "🎬 Загрузить решение" → выберите `solution.json`
|
||||
3. Нажмите "▶ Play"
|
||||
4. Наслаждайтесь анимацией!
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Быстрый тест с примерами
|
||||
|
||||
### Вариант 1: Простая карта
|
||||
```bash
|
||||
cd solution-player
|
||||
./open-player.sh
|
||||
```
|
||||
1. Загрузите карту: `simple-track.json`
|
||||
2. Загрузите решение: `example-solution.json`
|
||||
3. Play!
|
||||
|
||||
### Вариант 2: Демо-карта
|
||||
```bash
|
||||
cd solution-player
|
||||
./open-player.sh
|
||||
```
|
||||
1. Загрузите карту: `demo-with-start.json`
|
||||
2. Загрузите решение: `demo-solution.json`
|
||||
3. Play!
|
||||
|
||||
### Вариант 3: Сложная траектория
|
||||
```bash
|
||||
cd solution-player
|
||||
./open-player.sh
|
||||
```
|
||||
1. Загрузите карту: `demo-with-start.json`
|
||||
2. Загрузите решение: `complex-solution.json`
|
||||
3. Play!
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Управление визуализатором
|
||||
|
||||
- **▶ Play** - автоматическое воспроизведение
|
||||
- **⏸ Pause** - пауза
|
||||
- **⏮ Reset** - вернуться к началу
|
||||
- **⏪ Back** - шаг назад
|
||||
- **⏩ Forward** - шаг вперед
|
||||
- **Слайдер** - скорость 1x-10x
|
||||
|
||||
---
|
||||
|
||||
## 📚 Дополнительная информация
|
||||
|
||||
- **WEB-TOOLS.md** - подробное описание инструментов
|
||||
- **map-editor/README.md** - документация редактора
|
||||
- **solution-player/README.md** - документация визуализатора
|
||||
- **SEPARATION-SUMMARY.md** - детали разделения проектов
|
||||
|
||||
---
|
||||
|
||||
## 💡 Полезные советы
|
||||
|
||||
### Для редактора:
|
||||
- Используйте ЛКМ для рисования
|
||||
- Удерживайте кнопку мыши для быстрого рисования
|
||||
- Проверяйте JSON в консоли (F12)
|
||||
|
||||
### Для визуализатора:
|
||||
- Начните с малых скоростей (1x-3x)
|
||||
- Используйте пошаговый режим для анализа
|
||||
- Следите за красной стрелкой (вектор скорости)
|
||||
- Смотрите информацию о каждом шаге внизу
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
**Проблема:** Визуализатор не показывает траекторию
|
||||
**Решение:** Убедитесь, что на карте есть точка старта (тип 5)
|
||||
|
||||
**Проблема:** Траектория выходит за пределы карты
|
||||
**Решение:** Проверьте, что решение создано для правильной карты
|
||||
|
||||
**Проблема:** Браузер не открывается
|
||||
**Решение:** Откройте `index.html` вручную
|
||||
|
||||
---
|
||||
|
||||
Приятного использования! 🎉
|
||||
|
||||
81
RACING-TOOLS-INFO.md
Normal file
81
RACING-TOOLS-INFO.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 🏁 Racing Tools - Новый объединенный проект
|
||||
|
||||
## 🎉 Что нового?
|
||||
|
||||
Проекты `map-editor/` и `solution-player/` объединены в один: **`racing-tools/`**
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
```bash
|
||||
cd racing-tools
|
||||
|
||||
# Редактор карт
|
||||
./open-editor.sh
|
||||
# или
|
||||
firefox editor.html
|
||||
|
||||
# Визуализатор решений
|
||||
./open-player.sh
|
||||
# или
|
||||
firefox player.html
|
||||
```
|
||||
|
||||
## 📦 Что внутри?
|
||||
|
||||
### Два веб-приложения
|
||||
- **🏁 Редактор карт** (`editor.html`) - создание и редактирование карт
|
||||
- **🎬 Визуализатор решений** (`player.html`) - анимация траекторий
|
||||
|
||||
### Общие файлы
|
||||
- `styles.css` - единый стиль для обоих приложений
|
||||
- `common.js` - общие функции и константы
|
||||
- `README.md` - полная документация
|
||||
|
||||
## 🔑 Ключевые улучшения
|
||||
|
||||
✅ **Единый стиль** - общий CSS файл
|
||||
✅ **Навигация** - кнопки переключения между страницами
|
||||
✅ **Меньше дублирования** - общие функции в отдельном файле
|
||||
✅ **Удобство** - все в одной папке
|
||||
✅ **Единая документация** - один README
|
||||
|
||||
## 📄 Документация
|
||||
|
||||
- **[racing-tools/README.md](racing-tools/README.md)** - полная документация
|
||||
- **[racing-tools/MIGRATION.md](racing-tools/MIGRATION.md)** - руководство по миграции
|
||||
|
||||
## 🗂️ Структура проекта
|
||||
|
||||
```
|
||||
racing-tools/
|
||||
├── editor.html # Редактор карт
|
||||
├── player.html # Визуализатор решений
|
||||
├── styles.css # Общие стили
|
||||
├── common.js # Общий код
|
||||
├── editor.js # Логика редактора
|
||||
├── player.js # Логика визуализатора
|
||||
├── open-editor.sh # Запуск редактора
|
||||
├── open-player.sh # Запуск визуализатора
|
||||
├── demo-with-start.json # Демо-карта
|
||||
├── demo-with-start-solution.json # Демо-решение
|
||||
├── README.md # Документация
|
||||
└── MIGRATION.md # Руководство по миграции
|
||||
```
|
||||
|
||||
## ⚡ Совместимость
|
||||
|
||||
✅ Все форматы JSON остались прежними
|
||||
✅ Старые карты и решения работают без изменений
|
||||
✅ Можно использовать параллельно со старыми проектами
|
||||
|
||||
## 🔗 Связанные файлы
|
||||
|
||||
- [WEB-TOOLS.md](WEB-TOOLS.md) - общий обзор веб-инструментов
|
||||
- [MAP-FORMAT.md](MAP-FORMAT.md) - формат карт
|
||||
- [maps/](maps/) - коллекция карт
|
||||
|
||||
---
|
||||
|
||||
**Создано:** 20 октября 2025
|
||||
**Версия:** 2.0 (Объединенная)
|
||||
|
||||
190
SEPARATION-SUMMARY.md
Normal file
190
SEPARATION-SUMMARY.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# 📊 Итоги разделения проектов
|
||||
|
||||
## ✅ Выполненные изменения
|
||||
|
||||
### Создано два независимых проекта:
|
||||
|
||||
#### 1. 🗺️ Редактор карт (`map-editor/`)
|
||||
- **Назначение:** Создание и редактирование карт
|
||||
- **Функции:**
|
||||
- Рисование карт
|
||||
- Экспорт/импорт JSON
|
||||
- 6 типов ячеек
|
||||
- Валидация данных
|
||||
|
||||
#### 2. 🎬 Визуализатор решений (`solution-player/`)
|
||||
- **Назначение:** Визуализация и анимация решений
|
||||
- **Функции:**
|
||||
- Загрузка карт и решений
|
||||
- Пошаговое воспроизведение
|
||||
- Регулировка скорости
|
||||
- Отображение векторов
|
||||
- Детальная информация
|
||||
|
||||
---
|
||||
|
||||
## 📂 Структура файлов
|
||||
|
||||
### map-editor/
|
||||
```
|
||||
map-editor/
|
||||
├── index.html ✅ Обновлен (удалена визуализация)
|
||||
├── editor.js ✅ Обновлен (только редактирование)
|
||||
├── README.md ✅ Обновлен
|
||||
├── open-editor.sh ✅ Существующий скрипт
|
||||
├── *.json ✅ Карты и примеры
|
||||
└── документация...
|
||||
```
|
||||
|
||||
### solution-player/
|
||||
```
|
||||
solution-player/
|
||||
├── index.html ✅ Создан (только визуализация)
|
||||
├── player.js ✅ Создан (только плеер)
|
||||
├── README.md ✅ Создан
|
||||
├── open-player.sh ✅ Создан
|
||||
├── demo-with-start.json ✅ Скопирован
|
||||
├── simple-track.json ✅ Скопирован
|
||||
├── easy-test.json ✅ Скопирован
|
||||
├── example-solution.json ✅ Скопирован
|
||||
├── complex-solution.json ✅ Скопирован
|
||||
└── demo-solution.json ✅ Скопирован
|
||||
```
|
||||
|
||||
### Корневая документация
|
||||
```
|
||||
racing/
|
||||
├── WEB-TOOLS.md ✅ Создан (обзор обоих проектов)
|
||||
└── SEPARATION-SUMMARY.md ✅ Этот файл
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Основные изменения
|
||||
|
||||
### map-editor/index.html
|
||||
- ❌ Удалена секция "Визуализация решения"
|
||||
- ❌ Удалена панель воспроизведения
|
||||
- ❌ Удалены элементы управления плеером
|
||||
- ❌ Удалена информация о шагах
|
||||
- ❌ Удалены неиспользуемые CSS стили
|
||||
- ✅ Добавлена ссылка на визуализатор решений
|
||||
|
||||
### map-editor/editor.js
|
||||
- ❌ Удалены функции визуализации траектории
|
||||
- ❌ Удалены функции воспроизведения
|
||||
- ❌ Удалены переменные состояния плеера
|
||||
- ❌ Удалены функции симуляции
|
||||
- ✅ Оставлен только редактор карт
|
||||
|
||||
### solution-player/ (новый проект)
|
||||
- ✅ Создан полностью новый HTML
|
||||
- ✅ Создан player.js с функциями визуализации
|
||||
- ✅ Добавлена возможность загрузки карты
|
||||
- ✅ Добавлена возможность загрузки решения
|
||||
- ✅ Добавлена навигация вперед/назад
|
||||
- ✅ Улучшен UI/UX для просмотра
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Как использовать
|
||||
|
||||
### Редактор карт
|
||||
```bash
|
||||
cd map-editor
|
||||
./open-editor.sh
|
||||
```
|
||||
- Создавайте карты
|
||||
- Экспортируйте JSON
|
||||
- Переходите к визуализатору через ссылку
|
||||
|
||||
### Визуализатор решений
|
||||
```bash
|
||||
cd solution-player
|
||||
./open-player.sh
|
||||
```
|
||||
- Загрузите карту
|
||||
- Загрузите решение
|
||||
- Наслаждайтесь анимацией
|
||||
|
||||
---
|
||||
|
||||
## 📋 Workflow
|
||||
|
||||
```
|
||||
1. Создание карты
|
||||
↓
|
||||
map-editor → export map.json
|
||||
|
||||
2. Генерация решения
|
||||
↓
|
||||
dotnet run → solution.json
|
||||
|
||||
3. Визуализация
|
||||
↓
|
||||
solution-player → load map + solution
|
||||
↓
|
||||
Просмотр анимации
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Преимущества разделения
|
||||
|
||||
### ✅ Разделение ответственности
|
||||
- Редактор занимается только картами
|
||||
- Плеер занимается только визуализацией
|
||||
|
||||
### ✅ Независимость
|
||||
- Каждый проект можно использовать отдельно
|
||||
- Упрощенный код
|
||||
- Легче поддерживать
|
||||
|
||||
### ✅ Улучшенный UX
|
||||
- Четкое назначение каждого инструмента
|
||||
- Упрощенный интерфейс
|
||||
- Меньше путаницы
|
||||
|
||||
### ✅ Масштабируемость
|
||||
- Легко добавлять новые функции
|
||||
- Проще тестировать
|
||||
- Модульная архитектура
|
||||
|
||||
---
|
||||
|
||||
## 📚 Документация
|
||||
|
||||
- **WEB-TOOLS.md** - Обзор обоих инструментов
|
||||
- **map-editor/README.md** - Документация редактора
|
||||
- **solution-player/README.md** - Документация визуализатора
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Связи между проектами
|
||||
|
||||
1. **map-editor** создает JSON карты
|
||||
2. **C# solver** генерирует решения
|
||||
3. **solution-player** визуализирует результаты
|
||||
|
||||
Все проекты полностью независимы и связаны только через JSON файлы.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Итого
|
||||
|
||||
Проект успешно разделен на два независимых веб-приложения:
|
||||
- ✅ Редактор карт (map-editor)
|
||||
- ✅ Визуализатор решений (solution-player)
|
||||
|
||||
Оба приложения:
|
||||
- ✅ Полностью функциональны
|
||||
- ✅ Имеют свою документацию
|
||||
- ✅ Имеют скрипты запуска
|
||||
- ✅ Включают примеры файлов
|
||||
- ✅ Работают без сервера
|
||||
- ✅ Современный UI/UX
|
||||
|
||||
---
|
||||
|
||||
Дата разделения: 2025-10-20
|
||||
|
||||
166
WEB-TOOLS.md
Normal file
166
WEB-TOOLS.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# 🌐 Веб-инструменты для "Гонки на бумаге"
|
||||
|
||||
В проекте доступны два независимых веб-приложения:
|
||||
|
||||
## 🗺️ Редактор карт
|
||||
|
||||
**Назначение:** Создание и редактирование карт для игры
|
||||
|
||||
**Расположение:** `map-editor/`
|
||||
|
||||
**Функции:**
|
||||
- Рисование карт мышью
|
||||
- 6 типов ячеек (дорога, камень, снег, лёд, чекпоинт, старт)
|
||||
- Экспорт/импорт JSON
|
||||
- Изменение размеров карты
|
||||
- Валидация данных
|
||||
|
||||
**Запуск:**
|
||||
```bash
|
||||
cd map-editor
|
||||
./open-editor.sh
|
||||
# или просто откройте index.html в браузере
|
||||
```
|
||||
|
||||
**Документация:** [map-editor/README.md](map-editor/README.md)
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Визуализатор решений
|
||||
|
||||
**Назначение:** Анимация и визуализация решений
|
||||
|
||||
**Расположение:** `solution-player/`
|
||||
|
||||
**Функции:**
|
||||
- Загрузка карт и решений
|
||||
- Пошаговое воспроизведение
|
||||
- Регулировка скорости (1x-10x)
|
||||
- Навигация вперед/назад
|
||||
- Отображение векторов скорости
|
||||
- Детальная информация о каждом шаге
|
||||
|
||||
**Запуск:**
|
||||
```bash
|
||||
cd solution-player
|
||||
./open-player.sh
|
||||
# или просто откройте index.html в браузере
|
||||
```
|
||||
|
||||
**Документация:** [solution-player/README.md](solution-player/README.md)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Создание и тестирование карты
|
||||
|
||||
1. **Создайте карту:**
|
||||
```bash
|
||||
cd map-editor
|
||||
./open-editor.sh
|
||||
```
|
||||
- Нарисуйте карту
|
||||
- Обязательно добавьте точку старта (тип 5)
|
||||
- Экспортируйте в JSON
|
||||
|
||||
2. **Решите карту:**
|
||||
```bash
|
||||
cd ..
|
||||
dotnet run --project racing-astar.csproj maps/your-map.json
|
||||
```
|
||||
|
||||
3. **Визуализируйте решение:**
|
||||
```bash
|
||||
cd solution-player
|
||||
./open-player.sh
|
||||
```
|
||||
- Загрузите карту
|
||||
- Загрузите решение
|
||||
- Нажмите Play
|
||||
|
||||
---
|
||||
|
||||
## 📋 Форматы файлов
|
||||
|
||||
### Карта (map)
|
||||
|
||||
```json
|
||||
{
|
||||
"map": [
|
||||
[0, 0, 5],
|
||||
[0, 1, 0],
|
||||
[4, 0, 0]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Типы ячеек:**
|
||||
- `0` - Дорога
|
||||
- `1` - Камень (препятствие)
|
||||
- `2` - Снег (замедление)
|
||||
- `3` - Лёд (скольжение)
|
||||
- `4` - Чекпоинт
|
||||
- `5` - Старт
|
||||
|
||||
### Решение (solution)
|
||||
|
||||
```json
|
||||
{
|
||||
"solution": [
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
[-1, 0]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Формат:** массив векторов ускорения `[[ax, ay], ...]`
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Технологии
|
||||
|
||||
Оба приложения:
|
||||
- ✅ Работают без сервера
|
||||
- ✅ Pure HTML/CSS/JavaScript
|
||||
- ✅ HTML5 Canvas
|
||||
- ✅ Современный UI/UX
|
||||
- ✅ Не требуют установки
|
||||
|
||||
---
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
racing/
|
||||
├── map-editor/ # Редактор карт
|
||||
│ ├── index.html
|
||||
│ ├── editor.js
|
||||
│ ├── README.md
|
||||
│ └── *.json # Примеры карт
|
||||
│
|
||||
├── solution-player/ # Визуализатор решений
|
||||
│ ├── index.html
|
||||
│ ├── player.js
|
||||
│ ├── README.md
|
||||
│ └── *.json # Примеры карт и решений
|
||||
│
|
||||
└── maps/ # Основная коллекция карт
|
||||
└── *.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Связь между приложениями
|
||||
|
||||
1. **Редактор карт** → создает JSON файлы карт
|
||||
2. **C# Solver** → принимает карты, генерирует решения
|
||||
3. **Визуализатор** → отображает карты и решения
|
||||
|
||||
Приложения полностью независимы и могут использоваться отдельно.
|
||||
|
||||
---
|
||||
|
||||
Создано для проекта [Racing](README.md)
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
# 🎯 Возможности редактора карт
|
||||
|
||||
## 🗺️ Редактирование карт
|
||||
|
||||
### Интерфейс редактора
|
||||
- **Современный дизайн** с градиентами и анимациями
|
||||
- **Адаптивная сетка** для различных размеров экрана
|
||||
- **Интуитивные элементы управления** с подсказками
|
||||
- **Цветовая палитра** для быстрого выбора типов ячеек
|
||||
|
||||
### Типы поверхностей
|
||||
1. **Дорога (0)** - Обычная поверхность
|
||||
2. **Камень (1)** - Непроходимое препятствие
|
||||
3. **Снег (2)** - Поверхность с замедлением
|
||||
4. **Лёд (3)** - Скользкая поверхность
|
||||
5. **Чекпоинт (4)** - Контрольная точка маршрута
|
||||
6. **Старт (5)** - Начальная точка движения
|
||||
|
||||
### Инструменты рисования
|
||||
- **Одиночный клик** - установка одной ячейки
|
||||
- **Перетаскивание мыши** - непрерывное рисование
|
||||
- **Переключение типов** - мгновенный выбор из палитры
|
||||
- **Изменение размеров** - от 5×5 до 100×100
|
||||
- **Очистка карты** - быстрый сброс
|
||||
|
||||
### Импорт/Экспорт
|
||||
- **JSON формат** - стандартный формат данных
|
||||
- **Валидация** - проверка корректности при импорте
|
||||
- **Автоматическое именование** - файлы именуются по размеру
|
||||
- **Консольный вывод** - JSON доступен в консоли браузера (F12)
|
||||
|
||||
## 🎬 Визуализация решений
|
||||
|
||||
### Загрузка и симуляция
|
||||
- **Формат решения** - массив векторов ускорения `[[ax, ay], ...]`
|
||||
- **Физика движения** - точная симуляция инерции и ускорения
|
||||
- **Поиск старта** - автоматическое определение начальной позиции
|
||||
- **Валидация** - проверка корректности формата решения
|
||||
|
||||
### Элементы визуализации
|
||||
|
||||
#### Траектория движения
|
||||
- 🔵 **Синяя линия** - пройденный путь
|
||||
- 🔵 **Синие точки** - позиции на каждом шаге
|
||||
- 🔴 **Красный круг** - текущая позиция (10px)
|
||||
- ➡️ **Красная стрелка** - вектор скорости
|
||||
|
||||
#### Информационная панель
|
||||
Показывает в реальном времени:
|
||||
- **Номер шага** - текущий/всего
|
||||
- **Позиция (x, y)** - координаты на карте
|
||||
- **Скорость (vx, vy)** - вектор скорости
|
||||
- **Ускорение (ax, ay)** - текущий вектор ускорения
|
||||
|
||||
### Управление воспроизведением
|
||||
|
||||
#### Кнопки управления
|
||||
- **▶ Play** - автоматическое воспроизведение
|
||||
- **⏸ Pause** - остановка воспроизведения
|
||||
- **⏮ Reset** - возврат к началу
|
||||
- **⏭ Step** - пошаговое движение вперёд
|
||||
|
||||
#### Настройки скорости
|
||||
- **Slider** - регулятор от 1x до 10x
|
||||
- **Динамическое изменение** - применяется на лету
|
||||
- **Индикатор** - отображение текущей скорости
|
||||
|
||||
### Физическая модель
|
||||
|
||||
```
|
||||
Начальное состояние:
|
||||
position = start_cell
|
||||
velocity = (0, 0)
|
||||
|
||||
На каждом шаге:
|
||||
1. velocity += acceleration
|
||||
2. position += velocity
|
||||
|
||||
Пример:
|
||||
Шаг 0: pos=(0,0), vel=(0,0), acc=(0,0)
|
||||
Шаг 1: acc=(1,1) → vel=(1,1) → pos=(1,1)
|
||||
Шаг 2: acc=(1,0) → vel=(2,1) → pos=(3,2)
|
||||
Шаг 3: acc=(0,1) → vel=(2,2) → pos=(5,4)
|
||||
```
|
||||
|
||||
## 🎨 Визуальные эффекты
|
||||
|
||||
### Анимации
|
||||
- **Плавные переходы** - smooth transitions на всех элементах
|
||||
- **Hover эффекты** - поднятие кнопок при наведении
|
||||
- **Пульсация** - текущая позиция выделяется
|
||||
- **Градиенты** - современные цветовые переходы
|
||||
|
||||
### Цветовая схема
|
||||
- **Основной фон** - градиент фиолетовый → пурпурный
|
||||
- **Карточки** - белый с тенями и скруглением
|
||||
- **Акценты** - фиолетовый (#667eea)
|
||||
- **Траектория** - синий с прозрачностью
|
||||
- **Текущая позиция** - ярко-красный (#f5576c)
|
||||
|
||||
## 🔧 Технические детали
|
||||
|
||||
### Canvas отрисовка
|
||||
- **Размер ячейки** - 30px
|
||||
- **Сглаживание** - anti-aliasing включён
|
||||
- **Слои** - карта → траектория → маркеры
|
||||
- **Обновление** - перерисовка только при изменении
|
||||
|
||||
### Производительность
|
||||
- **Vanilla JS** - без тяжёлых фреймворков
|
||||
- **Оптимизация** - минимум перерисовок
|
||||
- **Память** - эффективное использование
|
||||
- **Масштабируемость** - работает до 100×100
|
||||
|
||||
### Совместимость
|
||||
- ✅ Chrome/Chromium
|
||||
- ✅ Firefox
|
||||
- ✅ Safari
|
||||
- ✅ Edge
|
||||
- ✅ Любой современный браузер с Canvas API
|
||||
|
||||
## 📊 Варианты использования
|
||||
|
||||
### Для разработчиков
|
||||
1. Создание тестовых карт
|
||||
2. Отладка алгоритмов поиска пути
|
||||
3. Визуализация результатов решения
|
||||
4. Демонстрация работы алгоритмов
|
||||
|
||||
### Для преподавателей
|
||||
1. Демонстрация алгоритмов на лекциях
|
||||
2. Создание заданий для студентов
|
||||
3. Визуализация различных стратегий
|
||||
4. Анализ оптимальности решений
|
||||
|
||||
### Для студентов
|
||||
1. Понимание физики движения
|
||||
2. Анализ работы алгоритмов
|
||||
3. Сравнение различных решений
|
||||
4. Отладка собственных реализаций
|
||||
|
||||
## 🚀 Преимущества
|
||||
|
||||
### Простота использования
|
||||
- **Без установки** - работает в браузере
|
||||
- **Без зависимостей** - pure HTML/CSS/JS
|
||||
- **Интуитивный UI** - понятен с первого взгляда
|
||||
- **Примеры** - готовые карты и решения
|
||||
|
||||
### Функциональность
|
||||
- **Полный цикл** - от создания до визуализации
|
||||
- **Гибкость** - настройка всех параметров
|
||||
- **Точность** - корректная физическая модель
|
||||
- **Наглядность** - понятное отображение данных
|
||||
|
||||
### Расширяемость
|
||||
- **Открытый код** - легко модифицировать
|
||||
- **Модульность** - чёткая структура функций
|
||||
- **Документация** - подробные комментарии
|
||||
- **Примеры** - образцы для кастомизации
|
||||
|
||||
---
|
||||
|
||||
🎯 **Итог**: Мощный и удобный инструмент для работы с картами гонок и визуализации решений!
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
# 📑 Индекс файлов редактора карт
|
||||
|
||||
## 📚 Документация
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| **README.md** | Полная документация с инструкциями |
|
||||
| **QUICKSTART.md** | Быстрый старт за 2 минуты |
|
||||
| **FEATURES.md** | Подробное описание всех возможностей |
|
||||
| **INDEX.md** | Этот файл - навигация по проекту |
|
||||
|
||||
## 🎯 Основные файлы
|
||||
|
||||
| Файл | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| **index.html** | HTML | Главная страница редактора |
|
||||
| **editor.js** | JS | Логика редактора и визуализации |
|
||||
| **open-editor.sh** | Shell | Скрипт для быстрого запуска |
|
||||
|
||||
## 🗺️ Примеры карт
|
||||
|
||||
| Файл | Размер | Описание |
|
||||
|------|--------|----------|
|
||||
| **simple-track.json** | 10×10 | Простая трасса для начинающих |
|
||||
| **demo-with-start.json** | 15×15 | Демонстрационная карта с препятствиями |
|
||||
| **example-maps.json** | Разные | Коллекция из 5 различных карт |
|
||||
|
||||
### Содержимое example-maps.json:
|
||||
1. Simple Track (10×10) - простая трасса
|
||||
2. Ice Circuit (10×10) - карта с ледяными участками
|
||||
3. Obstacle Course (15×15) - сложная карта с препятствиями
|
||||
4. Minimal (5×5) - минимальная карта для тестирования
|
||||
5. Empty Large (20×20) - пустая карта для создания
|
||||
|
||||
## 🎬 Примеры решений
|
||||
|
||||
| Файл | Шагов | Сложность | Рекомендуется для |
|
||||
|------|-------|-----------|-------------------|
|
||||
| **example-solution.json** | 10 | Простая | simple-track.json |
|
||||
| **complex-solution.json** | 20 | Сложная | Любой карты |
|
||||
| **demo-solution.json** | 15 | Средняя | demo-with-start.json |
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Вариант 1: Просмотр примера
|
||||
1. Откройте `index.html`
|
||||
2. Импортируйте `demo-with-start.json`
|
||||
3. Загрузите решение `demo-solution.json`
|
||||
4. Нажмите ▶ Play
|
||||
|
||||
### Вариант 2: Создание своей карты
|
||||
1. Откройте `index.html`
|
||||
2. Создайте карту с помощью инструментов
|
||||
3. Экспортируйте в JSON
|
||||
4. Используйте в игре
|
||||
|
||||
## 📖 Рекомендуемый порядок чтения
|
||||
|
||||
Для новичков:
|
||||
1. 📄 **QUICKSTART.md** - начните здесь
|
||||
2. 🎮 Поэкспериментируйте с редактором
|
||||
3. 📚 **README.md** - полная документация
|
||||
4. 🎯 **FEATURES.md** - подробности
|
||||
|
||||
Для опытных:
|
||||
1. 📚 **README.md** - вся информация сразу
|
||||
2. 🎯 **FEATURES.md** - технические детали
|
||||
|
||||
## 🎨 Типы ячеек
|
||||
|
||||
| Код | Тип | Цвет | Маркер |
|
||||
|-----|-----|------|--------|
|
||||
| 0 | Дорога | Светло-серый | - |
|
||||
| 1 | Камень | Тёмно-серый | - |
|
||||
| 2 | Снег | Голубой | - |
|
||||
| 3 | Лёд | Светло-голубой | - |
|
||||
| 4 | Чекпоинт | Жёлтый | C |
|
||||
| 5 | Старт | Зелёный | S |
|
||||
|
||||
## 📦 Форматы файлов
|
||||
|
||||
### Карта (map)
|
||||
```json
|
||||
{
|
||||
"map": [[0, 1, 2], [3, 4, 5]]
|
||||
}
|
||||
```
|
||||
|
||||
### Решение (solution)
|
||||
```json
|
||||
{
|
||||
"solution": [[1, 0], [0, 1], [-1, 0]]
|
||||
}
|
||||
```
|
||||
|
||||
## 🔗 Полезные ссылки
|
||||
|
||||
- [Основной README проекта](../README.md)
|
||||
- [Документация BFS алгоритма](../README.md)
|
||||
- [Документация A* алгоритма](../ASTAR-README.md)
|
||||
|
||||
## 💡 Быстрые команды
|
||||
|
||||
```bash
|
||||
# Открыть редактор
|
||||
./open-editor.sh
|
||||
|
||||
# Или напрямую в браузере
|
||||
firefox index.html
|
||||
chrome index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Создано**: 2025-10-19
|
||||
**Версия**: 2.0 (с визуализацией решений)
|
||||
**Технологии**: HTML5, CSS3, JavaScript ES6+, Canvas API
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# 🚀 Быстрый старт - Редактор карт
|
||||
|
||||
## За 2 минуты
|
||||
|
||||
### 1️⃣ Открыть редактор
|
||||
```bash
|
||||
./open-editor.sh
|
||||
```
|
||||
или просто откройте `index.html` в браузере
|
||||
|
||||
### 2️⃣ Создать карту
|
||||
1. Установите размеры (например, 15×15)
|
||||
2. Выберите тип ячейки из палитры
|
||||
3. Рисуйте мышью на карте
|
||||
4. Обязательно добавьте точку **Старта** (зелёная, код 5)
|
||||
|
||||
### 3️⃣ Сохранить карту
|
||||
- Нажмите "📥 Экспорт JSON"
|
||||
- Файл скачается автоматически
|
||||
|
||||
### 4️⃣ Визуализировать решение (опционально)
|
||||
1. Нажмите "📂 Загрузить решение"
|
||||
2. Выберите файл с решением (например, `example-solution.json`)
|
||||
3. Нажмите ▶ Play
|
||||
4. Наслаждайтесь анимацией!
|
||||
|
||||
## 🎨 Типы ячеек
|
||||
|
||||
| Кнопка | Код | Описание |
|
||||
|--------|-----|----------|
|
||||
| Дорога | 0 | Обычная дорога (серая) |
|
||||
| Камень | 1 | Препятствие (тёмно-серая) |
|
||||
| Снег | 2 | Замедление (голубая) |
|
||||
| Лёд | 3 | Скользко (светло-голубая) |
|
||||
| Чекпоинт | 4 | Контрольная точка (жёлтая, "C") |
|
||||
| **Старт** | **5** | **Точка старта (зелёная, "S")** |
|
||||
|
||||
## 🎮 Управление визуализацией
|
||||
|
||||
| Кнопка | Действие |
|
||||
|--------|----------|
|
||||
| ▶ Play | Автоматическое воспроизведение |
|
||||
| ⏸ Pause | Пауза |
|
||||
| ⏮ Reset | Сброс к началу |
|
||||
| ⏭ Step | Один шаг вперёд |
|
||||
| Slider | Скорость (1x - 10x) |
|
||||
|
||||
## 📄 Формат решения
|
||||
|
||||
```json
|
||||
{
|
||||
"solution": [
|
||||
[1, 0], // вправо
|
||||
[1, 1], // вправо-вниз
|
||||
[0, 1], // вниз
|
||||
[-1, 0], // торможение по X
|
||||
[0, -1] // вверх
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 Советы
|
||||
- Удерживайте мышь для быстрого рисования
|
||||
- Используйте Step для детального анализа
|
||||
- Начните с примеров: `demo-with-start.json` + `example-solution.json`
|
||||
|
||||
---
|
||||
|
||||
📖 Полная документация: [README.md](README.md)
|
||||
|
||||
|
||||
@@ -1,64 +1,47 @@
|
||||
# 🗺️ Редактор карт для "Гонки на бумаге"
|
||||
|
||||
Веб-приложение для создания и редактирования карт для игры "Гонки на бумаге".
|
||||
Веб-приложение для создания и редактирования карт. Работает без сервера, просто откройте в браузере.
|
||||
|
||||
> **Примечание:** Для визуализации решений используйте [Визуализатор решений](../solution-player/).
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
Просто откройте `index.html` в браузере. Никаких зависимостей или установки не требуется!
|
||||
|
||||
```bash
|
||||
# Из папки map-editor
|
||||
firefox index.html
|
||||
# Запуск редактора
|
||||
./open-editor.sh
|
||||
# или
|
||||
chrome index.html
|
||||
# или просто откройте файл двойным кликом
|
||||
firefox index.html
|
||||
```
|
||||
|
||||
## 📖 Инструкция
|
||||
**Три простых шага:**
|
||||
1. Установите размеры (5-100), выберите тип ячейки из палитры
|
||||
2. Рисуйте мышью на карте (обязательно добавьте точку старта)
|
||||
3. Нажмите "Экспорт JSON" - файл скачается автоматически
|
||||
|
||||
### Типы ячеек
|
||||
## 🎨 Типы ячеек
|
||||
|
||||
| Тип | Код | Описание | Цвет |
|
||||
|-----|-----|----------|------|
|
||||
| Дорога | 0 | Обычная дорога | Светло-серый |
|
||||
| Камень | 1 | Препятствие (непроходимо) | Тёмно-серый |
|
||||
| Снег | 2 | Замедление движения | Голубой |
|
||||
| Лёд | 3 | Скользкая поверхность | Светло-голубой |
|
||||
| Чекпоинт | 4 | Контрольная точка | Жёлтый с "C" |
|
||||
| Старт | 5 | Точка старта | Зелёный с "S" |
|
||||
| Код | Тип | Описание | Цвет | Маркер |
|
||||
|-----|-----|----------|------|--------|
|
||||
| 0 | Дорога | Обычная дорога | Светло-серый | - |
|
||||
| 1 | Камень | Препятствие (непроходимо) | Тёмно-серый | - |
|
||||
| 2 | Снег | Замедление движения | Голубой | - |
|
||||
| 3 | Лёд | Скользкая поверхность | Светло-голубой | - |
|
||||
| 4 | Чекпоинт | Контрольная точка | Жёлтый | C |
|
||||
| 5 | Старт | Точка старта | Зелёный | S |
|
||||
|
||||
### Использование
|
||||
## 📖 Использование
|
||||
|
||||
1. **Изменение размеров:**
|
||||
- Укажите ширину и высоту карты (5-100)
|
||||
- Нажмите "Применить"
|
||||
- Существующие данные сохранятся при изменении размера
|
||||
|
||||
2. **Рисование:**
|
||||
**Рисование:**
|
||||
- Выберите тип ячейки из палитры
|
||||
- Кликните на ячейку для изменения типа
|
||||
- Удерживайте кнопку мыши для рисования
|
||||
- Кликните на ячейку или удерживайте мышь для быстрого рисования
|
||||
|
||||
3. **Экспорт:**
|
||||
- Нажмите "Экспорт JSON"
|
||||
- Файл автоматически скачается
|
||||
- JSON также выводится в консоль браузера (F12)
|
||||
**Экспорт/Импорт:**
|
||||
- Экспорт: нажмите "Экспорт JSON" (также выводится в консоль F12)
|
||||
- Импорт: нажмите "Импорт JSON" и выберите файл
|
||||
|
||||
4. **Импорт:**
|
||||
- Нажмите "Импорт JSON"
|
||||
- Выберите JSON файл
|
||||
- Карта загрузится автоматически
|
||||
|
||||
5. **Визуализация решения:**
|
||||
- Убедитесь, что на карте есть точка старта (тип 5)
|
||||
- Нажмите "Загрузить решение"
|
||||
- Выберите JSON файл с решением
|
||||
- Используйте кнопки управления:
|
||||
- ▶ Play - автоматическое воспроизведение
|
||||
- ⏸ Pause - пауза
|
||||
- ⏮ Reset - сброс к началу
|
||||
- ⏭ Step - шаг вперед
|
||||
- Регулируйте скорость воспроизведения (1x - 10x)
|
||||
**Изменение размеров:**
|
||||
- Укажите ширину и высоту (5-100), нажмите "Применить"
|
||||
- Существующие данные сохранятся при изменении размера
|
||||
|
||||
## 📄 Формат JSON
|
||||
|
||||
@@ -74,106 +57,13 @@ chrome index.html
|
||||
}
|
||||
```
|
||||
|
||||
### Структура карты:
|
||||
**Структура:**
|
||||
- `map` - двумерный массив целых чисел (int[][])
|
||||
- Каждая строка массива = строка карты
|
||||
- Каждый элемент = тип ячейки (0-5)
|
||||
- Первая строка = верхняя строка карты
|
||||
- Первый элемент в строке = левая ячейка
|
||||
- Значения: 0-5 (типы ячеек)
|
||||
|
||||
## 🎬 Формат решения
|
||||
|
||||
Решение представляет собой JSON файл с массивом векторов ускорений:
|
||||
|
||||
```json
|
||||
{
|
||||
"solution": [
|
||||
[1, 1],
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
[-1, 0],
|
||||
[0, -1]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Структура решения:
|
||||
- `solution` - массив векторов ускорения [[ax, ay], ...]
|
||||
- Каждый элемент - вектор ускорения [ax, ay]
|
||||
- ax, ay - целые числа (обычно от -1 до 1)
|
||||
- Применяются последовательно от точки старта
|
||||
|
||||
### Физика движения:
|
||||
1. Начальная позиция - ячейка с типом 5 (старт)
|
||||
2. Начальная скорость = (0, 0)
|
||||
3. На каждом шаге:
|
||||
- velocity += acceleration
|
||||
- position += velocity
|
||||
|
||||
### Пример:
|
||||
```
|
||||
Шаг 0: pos=(0,0), vel=(0,0), acc=(0,0)
|
||||
Шаг 1: acc=(1,1) → vel=(1,1) → pos=(1,1)
|
||||
Шаг 2: acc=(1,0) → vel=(2,1) → pos=(3,2)
|
||||
Шаг 3: acc=(0,1) → vel=(2,2) → pos=(5,4)
|
||||
```
|
||||
|
||||
### Визуализация показывает:
|
||||
- 🔵 Синяя линия - пройденная траектория
|
||||
- 🔴 Красный круг - текущая позиция
|
||||
- ➡️ Красная стрелка - направление и скорость движения
|
||||
- Информацию о шаге, позиции, скорости и ускорении
|
||||
|
||||
## 🎨 Особенности
|
||||
|
||||
### Редактор карт:
|
||||
- ✅ Современный красивый интерфейс
|
||||
- ✅ Интуитивное рисование мышью
|
||||
- ✅ Валидация данных при импорте
|
||||
- ✅ Автоматическое сохранение с именем по размеру
|
||||
- ✅ Вывод JSON в консоль для быстрого копирования
|
||||
- ✅ Адаптивный дизайн
|
||||
- ✅ Сохранение данных при изменении размера
|
||||
- ✅ 6 типов ячеек (дорога, камень, снег, лёд, чекпоинт, старт)
|
||||
|
||||
### Визуализация решений:
|
||||
- ✅ Пошаговое воспроизведение траектории
|
||||
- ✅ Регулируемая скорость воспроизведения (1x - 10x)
|
||||
- ✅ Отображение вектора скорости в реальном времени
|
||||
- ✅ Информация о позиции, скорости и ускорении на каждом шаге
|
||||
- ✅ Плавная анимация движения
|
||||
- ✅ Визуализация пройденного пути
|
||||
- ✅ Управление: Play, Pause, Reset, Step
|
||||
|
||||
### Технологии:
|
||||
- ✅ Работает без сервера (pure HTML/JS/CSS)
|
||||
- ✅ HTML5 Canvas для отрисовки
|
||||
- ✅ Vanilla JavaScript (ES6+)
|
||||
- ✅ FileReader API для импорта
|
||||
- ✅ Blob API для экспорта
|
||||
|
||||
## 💡 Советы
|
||||
|
||||
### Работа с картами:
|
||||
- Используйте консоль браузера (F12) для просмотра экспортированного JSON
|
||||
- При импорте проверяются все значения ячеек (только 0-5)
|
||||
- Все строки карты должны иметь одинаковую длину
|
||||
- Максимальный размер карты: 100×100
|
||||
- Минимальный размер карты: 5×5
|
||||
- Рекомендуется устанавливать одну точку старта (5) на карте
|
||||
|
||||
### Работа с визуализацией:
|
||||
- Сначала создайте или загрузите карту с точкой старта
|
||||
- Решение загружайте из отдельного JSON файла
|
||||
- Используйте Step для детального анализа каждого шага
|
||||
- Регулируйте скорость для комфортного просмотра
|
||||
- Красная стрелка показывает направление и скорость движения
|
||||
- Точка старта должна совпадать с началом траектории решения
|
||||
- Примеры решений: `example-solution.json`, `complex-solution.json`
|
||||
|
||||
## 🔗 Интеграция с игрой
|
||||
|
||||
Экспортированный JSON можно использовать в C# коде:
|
||||
## 🔗 Интеграция с C#
|
||||
|
||||
```csharp
|
||||
using System.Text.Json;
|
||||
@@ -182,79 +72,34 @@ var json = File.ReadAllText("racing-map-15x15.json");
|
||||
var mapData = JsonSerializer.Deserialize<MapData>(json);
|
||||
int[][] map = mapData.map;
|
||||
|
||||
// Использование карты
|
||||
for (int y = 0; y < map.Length; y++)
|
||||
{
|
||||
for (int x = 0; x < map[y].Length; x++)
|
||||
{
|
||||
int cellType = map[y][x];
|
||||
// обработка...
|
||||
}
|
||||
}
|
||||
|
||||
public class MapData
|
||||
{
|
||||
public int[][] map { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ Технологии
|
||||
## 🎯 Файлы проекта
|
||||
|
||||
- HTML5 Canvas
|
||||
- Vanilla JavaScript (ES6+)
|
||||
- CSS3 с градиентами и анимациями
|
||||
- FileReader API для импорта
|
||||
- Blob API для экспорта
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `index.html` | Главная страница редактора |
|
||||
| `editor.js` | Логика редактора и визуализация |
|
||||
| `open-editor.sh` | Скрипт для быстрого запуска |
|
||||
| `demo-with-start.json` | Демонстрационная карта 15×15 |
|
||||
|
||||
## 📝 Файлы примеров
|
||||
## 🎨 Особенности
|
||||
|
||||
### Карты:
|
||||
- `simple-track.json` - Простая трасса 10×10
|
||||
- `demo-with-start.json` - Демонстрационная карта 15×15
|
||||
- `example-maps.json` - Коллекция различных карт
|
||||
- ✅ Современный интерфейс, работает без сервера
|
||||
- ✅ Интуитивное рисование мышью
|
||||
- ✅ Валидация данных при импорте
|
||||
- ✅ Автоматическое сохранение с именем по размеру
|
||||
- ✅ HTML5 Canvas, Vanilla JavaScript (ES6+)
|
||||
|
||||
### Решения:
|
||||
- `example-solution.json` - Простое решение на 10 шагов
|
||||
- `complex-solution.json` - Сложное решение на 20 шагов
|
||||
## 🔗 Связанные проекты
|
||||
|
||||
## 🎯 Быстрый старт с визуализацией
|
||||
|
||||
1. Откройте редактор:
|
||||
```bash
|
||||
./open-editor.sh
|
||||
# или просто откройте index.html в браузере
|
||||
```
|
||||
|
||||
2. Загрузите карту с примером:
|
||||
- Нажмите "Импорт JSON"
|
||||
- Выберите `demo-with-start.json`
|
||||
|
||||
3. Загрузите решение:
|
||||
- Нажмите "Загрузить решение"
|
||||
- Выберите `example-solution.json`
|
||||
|
||||
4. Запустите визуализацию:
|
||||
- Нажмите ▶ Play
|
||||
- Наблюдайте за движением
|
||||
- Экспериментируйте со скоростью и пошаговым режимом
|
||||
|
||||
## 🔧 Создание собственного решения
|
||||
|
||||
Пример структуры файла решения:
|
||||
|
||||
```json
|
||||
{
|
||||
"solution": [
|
||||
[1, 0], // ускорение вправо
|
||||
[1, 0], // еще ускорение вправо (скорость нарастает)
|
||||
[0, 1], // ускорение вниз
|
||||
[-1, 0], // торможение по X
|
||||
[0, 0] // без ускорения (движение по инерции)
|
||||
]
|
||||
}
|
||||
```
|
||||
- [Визуализатор решений](../solution-player/) - анимация решений
|
||||
- [Основной проект](../README.md) - C# решатель на основе A*
|
||||
|
||||
---
|
||||
|
||||
Создано для проекта [Racing](../README.md)
|
||||
|
||||
**Технологии:** HTML5, CSS3, JavaScript ES6+, Canvas API
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"solution": [
|
||||
[1, 0],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
[0, 1],
|
||||
[-1, 0],
|
||||
[-1, 0],
|
||||
[0, -1],
|
||||
[0, -1],
|
||||
[1, 0],
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
[0, 0],
|
||||
[-1, 0],
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
[0, -1],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[-1, -1]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"solution": [
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[-1, 0],
|
||||
[-1, 0],
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
[1, 0],
|
||||
[0, -1],
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
[0, 1]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -17,15 +17,6 @@ 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');
|
||||
@@ -99,98 +90,6 @@ function drawMap() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Рисуем визуализацию траектории, если она есть
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Выбор типа ячейки
|
||||
@@ -387,187 +286,5 @@ 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();
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
{
|
||||
"examples": [
|
||||
{
|
||||
"name": "Simple Track",
|
||||
"description": "Простая карта 10x10 с точкой старта и чекпоинтом",
|
||||
"map": [
|
||||
[5, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 1, 0, 0, 1, 1, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
[0, 1, 0, 2, 2, 2, 2, 0, 1, 0],
|
||||
[0, 1, 0, 2, 4, 4, 2, 0, 1, 0],
|
||||
[0, 1, 0, 2, 2, 2, 2, 0, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
[0, 1, 1, 1, 0, 0, 1, 1, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ice Circuit",
|
||||
"description": "Карта с ледяными участками и точкой старта",
|
||||
"map": [
|
||||
[5, 0, 0, 1, 1, 1, 1, 0, 0, 0],
|
||||
[0, 3, 3, 3, 0, 0, 1, 0, 4, 0],
|
||||
[0, 3, 1, 3, 0, 0, 1, 0, 0, 0],
|
||||
[0, 3, 3, 3, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 2, 2, 2, 0, 0],
|
||||
[0, 0, 2, 2, 0, 2, 1, 2, 0, 0],
|
||||
[0, 0, 2, 1, 0, 2, 2, 2, 0, 0],
|
||||
[0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 3, 3, 0],
|
||||
[0, 4, 0, 0, 1, 1, 0, 3, 3, 0]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Obstacle Course",
|
||||
"description": "Сложная карта с множеством препятствий и точкой старта",
|
||||
"map": [
|
||||
[5, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 4, 0, 0, 1, 0, 2, 2, 2, 0, 1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 1, 0, 2, 1, 2, 0, 0, 0, 0, 0, 0],
|
||||
[1, 1, 1, 0, 0, 0, 2, 2, 2, 0, 0, 3, 3, 3, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 3, 0],
|
||||
[0, 3, 3, 3, 0, 1, 1, 0, 1, 1, 0, 3, 3, 3, 0],
|
||||
[0, 3, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 3, 3, 3, 0, 0, 4, 0, 0, 0, 2, 2, 2, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 2, 1, 2, 0, 0],
|
||||
[0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0],
|
||||
[0, 2, 2, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 4],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 0, 0, 3, 3, 3, 3, 0, 0, 1, 1, 1, 0, 0],
|
||||
[0, 1, 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]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Minimal",
|
||||
"description": "Минимальная карта 5x5 для тестирования с точкой старта",
|
||||
"map": [
|
||||
[5, 0, 0, 0, 4],
|
||||
[0, 1, 1, 0, 0],
|
||||
[0, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0],
|
||||
[4, 0, 0, 0, 0]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Empty Large",
|
||||
"description": "Пустая карта 20x20 для создания своей карты",
|
||||
"map": [
|
||||
[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, 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],
|
||||
[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, 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],
|
||||
[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, 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],
|
||||
[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, 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],
|
||||
[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, 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],
|
||||
[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, 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],
|
||||
[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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"solution": [
|
||||
[1, 1],
|
||||
[1, 0],
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
[0, 1],
|
||||
[-1, 0],
|
||||
[0, -1],
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[0, 1]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 30px;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -244,111 +243,6 @@
|
||||
border-radius: 4px;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.visualization-panel {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.visualization-panel h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.trajectory-path {
|
||||
stroke: #667eea;
|
||||
stroke-width: 3;
|
||||
fill: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.current-position {
|
||||
fill: #f5576c;
|
||||
stroke: white;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.start-marker {
|
||||
fill: #28a745;
|
||||
stroke: white;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -413,63 +307,18 @@
|
||||
</div>
|
||||
<input type="file" id="fileInput" accept=".json" onchange="importMap(event)">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>🎬 Визуализация решения</h3>
|
||||
<div class="buttons">
|
||||
<button class="btn-primary" onclick="document.getElementById('solutionInput').click()">📂 Загрузить решение</button>
|
||||
<button class="btn-danger" onclick="clearVisualization()" id="clearVisBtn" disabled>🗑️ Очистить</button>
|
||||
</div>
|
||||
<input type="file" id="solutionInput" accept=".json" onchange="loadSolution(event)" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="canvas-wrapper">
|
||||
<canvas id="mapCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div id="visualizationPanel" 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="stepForward()">⏭ Step</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 class="step-info">
|
||||
<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>
|
||||
|
||||
<div class="info">
|
||||
<strong>💡 Подсказки:</strong>
|
||||
• Кликайте по ячейкам для изменения типа<br>
|
||||
• Удерживайте кнопку мыши для рисования<br>
|
||||
• Экспортируйте карту в JSON для использования в игре<br>
|
||||
• Загрузите решение для визуализации прохождения карты
|
||||
• Для визуализации решений используйте отдельный <a href="../solution-player/index.html" style="color: #0d47a1; font-weight: bold;">Визуализатор решений</a>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"map": [
|
||||
[5, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 1, 0, 0, 1, 1, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
[0, 1, 0, 2, 2, 2, 2, 0, 1, 0],
|
||||
[0, 1, 0, 2, 4, 4, 2, 0, 1, 0],
|
||||
[0, 1, 0, 2, 2, 2, 2, 0, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
[0, 1, 1, 1, 0, 0, 1, 1, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
]
|
||||
}
|
||||
|
||||
259
maps/racing-map-15x15.json
Normal file
259
maps/racing-map-15x15.json
Normal file
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
3,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
3,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
4,
|
||||
3,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
3,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
3,
|
||||
3,
|
||||
0,
|
||||
4,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
4
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
294
racing-tools/README.md
Normal file
294
racing-tools/README.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# 🏁 Racing Tools - Инструменты для "Гонки на бумаге"
|
||||
|
||||
Веб-приложения для создания карт и визуализации решений. Работают без сервера, просто откройте в браузере.
|
||||
|
||||
## 📦 Что внутри
|
||||
|
||||
Два веб-приложения в одном проекте:
|
||||
|
||||
- **🏁 Редактор карт** (`editor.html`) - создание и редактирование игровых карт
|
||||
- **🎬 Визуализатор решений** (`player.html`) - анимация траекторий движения
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Вариант 1: Через скрипты
|
||||
```bash
|
||||
# Редактор карт
|
||||
./open-editor.sh
|
||||
|
||||
# Визуализатор решений
|
||||
./open-player.sh
|
||||
```
|
||||
|
||||
### Вариант 2: Напрямую в браузере
|
||||
```bash
|
||||
firefox editor.html # Редактор карт
|
||||
firefox player.html # Визуализатор решений
|
||||
```
|
||||
|
||||
### Вариант 3: Через Python HTTP сервер
|
||||
```bash
|
||||
python3 -m http.server 8000
|
||||
# Откройте http://localhost:8000/editor.html или http://localhost:8000/player.html
|
||||
```
|
||||
|
||||
## 🏁 Редактор карт
|
||||
|
||||
### Возможности
|
||||
- Создание карт размером от 5×5 до 100×100
|
||||
- Интуитивное рисование мышью (клик или удержание)
|
||||
- 6 типов ячеек: дорога, камень, снег, лёд, чекпоинт, старт
|
||||
- Экспорт/импорт в JSON формате
|
||||
- Изменение размера с сохранением данных
|
||||
|
||||
### Типы ячеек
|
||||
|
||||
| Код | Тип | Описание | Цвет | Маркер |
|
||||
|-----|-----|----------|------|--------|
|
||||
| 0 | Дорога | Обычная дорога | Светло-серый | - |
|
||||
| 1 | Камень | Препятствие (непроходимо) | Тёмно-серый | - |
|
||||
| 2 | Снег | Замедление движения | Голубой | - |
|
||||
| 3 | Лёд | Скользкая поверхность | Светло-голубой | - |
|
||||
| 4 | Чекпоинт | Контрольная точка | Жёлтый | C |
|
||||
| 5 | Старт | Точка старта (обязательно!) | Зелёный | S |
|
||||
|
||||
### Быстрый старт редактора
|
||||
|
||||
1. Откройте `editor.html`
|
||||
2. Установите размеры (по умолчанию 15×15)
|
||||
3. Выберите тип ячейки из палитры
|
||||
4. Рисуйте мышью на карте (обязательно добавьте точку старта - тип 5)
|
||||
5. Нажмите "Экспорт JSON" - файл скачается автоматически
|
||||
|
||||
## 🎬 Визуализатор решений
|
||||
|
||||
### Возможности
|
||||
- Загрузка карты и решения из JSON
|
||||
- Пошаговая анимация траектории
|
||||
- Контроль скорости воспроизведения (1x - 10x)
|
||||
- Ручное управление (шаг вперед/назад)
|
||||
- Отображение позиции, скорости и ускорения
|
||||
|
||||
### Элементы визуализации
|
||||
|
||||
- 🔵 **Синяя линия и точки** - пройденная траектория
|
||||
- 🔴 **Красный круг** - текущая позиция
|
||||
- ➡️ **Красная стрелка** - вектор скорости (направление и величина)
|
||||
- **Панель информации** - шаг, позиция (x, y), скорость (vx, vy), ускорение (ax, ay)
|
||||
|
||||
### Управление
|
||||
|
||||
| Кнопка | Действие |
|
||||
|--------|----------|
|
||||
| ▶ Play | Автоматическое воспроизведение |
|
||||
| ⏸ Pause | Пауза |
|
||||
| ⏮ Reset | Сброс к началу |
|
||||
| ⏪ Back | Шаг назад |
|
||||
| ⏩ Forward | Шаг вперед |
|
||||
| Слайдер | Скорость 1x - 10x |
|
||||
|
||||
### Быстрый старт визуализатора
|
||||
|
||||
1. Откройте `player.html`
|
||||
2. Загрузите карту (📂 Загрузить карту)
|
||||
3. Загрузите решение (🎬 Загрузить решение)
|
||||
4. Нажмите ▶ Play и наблюдайте за движением
|
||||
|
||||
## 📄 Форматы файлов
|
||||
|
||||
### Формат карты (map)
|
||||
|
||||
```json
|
||||
{
|
||||
"map": [
|
||||
[5, 0, 0, 1, 0],
|
||||
[0, 1, 0, 1, 0],
|
||||
[0, 0, 2, 2, 4]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Структура:**
|
||||
- `map` - двумерный массив целых чисел (int[][])
|
||||
- Первая строка = верхняя строка карты
|
||||
- Первый элемент в строке = левая ячейка
|
||||
- Значения: 0-5 (типы ячеек)
|
||||
- **Обязательно наличие точки старта (5)**
|
||||
|
||||
### Формат решения (solution)
|
||||
|
||||
```json
|
||||
{
|
||||
"solution": [
|
||||
[1, 1],
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
[-1, 0]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Структура:**
|
||||
- `solution` - массив векторов ускорения `[[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)
|
||||
Шаг 3: acc=(0,0) → vel=(2,1) → pos=(5,3)
|
||||
```
|
||||
|
||||
## 🔗 Интеграция с C#
|
||||
|
||||
### Чтение карты
|
||||
```csharp
|
||||
using System.Text.Json;
|
||||
|
||||
var json = File.ReadAllText("racing-map-15x15.json");
|
||||
var mapData = JsonSerializer.Deserialize<MapData>(json);
|
||||
int[][] map = mapData.map;
|
||||
|
||||
public class MapData
|
||||
{
|
||||
public int[][] map { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### Запись решения
|
||||
```csharp
|
||||
var solution = new { solution = new int[][] {
|
||||
new[] { 1, 1 },
|
||||
new[] { 1, 0 },
|
||||
new[] { 0, 1 }
|
||||
}};
|
||||
|
||||
var json = JsonSerializer.Serialize(solution, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
File.WriteAllText("solution.json", json);
|
||||
```
|
||||
|
||||
## 🎯 Структура проекта
|
||||
|
||||
```
|
||||
racing-tools/
|
||||
├── editor.html # Страница редактора карт
|
||||
├── player.html # Страница визуализатора решений
|
||||
├── styles.css # Общие стили для обоих приложений
|
||||
├── common.js # Общие функции и константы
|
||||
├── editor.js # Логика редактора
|
||||
├── player.js # Логика визуализатора
|
||||
├── open-editor.sh # Скрипт запуска редактора
|
||||
├── open-player.sh # Скрипт запуска визуализатора
|
||||
├── demo-with-start.json # Демо-карта 15×15
|
||||
├── demo-with-start-solution.json # Демо-решение
|
||||
└── README.md # Этот файл
|
||||
```
|
||||
|
||||
## 🎨 Технические детали
|
||||
|
||||
### Технологии
|
||||
- **HTML5** - структура страниц
|
||||
- **CSS3** - современный градиентный дизайн
|
||||
- **JavaScript (ES6+)** - вся логика на клиенте
|
||||
- **Canvas API** - отрисовка карт и визуализация
|
||||
|
||||
### Особенности
|
||||
- ✅ Работает без сервера (статические файлы)
|
||||
- ✅ Валидация данных при импорте
|
||||
- ✅ Адаптивная сетка интерфейса
|
||||
- ✅ Общий CSS и JS для уменьшения дублирования
|
||||
- ✅ Модульная архитектура (common.js для общего кода)
|
||||
- ✅ Навигация между страницами
|
||||
- ✅ Визуальная обратная связь (анимации, подсветка)
|
||||
|
||||
### Зависимости
|
||||
- Нет внешних зависимостей
|
||||
- Чистый Vanilla JavaScript
|
||||
- Работает в любом современном браузере
|
||||
|
||||
## 🧪 Быстрый тест
|
||||
|
||||
### Тест редактора
|
||||
1. Откройте `editor.html`
|
||||
2. Нарисуйте простую карту с точкой старта
|
||||
3. Экспортируйте в JSON
|
||||
4. Импортируйте обратно - данные должны сохраниться
|
||||
|
||||
### Тест визуализатора
|
||||
1. Откройте `player.html`
|
||||
2. Загрузите `demo-with-start.json`
|
||||
3. Загрузите `demo-with-start-solution.json`
|
||||
4. Нажмите ▶ Play и экспериментируйте с кнопками и скоростью
|
||||
|
||||
## 📚 Примеры использования
|
||||
|
||||
### Создание карты с чекпоинтами
|
||||
1. Создайте дорогу (тип 0) в виде трассы
|
||||
2. Добавьте препятствия (тип 1) по краям
|
||||
3. Разместите чекпоинты (тип 4) вдоль трассы
|
||||
4. Обозначьте старт (тип 5) в начале
|
||||
5. Экспортируйте и используйте в решателе
|
||||
|
||||
### Визуализация решения A*
|
||||
1. Запустите C# решатель, получите JSON с решением
|
||||
2. Откройте визуализатор
|
||||
3. Загрузите карту и решение
|
||||
4. Наблюдайте за оптимальным путем
|
||||
|
||||
## 🔗 Связанные проекты
|
||||
|
||||
- [Основной проект C#](../README.md) - решатель на основе A*
|
||||
- [Примеры карт](../maps/) - коллекция готовых карт
|
||||
- [Документация формата](../MAP-FORMAT.md) - подробное описание формата
|
||||
|
||||
## 🤝 Советы и трюки
|
||||
|
||||
### Редактор
|
||||
- Используйте Enter в полях размера для быстрого применения
|
||||
- Удерживайте мышь для быстрого рисования линий
|
||||
- JSON также выводится в консоль (F12) для быстрого копирования
|
||||
- При изменении размера существующие данные сохраняются
|
||||
|
||||
### Визуализатор
|
||||
- Используйте пошаговый режим для детального анализа
|
||||
- Скорость 1x подходит для медленного разбора
|
||||
- Скорость 10x для быстрого просмотра длинных решений
|
||||
- Можно загрузить новое решение без перезагрузки карты
|
||||
|
||||
## 🐛 Решение проблем
|
||||
|
||||
**Карта не загружается?**
|
||||
- Проверьте формат JSON
|
||||
- Убедитесь, что все значения от 0 до 5
|
||||
- Размеры должны быть от 5×5 до 100×100
|
||||
|
||||
**Решение не работает?**
|
||||
- Убедитесь, что на карте есть точка старта (5)
|
||||
- Проверьте формат: массив массивов [[ax, ay], ...]
|
||||
- Сначала загрузите карту, потом решение
|
||||
|
||||
**Визуализация странная?**
|
||||
- Возможно, решение выходит за границы карты
|
||||
- Проверьте корректность векторов ускорений
|
||||
|
||||
---
|
||||
|
||||
**Версия:** 2.0 (Объединенная)
|
||||
**Дата:** 2025
|
||||
**Лицензия:** MIT
|
||||
**Автор:** Racing Team
|
||||
|
||||
111
racing-tools/common.js
Normal file
111
racing-tools/common.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// Общие константы для редактора и плеера
|
||||
const CELL_SIZE = 30;
|
||||
const GRID_COLOR = '#dee2e6';
|
||||
const COLORS = {
|
||||
0: '#f8f9fa', // Дорога
|
||||
1: '#6c757d', // Камень
|
||||
2: '#e3f2fd', // Снег
|
||||
3: '#b3e5fc', // Лёд
|
||||
4: '#fff3cd', // Чекпоинт
|
||||
5: '#d4edda' // Старт
|
||||
};
|
||||
|
||||
// Общие функции для работы с canvas
|
||||
function drawGrid(ctx, width, height) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
function drawCells(ctx, map, width, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawMarkers(ctx, map, width, height) {
|
||||
// Рисуем маркеры для чекпоинтов и старта
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateMap(data) {
|
||||
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}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { map: newMap, width: newWidth, height: newHeight };
|
||||
}
|
||||
|
||||
function findStartPosition(map, width, height) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (map[y][x] === 5) {
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,28 +5,28 @@
|
||||
0
|
||||
],
|
||||
[
|
||||
-1,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
-1
|
||||
],
|
||||
[
|
||||
-2,
|
||||
-1
|
||||
],
|
||||
[
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
2
|
||||
],
|
||||
[
|
||||
-2,
|
||||
-2
|
||||
],
|
||||
[
|
||||
1,
|
||||
0
|
||||
],
|
||||
[
|
||||
-1,
|
||||
0
|
||||
],
|
||||
[
|
||||
1,
|
||||
0
|
||||
0,
|
||||
-1
|
||||
]
|
||||
]
|
||||
}
|
||||
21
racing-tools/demo-with-start.json
Normal file
21
racing-tools/demo-with-start.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"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]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
125
racing-tools/editor.html
Normal file
125
racing-tools/editor.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Редактор карт - Гонки на бумаге</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🏁 Редактор карт</h1>
|
||||
<p class="subtitle">Гонки на бумаге / Paper Racing</p>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="editor.html">🏁 Редактор карт</a>
|
||||
<a href="player.html">🎬 Визуализатор решений</a>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<h3>📐 Размеры карты</h3>
|
||||
<div class="size-inputs">
|
||||
<div class="input-wrapper">
|
||||
<label for="width">Ширина:</label>
|
||||
<input type="number" id="width" min="5" max="100" value="15">
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<label for="height">Высота:</label>
|
||||
<input type="number" id="height" min="5" max="100" value="15">
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button class="btn-primary" onclick="resizeMap()">Применить</button>
|
||||
<button class="btn-danger" onclick="clearMap()">Очистить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>🎨 Тип ячейки</h3>
|
||||
<div class="palette">
|
||||
<div class="cell-type road active" data-type="0" onclick="selectCellType(0)">
|
||||
<span>Дорога</span>
|
||||
<span class="code">(0)</span>
|
||||
</div>
|
||||
<div class="cell-type stone" data-type="1" onclick="selectCellType(1)">
|
||||
<span>Камень</span>
|
||||
<span class="code">(1)</span>
|
||||
</div>
|
||||
<div class="cell-type snow" data-type="2" onclick="selectCellType(2)">
|
||||
<span>Снег</span>
|
||||
<span class="code">(2)</span>
|
||||
</div>
|
||||
<div class="cell-type ice" data-type="3" onclick="selectCellType(3)">
|
||||
<span>Лёд</span>
|
||||
<span class="code">(3)</span>
|
||||
</div>
|
||||
<div class="cell-type checkpoint" data-type="4" onclick="selectCellType(4)">
|
||||
<span>Чекпоинт</span>
|
||||
<span class="code">(4)</span>
|
||||
</div>
|
||||
<div class="cell-type start" data-type="5" onclick="selectCellType(5)">
|
||||
<span>Старт</span>
|
||||
<span class="code">(5)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>💾 Импорт / Экспорт</h3>
|
||||
<div class="buttons">
|
||||
<button class="btn-success" onclick="exportMap()">📥 Экспорт JSON</button>
|
||||
<button class="btn-warning" onclick="document.getElementById('fileInput').click()">📤 Импорт JSON</button>
|
||||
</div>
|
||||
<input type="file" id="fileInput" accept=".json" onchange="importMap(event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="canvas-wrapper">
|
||||
<canvas id="mapCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<strong>💡 Подсказки:</strong>
|
||||
• Кликайте по ячейкам для изменения типа<br>
|
||||
• Удерживайте кнопку мыши для рисования<br>
|
||||
• Экспортируйте карту в JSON для использования в игре<br>
|
||||
• Для визуализации решений используйте <a href="player.html" style="color: #0d47a1; font-weight: bold;">Визуализатор решений</a>
|
||||
</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="common.js"></script>
|
||||
<script src="editor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
253
racing-tools/editor.js
Normal file
253
racing-tools/editor.js
Normal file
@@ -0,0 +1,253 @@
|
||||
// Состояние редактора
|
||||
let width = 15;
|
||||
let height = 15;
|
||||
let map = [];
|
||||
let selectedType = 0;
|
||||
let isDrawing = false;
|
||||
|
||||
let scale = 1;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let isPanning = false;
|
||||
let startPanX = 0;
|
||||
let startPanY = 0;
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
const wrapper = canvas.parentElement;
|
||||
canvas.width = Math.max(1000, wrapper.clientWidth || 1000);
|
||||
canvas.height = Math.max(1000, wrapper.clientHeight || 1000);
|
||||
}
|
||||
|
||||
// Отрисовка карты
|
||||
function drawMap() {
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.translate(offsetX, offsetY);
|
||||
ctx.scale(scale, scale);
|
||||
drawCells(ctx, map, width, height);
|
||||
drawGrid(ctx, width, height);
|
||||
drawMarkers(ctx, map, width, height);
|
||||
}
|
||||
|
||||
// Выбор типа ячейки
|
||||
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 canvasX = (event.clientX - rect.left - offsetX) / scale;
|
||||
const canvasY = (event.clientY - rect.top - offsetY) / scale;
|
||||
const x = Math.floor(canvasX / CELL_SIZE);
|
||||
const y = Math.floor(canvasY / 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) => {
|
||||
if (e.button === 2 || e.shiftKey) {
|
||||
isPanning = true;
|
||||
startPanX = e.clientX - offsetX;
|
||||
startPanY = e.clientY - offsetY;
|
||||
canvas.style.cursor = 'grabbing';
|
||||
} else {
|
||||
isDrawing = true;
|
||||
const coords = getCellCoords(e);
|
||||
if (coords) {
|
||||
setCellType(coords.x, coords.y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (isPanning) {
|
||||
offsetX = e.clientX - startPanX;
|
||||
offsetY = e.clientY - startPanY;
|
||||
drawMap();
|
||||
} else if (isDrawing) {
|
||||
const coords = getCellCoords(e);
|
||||
if (coords) {
|
||||
setCellType(coords.x, coords.y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
isDrawing = false;
|
||||
isPanning = false;
|
||||
canvas.style.cursor = 'default';
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
isDrawing = false;
|
||||
isPanning = false;
|
||||
canvas.style.cursor = 'default';
|
||||
});
|
||||
|
||||
canvas.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
const zoom = e.deltaY < 0 ? 1.1 : 0.9;
|
||||
const newScale = Math.min(Math.max(0.1, scale * zoom), 5);
|
||||
|
||||
offsetX = mouseX - (mouseX - offsetX) * (newScale / scale);
|
||||
offsetY = mouseY - (mouseY - offsetY) * (newScale / scale);
|
||||
scale = newScale;
|
||||
|
||||
drawMap();
|
||||
});
|
||||
|
||||
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
|
||||
// Изменение размера карты
|
||||
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);
|
||||
const validated = validateMap(data);
|
||||
|
||||
// Импортируем карту
|
||||
height = validated.height;
|
||||
width = validated.width;
|
||||
map = validated.map;
|
||||
|
||||
// Обновляем 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();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
resizeCanvas();
|
||||
drawMap();
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
43
racing-tools/open-editor.sh
Executable file
43
racing-tools/open-editor.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт для запуска редактора карт в браузере
|
||||
# Автоматически определяет доступный браузер и открывает страницу
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HTML_FILE="$SCRIPT_DIR/editor.html"
|
||||
|
||||
echo "🏁 Запуск редактора карт..."
|
||||
echo "📁 Путь: $HTML_FILE"
|
||||
|
||||
# Проверяем наличие файла
|
||||
if [ ! -f "$HTML_FILE" ]; then
|
||||
echo "❌ Ошибка: Файл $HTML_FILE не найден"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Функция для открытия в браузере
|
||||
open_browser() {
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open "$HTML_FILE"
|
||||
elif command -v firefox &> /dev/null; then
|
||||
firefox "$HTML_FILE"
|
||||
elif command -v google-chrome &> /dev/null; then
|
||||
google-chrome "$HTML_FILE"
|
||||
elif command -v chromium &> /dev/null; then
|
||||
chromium "$HTML_FILE"
|
||||
else
|
||||
echo "❌ Браузер не найден. Откройте вручную: $HTML_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Открываем браузер
|
||||
open_browser
|
||||
|
||||
echo "✅ Редактор открыт в браузере"
|
||||
echo ""
|
||||
echo "💡 Подсказки:"
|
||||
echo " • Нарисуйте карту, обязательно добавьте точку старта (тип 5)"
|
||||
echo " • Экспортируйте в JSON для использования в решателе"
|
||||
echo " • Для визуализации решений откройте ./open-player.sh"
|
||||
|
||||
44
racing-tools/open-player.sh
Executable file
44
racing-tools/open-player.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт для запуска визуализатора решений в браузере
|
||||
# Автоматически определяет доступный браузер и открывает страницу
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HTML_FILE="$SCRIPT_DIR/player.html"
|
||||
|
||||
echo "🎬 Запуск визуализатора решений..."
|
||||
echo "📁 Путь: $HTML_FILE"
|
||||
|
||||
# Проверяем наличие файла
|
||||
if [ ! -f "$HTML_FILE" ]; then
|
||||
echo "❌ Ошибка: Файл $HTML_FILE не найден"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Функция для открытия в браузере
|
||||
open_browser() {
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open "$HTML_FILE"
|
||||
elif command -v firefox &> /dev/null; then
|
||||
firefox "$HTML_FILE"
|
||||
elif command -v google-chrome &> /dev/null; then
|
||||
google-chrome "$HTML_FILE"
|
||||
elif command -v chromium &> /dev/null; then
|
||||
chromium "$HTML_FILE"
|
||||
else
|
||||
echo "❌ Браузер не найден. Откройте вручную: $HTML_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Открываем браузер
|
||||
open_browser
|
||||
|
||||
echo "✅ Визуализатор открыт в браузере"
|
||||
echo ""
|
||||
echo "💡 Подсказки:"
|
||||
echo " • Сначала загрузите карту (📂 Загрузить карту)"
|
||||
echo " • Затем загрузите решение (🎬 Загрузить решение)"
|
||||
echo " • Используйте кнопки управления для анимации"
|
||||
echo " • Демо-файлы: demo-with-start.json и demo-with-start-solution.json"
|
||||
|
||||
117
racing-tools/player.html
Normal file
117
racing-tools/player.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Визуализатор решений - Гонки на бумаге</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎬 Визуализатор решений</h1>
|
||||
<p class="subtitle">Гонки на бумаге / Paper Racing</p>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="editor.html">🏁 Редактор карт</a>
|
||||
<a href="player.html">🎬 Визуализатор решений</a>
|
||||
</div>
|
||||
|
||||
<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="common.js"></script>
|
||||
<script src="player.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
413
racing-tools/player.js
Normal file
413
racing-tools/player.js
Normal file
@@ -0,0 +1,413 @@
|
||||
// Состояние плеера
|
||||
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;
|
||||
|
||||
let scale = 1;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let isPanning = false;
|
||||
let startPanX = 0;
|
||||
let startPanY = 0;
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
const wrapper = canvas.parentElement;
|
||||
canvas.width = Math.max(1000, wrapper.clientWidth || 1000);
|
||||
canvas.height = Math.max(1000, wrapper.clientHeight || 1000);
|
||||
}
|
||||
|
||||
// Отрисовка карты
|
||||
function drawMap() {
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.translate(offsetX, offsetY);
|
||||
ctx.scale(scale, scale);
|
||||
drawCells(ctx, map, width, height);
|
||||
drawGrid(ctx, width, height);
|
||||
drawMarkers(ctx, map, width, height);
|
||||
|
||||
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 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);
|
||||
const validated = validateMap(data);
|
||||
|
||||
// Импортируем карту
|
||||
height = validated.height;
|
||||
width = validated.width;
|
||||
map = validated.map;
|
||||
|
||||
// Очищаем траекторию при загрузке новой карты
|
||||
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(map, width, height);
|
||||
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();
|
||||
}
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
if (e.button === 2 || e.shiftKey) {
|
||||
isPanning = true;
|
||||
startPanX = e.clientX - offsetX;
|
||||
startPanY = e.clientY - offsetY;
|
||||
canvas.style.cursor = 'grabbing';
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (isPanning) {
|
||||
offsetX = e.clientX - startPanX;
|
||||
offsetY = e.clientY - startPanY;
|
||||
drawMap();
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
isPanning = false;
|
||||
canvas.style.cursor = 'default';
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
isPanning = false;
|
||||
canvas.style.cursor = 'default';
|
||||
});
|
||||
|
||||
canvas.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
const zoom = e.deltaY < 0 ? 1.1 : 0.9;
|
||||
const newScale = Math.min(Math.max(0.1, scale * zoom), 5);
|
||||
|
||||
offsetX = mouseX - (mouseX - offsetX) * (newScale / scale);
|
||||
offsetY = mouseY - (mouseY - offsetY) * (newScale / scale);
|
||||
scale = newScale;
|
||||
|
||||
drawMap();
|
||||
});
|
||||
|
||||
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
resizeCanvas();
|
||||
drawMap();
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
366
racing-tools/styles.css
Normal file
366
racing-tools/styles.css
Normal file
@@ -0,0 +1,366 @@
|
||||
* {
|
||||
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: flex-start;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
margin: 0 15px;
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.size-inputs {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.input-wrapper label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.input-wrapper input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-wrapper input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.palette {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cell-type {
|
||||
padding: 15px;
|
||||
border: 3px solid #dee2e6;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cell-type:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.cell-type.active {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.cell-type .code {
|
||||
font-size: 0.8em;
|
||||
color: #6c757d;
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.road { background: #f8f9fa; color: #495057; }
|
||||
.stone { background: #6c757d; color: white; }
|
||||
.snow { background: #e3f2fd; color: #1976d2; }
|
||||
.ice { background: #b3e5fc; color: #0277bd; }
|
||||
.checkpoint { background: #fff3cd; color: #856404; border-color: #ffc107 !important; }
|
||||
.start { background: #d4edda; color: #155724; border-color: #28a745 !important; }
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-height: 1000px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 3px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
cursor: crosshair;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
#fileInput, #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;
|
||||
}
|
||||
|
||||
/* Стили для визуализатора */
|
||||
.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;
|
||||
}
|
||||
|
||||
116
sol-15x15.json
Normal file
116
sol-15x15.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"solution": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
2,
|
||||
-1
|
||||
],
|
||||
[
|
||||
2,
|
||||
1
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-2,
|
||||
0
|
||||
],
|
||||
[
|
||||
-1,
|
||||
0
|
||||
],
|
||||
[
|
||||
-1,
|
||||
1
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-2,
|
||||
-2
|
||||
],
|
||||
[
|
||||
1,
|
||||
-2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
-2,
|
||||
2
|
||||
],
|
||||
[
|
||||
-2,
|
||||
-1
|
||||
],
|
||||
[
|
||||
-1,
|
||||
0
|
||||
],
|
||||
[
|
||||
2,
|
||||
-2
|
||||
],
|
||||
[
|
||||
1,
|
||||
0
|
||||
],
|
||||
[
|
||||
2,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
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