split editor
This commit is contained in:
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();
|
||||
|
||||
Reference in New Issue
Block a user