act piscina.js
This commit is contained in:
parent
c9afa55243
commit
df417b036a
public/js
|
@ -1,104 +1,263 @@
|
||||||
const canvas = document.getElementById("poolCanvas");
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const ctx = canvas.getContext("2d");
|
const rutinaId = new URLSearchParams(window.location.search).get('routineId');
|
||||||
|
if (!rutinaId) return alert('No se proporcionó ID de rutina.');
|
||||||
|
|
||||||
let currentAtleta = null;
|
const select = document.getElementById('selectAtleta');
|
||||||
let rutinaId = new URLSearchParams(window.location.search).get("routineId");
|
const tituloRutina = document.getElementById('tituloRutina');
|
||||||
let atletas = [];
|
const tipoRutina = document.getElementById('tipoRutina');
|
||||||
let colocados = [];
|
const modalidadRutina = document.getElementById('modalidadRutina');
|
||||||
|
|
||||||
function drawPool() {
|
const rutina = await fetch(`/routines/${rutinaId}`).then(res => res.json());
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
const modalidad = rutina.modalidad;
|
||||||
ctx.fillStyle = "#cceeff";
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
ctx.beginPath();
|
tituloRutina.textContent = rutina.title || rutina.nombreCompetencia;
|
||||||
ctx.moveTo(canvas.width / 2, 0);
|
tipoRutina.textContent = rutina.tipoCompetencia;
|
||||||
ctx.lineTo(canvas.width / 2, canvas.height);
|
modalidadRutina.textContent = modalidad;
|
||||||
ctx.strokeStyle = "#555";
|
|
||||||
ctx.setLineDash([6, 4]);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
|
|
||||||
colocados.forEach(({ x, y, atleta }) => drawAthlete(x, y, atleta));
|
const atletas = await fetch('/users/athletes').then(res => res.json());
|
||||||
}
|
atletas.forEach(a => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = a._id;
|
||||||
|
opt.textContent = a.name;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
function drawAthlete(x, y, atleta) {
|
const stage = new Konva.Stage({
|
||||||
ctx.beginPath();
|
container: 'piscina',
|
||||||
ctx.arc(x, y, 14, 0, Math.PI * 2);
|
width: 900,
|
||||||
ctx.fillStyle = "#ff4444";
|
height: 400
|
||||||
ctx.fill();
|
});
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.strokeStyle = "#fff";
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
ctx.fillStyle = "#fff";
|
const layer = new Konva.Layer();
|
||||||
ctx.font = "bold 12px sans-serif";
|
stage.add(layer);
|
||||||
ctx.textAlign = "center";
|
|
||||||
ctx.textBaseline = "middle";
|
|
||||||
ctx.fillText(atleta.idPersonalizado, x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.addEventListener("click", (e) => {
|
let formacionActual = [];
|
||||||
if (!currentAtleta) return;
|
let currentAtleta = null;
|
||||||
|
const maxAtletas = modalidad === 'solo' ? 1 : modalidad === 'duo' ? 2 : 8;
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
document.getElementById('btnAgregarAtleta').addEventListener('click', () => {
|
||||||
const x = e.clientX - rect.left;
|
if (formacionActual.length >= maxAtletas) {
|
||||||
const y = e.clientY - rect.top;
|
alert(`Modalidad "${modalidad}" permite un máximo de ${maxAtletas} atletas.`);
|
||||||
|
|
||||||
if (x < 0 || y < 0 || x > canvas.width || y > canvas.height) return;
|
|
||||||
|
|
||||||
if (colocados.find(a => a.atleta.idPersonalizado === currentAtleta.idPersonalizado)) {
|
|
||||||
alert("Este atleta ya fue colocado.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
colocados.push({ x, y, atleta: currentAtleta });
|
|
||||||
updateLegend(currentAtleta);
|
|
||||||
drawPool();
|
|
||||||
currentAtleta = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateLegend(atleta) {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.textContent = `${atleta.idPersonalizado}: ${atleta.atletaId?.name || 'Desconocido'} (${atleta.rol})`;
|
|
||||||
document.getElementById("legend").appendChild(li);
|
|
||||||
}
|
|
||||||
|
|
||||||
function enablePlacement() {
|
|
||||||
const index = document.getElementById("athleteSelect").value;
|
|
||||||
if (index >= 0) {
|
|
||||||
currentAtleta = atletas[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRoutine() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/routines/${rutinaId}`);
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
document.getElementById("routineTitle").textContent = data.nombreCompetencia;
|
|
||||||
document.getElementById("routineType").textContent = data.tipoCompetencia;
|
|
||||||
document.getElementById("routineMode").textContent = data.modalidad;
|
|
||||||
|
|
||||||
atletas = data.participantes;
|
|
||||||
const select = document.getElementById("athleteSelect");
|
|
||||||
|
|
||||||
if (!atletas || atletas.length === 0) {
|
|
||||||
select.innerHTML = `<option disabled>No hay atletas</option>`;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
atletas.forEach((a, i) => {
|
const idAtleta = select.value;
|
||||||
const opt = document.createElement("option");
|
const rol = document.getElementById('rolAtleta').value.trim().toLowerCase();
|
||||||
opt.value = i;
|
const idPersonalizado = document.getElementById('idPersonalizado').value.trim();
|
||||||
opt.textContent = `${a.idPersonalizado} - ${a.atletaId?.name || 'Desconocido'} (${a.rol})`;
|
|
||||||
select.appendChild(opt);
|
if (!idAtleta || !rol || !idPersonalizado) {
|
||||||
|
alert('Faltan datos del atleta');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAtleta = {
|
||||||
|
atletaId: idAtleta,
|
||||||
|
rol,
|
||||||
|
idPersonalizado
|
||||||
|
};
|
||||||
|
|
||||||
|
alert('Haz clic en la piscina para colocarlo');
|
||||||
|
});
|
||||||
|
|
||||||
|
stage.on('click', (e) => {
|
||||||
|
if (!currentAtleta) return;
|
||||||
|
|
||||||
|
const pos = stage.getPointerPosition();
|
||||||
|
|
||||||
|
let fillColor = 'gray';
|
||||||
|
if (currentAtleta.rol === 'volador') fillColor = 'purple';
|
||||||
|
else if (currentAtleta.rol === 'pilar') fillColor = 'blue';
|
||||||
|
else fillColor = 'red';
|
||||||
|
|
||||||
|
const grupo = prompt("Grupo del atleta (A, B o vacío)", "");
|
||||||
|
let strokeColor = 'white';
|
||||||
|
if (grupo.toUpperCase() === 'A') strokeColor = 'green';
|
||||||
|
else if (grupo.toUpperCase() === 'B') strokeColor = 'yellow';
|
||||||
|
|
||||||
|
const circle = new Konva.Circle({
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y,
|
||||||
|
radius: 15,
|
||||||
|
fill: fillColor,
|
||||||
|
stroke: strokeColor,
|
||||||
|
strokeWidth: 3,
|
||||||
|
draggable: true
|
||||||
});
|
});
|
||||||
|
|
||||||
drawPool();
|
const text = new Konva.Text({
|
||||||
} catch (err) {
|
x: pos.x - 10,
|
||||||
console.error("Error cargando rutina:", err);
|
y: pos.y - 7,
|
||||||
}
|
text: currentAtleta.idPersonalizado,
|
||||||
}
|
fontSize: 12,
|
||||||
|
fill: 'white'
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", loadRoutine);
|
layer.add(circle);
|
||||||
|
layer.add(text);
|
||||||
|
layer.draw();
|
||||||
|
|
||||||
|
const atletaObj = {
|
||||||
|
...currentAtleta,
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y,
|
||||||
|
grupo: grupo || '',
|
||||||
|
direccion: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
formacionActual.push(atletaObj);
|
||||||
|
|
||||||
|
circle.on('click', (evt) => {
|
||||||
|
if (evt.evt.ctrlKey) {
|
||||||
|
const pointer = stage.getPointerPosition();
|
||||||
|
const dx = pointer.x - circle.x();
|
||||||
|
const dy = pointer.y - circle.y();
|
||||||
|
const angle = Math.atan2(dy, dx);
|
||||||
|
const length = 40;
|
||||||
|
const toX = circle.x() + length * Math.cos(angle);
|
||||||
|
const toY = circle.y() + length * Math.sin(angle);
|
||||||
|
|
||||||
|
const line = new Konva.Line({
|
||||||
|
points: [circle.x(), circle.y(), toX, toY],
|
||||||
|
stroke: 'black',
|
||||||
|
strokeWidth: 2,
|
||||||
|
lineCap: 'round',
|
||||||
|
lineJoin: 'round'
|
||||||
|
});
|
||||||
|
|
||||||
|
layer.add(line);
|
||||||
|
layer.draw();
|
||||||
|
|
||||||
|
atletaObj.direccion = angle.toFixed(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
currentAtleta = null;
|
||||||
|
document.getElementById('rolAtleta').value = '';
|
||||||
|
document.getElementById('idPersonalizado').value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnGuardarFormacion').addEventListener('click', async () => {
|
||||||
|
const nombre = document.getElementById('nombreFormacion').value.trim();
|
||||||
|
const notas = document.getElementById('notasFormacion').value.trim();
|
||||||
|
|
||||||
|
if (!nombre || formacionActual.length === 0) {
|
||||||
|
alert('Agrega nombre de formación y al menos un atleta');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
nombreColoquial: nombre,
|
||||||
|
notasTacticas: notas,
|
||||||
|
atletas: formacionActual
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`/routines/${rutinaId}/formations`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
alert('✅ Formación guardada correctamente');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('❌ Error al guardar formación');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔽 Botón exportar imagen
|
||||||
|
const exportBtn = document.createElement('button');
|
||||||
|
exportBtn.textContent = 'Exportar formación como imagen';
|
||||||
|
exportBtn.className = 'btn btn-outline-secondary w-100 mb-3';
|
||||||
|
exportBtn.onclick = () => {
|
||||||
|
const dataURL = stage.toDataURL({ pixelRatio: 3 });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = `formacion_${Date.now()}.png`;
|
||||||
|
link.href = dataURL;
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
document.querySelector('#piscinaContainer').appendChild(exportBtn);
|
||||||
|
|
||||||
|
// 🔁 HISTORIAL
|
||||||
|
const formaciones = await fetch(`/routines/${rutinaId}/formations`).then(r => r.json());
|
||||||
|
const historial = document.getElementById('formacionesPrevias');
|
||||||
|
|
||||||
|
if (Array.isArray(formaciones)) {
|
||||||
|
formaciones.forEach((f, i) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'col-md-6 mb-2';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="p-2 border rounded shadow-sm bg-light">
|
||||||
|
<strong>${i + 1}. ${f.nombreColoquial}</strong>
|
||||||
|
<br />
|
||||||
|
<small>${f.notasTacticas}</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
historial.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ▶ Ver animación
|
||||||
|
const btnVerAnimacion = document.createElement('button');
|
||||||
|
btnVerAnimacion.textContent = '▶ Ver animación';
|
||||||
|
btnVerAnimacion.className = 'btn btn-primary w-100 mt-3';
|
||||||
|
btnVerAnimacion.onclick = async () => {
|
||||||
|
if (!formaciones || formaciones.length === 0) return alert('No hay formaciones.');
|
||||||
|
|
||||||
|
for (let i = 0; i < formaciones.length; i++) {
|
||||||
|
const formacion = formaciones[i];
|
||||||
|
layer.destroyChildren();
|
||||||
|
|
||||||
|
formacion.atletas.forEach((a) => {
|
||||||
|
let color = 'gray';
|
||||||
|
if (a.rol === 'volador') color = 'purple';
|
||||||
|
else if (a.rol === 'pilar') color = 'blue';
|
||||||
|
else color = 'red';
|
||||||
|
|
||||||
|
let stroke = 'white';
|
||||||
|
if (a.grupo === 'A') stroke = 'green';
|
||||||
|
else if (a.grupo === 'B') stroke = 'yellow';
|
||||||
|
|
||||||
|
const circle = new Konva.Circle({
|
||||||
|
x: a.x,
|
||||||
|
y: a.y,
|
||||||
|
radius: 15,
|
||||||
|
fill: color,
|
||||||
|
stroke: stroke,
|
||||||
|
strokeWidth: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = new Konva.Text({
|
||||||
|
x: a.x - 10,
|
||||||
|
y: a.y - 7,
|
||||||
|
text: a.idPersonalizado,
|
||||||
|
fontSize: 12,
|
||||||
|
fill: 'white'
|
||||||
|
});
|
||||||
|
|
||||||
|
layer.add(circle);
|
||||||
|
layer.add(text);
|
||||||
|
|
||||||
|
if (a.direccion) {
|
||||||
|
const angle = parseFloat(a.direccion);
|
||||||
|
const len = 40;
|
||||||
|
const toX = a.x + len * Math.cos(angle);
|
||||||
|
const toY = a.y + len * Math.sin(angle);
|
||||||
|
|
||||||
|
const line = new Konva.Line({
|
||||||
|
points: [a.x, a.y, toX, toY],
|
||||||
|
stroke: 'black',
|
||||||
|
strokeWidth: 2,
|
||||||
|
lineCap: 'round'
|
||||||
|
});
|
||||||
|
|
||||||
|
layer.add(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
layer.draw();
|
||||||
|
await new Promise(res => setTimeout(res, 2000));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.querySelector('#historial').appendChild(btnVerAnimacion);
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue