Simulador_Maniobra-PER/simulador_per.html

459 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simulador de Maniobra y Ciaboga - PER</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0f172a;
color: #f8fafc;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 20px;
}
h1 { font-size: 1.5rem; margin-bottom: 5px; color: #38bdf8; text-align: center; }
p.sub { font-size: 0.9rem; color: #94a3b8; margin-bottom: 15px; text-align: center; }
#sim-container {
position: relative;
background: #0284c7;
border: 3px solid #0369a1;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
overflow: hidden;
}
canvas { display: block; background: #0284c7; }
.hud {
position: absolute;
top: 10px;
left: 10px;
background: rgba(15, 23, 42, 0.85);
padding: 12px;
border-radius: 8px;
font-size: 0.85rem;
border: 1px solid #334155;
min-width: 180px;
}
.hud div { margin-bottom: 4px; }
.hud span { font-weight: bold; color: #38bdf8; }
.controls-hint {
position: absolute;
bottom: 10px;
right: 10px;
background: rgba(15, 23, 42, 0.85);
padding: 8px 12px;
border-radius: 8px;
font-size: 0.8rem;
color: #94a3b8;
border: 1px solid #334155;
}
.controls-panel {
margin-top: 15px;
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
max-width: 600px;
}
.btn-group { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.btn-row { display: flex; gap: 5px; }
button {
background: #334155;
color: #fff;
border: 1px solid #475569;
padding: 10px 16px;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
user-select: none;
touch-action: manipulation;
}
button:active, button.active { background: #0284c7; border-color: #38bdf8; }
.btn-reset { background: #991b1b; border-color: #dc2626; margin-top: 5px; }
.btn-reset:active { background: #dc2626; }
.instrucciones {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #cbd5e1;
font-size: 8pt;
color: white;
text-align: center;
}
.footer {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #cbd5e1;
font-size: 8pt;
color: #64748b;
text-align: center;
}
</style>
</head>
<body>
<h1>Simulador de Maniobra PER (Ciaboga y Atraque)</h1>
<p class="sub">
Practica el comportamiento de la caña, motor y amarre en pantalán según el PER
</p>
<p>
<b>Barco de una sóla hélice dextrógira<b>
<br>
&nbsp;
</p>
<div id="sim-container">
<canvas id="canvas" width="600" height="450"></canvas>
<div class="hud">
<div>Motor: <span id="hud-engine">PARADO</span></div>
<div>Timón: <span id="hud-rudder">A LA VÍA (0°)</span></div>
<div>Velocidad: <span id="hud-speed">0.0 nds</span></div>
<div>Rumbo: <span id="hud-heading">000°</span></div>
</div>
<div class="controls-hint">
Usa las <b>Flechas del Teclado</b>
</div>
</div>
<div class="controls-panel">
<div class="btn-group">
<button id="btn-up">▲ AVANTE</button>
<div class="btn-row">
<button id="btn-left">◄ BABOR</button>
<button id="btn-down">▼ ATRÁS</button>
<button id="btn-right">ESTRIBOR ►</button>
</div>
</div>
<button id="btn-reset" class="btn-reset">Reiniciar Posición</button>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Estado del barco
let boat = {
x: canvas.width / 2,
y: canvas.height / 2 - 50,
angle: -Math.PI / 2, // Apuntando hacia arriba
speed: 0,
rudderAngle: 0, // -30 (babor) a +30 (estribor)
engine: 'STOP' // 'FORWARD', 'REVERSE', 'STOP'
};
// Definición del Pantalán y Barcos Amarrados
const dock = {
x: 20,
y: 380,
width: 560,
height: 16
};
// Barcos amarrados en punta al pantalán (perpendiculares, apuntando hacia el norte)
// Dejamos un hueco en x = 270-280 para realizar la maniobra de atraque.
const dockedBoats = [
{ x: 60, y: dock.y - 22, angle: -Math.PI / 2 },
{ x: 130, y: dock.y - 22, angle: -Math.PI / 2 },
{ x: 200, y: dock.y - 22, angle: -Math.PI / 2 },
// Hueco libre para amarre
{ x: 350, y: dock.y - 22, angle: -Math.PI / 2 },
{ x: 420, y: dock.y - 22, angle: -Math.PI / 2 },
{ x: 490, y: dock.y - 22, angle: -Math.PI / 2 }
];
// Teclas presionadas
const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false };
// Eventos de teclado
window.addEventListener('keydown', (e) => {
if (keys.hasOwnProperty(e.key)) {
e.preventDefault();
keys[e.key] = true;
}
});
window.addEventListener('keyup', (e) => {
if (keys.hasOwnProperty(e.key)) {
e.preventDefault();
keys[e.key] = false;
}
});
// Eventos de botones táctiles / clic
const bindBtn = (id, keyName) => {
const btn = document.getElementById(id);
btn.addEventListener('mousedown', () => keys[keyName] = true);
btn.addEventListener('mouseup', () => keys[keyName] = false);
btn.addEventListener('touchstart', (e) => { e.preventDefault(); keys[keyName] = true; });
btn.addEventListener('touchend', (e) => { e.preventDefault(); keys[keyName] = false; });
};
bindBtn('btn-up', 'ArrowUp');
bindBtn('btn-down', 'ArrowDown');
bindBtn('btn-left', 'ArrowLeft');
bindBtn('btn-right', 'ArrowRight');
document.getElementById('btn-reset').addEventListener('click', () => {
boat.x = canvas.width / 2;
boat.y = canvas.height / 2 - 50;
boat.angle = -Math.PI / 2;
boat.speed = 0;
boat.rudderAngle = 0;
boat.engine = 'STOP';
});
function updatePhysics() {
// 1. Manejo del Timón
if (keys.ArrowLeft) {
boat.rudderAngle = Math.max(-30, boat.rudderAngle - 2);
} else if (keys.ArrowRight) {
boat.rudderAngle = Math.min(30, boat.rudderAngle + 2);
} else {
// Retorno gradual a la vía
if (boat.rudderAngle > 0) boat.rudderAngle = Math.max(0, boat.rudderAngle - 1.5);
if (boat.rudderAngle < 0) boat.rudderAngle = Math.min(0, boat.rudderAngle + 1.5);
}
// 2. Control del Motor
if (keys.ArrowUp) {
boat.engine = 'FORWARD';
boat.speed += 0.04; // Aceleración avante
} else if (keys.ArrowDown) {
boat.engine = 'REVERSE';
boat.speed -= 0.03; // Aceleración atrás
} else {
boat.engine = 'STOP';
}
// Rozamiento del agua
boat.speed *= 0.98;
// Cálculo de velocidad en nudos para el modelo físico
const speedKnots = Math.abs(boat.speed) * 3;
// 3. Efectividad del giro y efecto de HÉLICE DEXTRÓGIRA
let turnFactor = 0;
const radRudder = (boat.rudderAngle * Math.PI) / 180;
if (boat.engine === 'FORWARD') {
const propEffect = speedKnots < 1.0 ? -0.002 * (1.0 - speedKnots) : 0;
turnFactor = radRudder * 0.04 + (boat.speed * radRudder * 0.01) + propEffect;
} else if (boat.engine === 'REVERSE') {
const propWalkAtras = speedKnots < 1.0 ? 0.025 * (1.0 - speedKnots) : 0;
const rudderEffectAtras = boat.speed * radRudder * 0.015;
turnFactor = propWalkAtras + rudderEffectAtras;
} else {
turnFactor = boat.speed * radRudder * 0.02;
}
boat.angle += turnFactor;
// Movimiento de la posición
boat.x += Math.cos(boat.angle) * boat.speed;
boat.y += Math.sin(boat.angle) * boat.speed;
// Límites de pantalla (bucle)
if (boat.x < 0) boat.x = canvas.width;
if (boat.x > canvas.width) boat.x = 0;
if (boat.y < 0) boat.y = canvas.height;
if (boat.y > canvas.height) boat.y = 0;
// Actualizar HUD
document.getElementById('hud-engine').innerText =
boat.engine === 'FORWARD' ? 'AVANTE' : boat.engine === 'REVERSE' ? 'ATRÁS (Dextrógira)' : 'PARADO';
let rText = 'A LA VÍA (0°)';
if (boat.rudderAngle < -2) rText = `BABOR (${Math.abs(Math.round(boat.rudderAngle))}°)`;
if (boat.rudderAngle > 2) rText = `ESTRIBOR (${Math.round(boat.rudderAngle)}°)`;
document.getElementById('hud-rudder').innerText = rText;
document.getElementById('hud-speed').innerText = speedKnots.toFixed(1) + ' nds';
let deg = Math.round((boat.angle * 180 / Math.PI + 90 + 360) % 360);
document.getElementById('hud-heading').innerText = String(deg).padStart(3, '0') + '°';
}
function drawGrid() {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
}
function drawDock() {
// Estructura principal del pantalán
ctx.fillStyle = '#78350f'; // Madera
ctx.strokeStyle = '#451a03';
ctx.lineWidth = 2;
ctx.fillRect(dock.x, dock.y, dock.width, dock.height);
ctx.strokeRect(dock.x, dock.y, dock.width, dock.height);
// Tablas de madera
ctx.strokeStyle = '#92400e';
ctx.lineWidth = 1;
for (let x = dock.x + 10; x < dock.x + dock.width; x += 12) {
ctx.beginPath();
ctx.moveTo(x, dock.y);
ctx.lineTo(x, dock.y + dock.height);
ctx.stroke();
}
// Norays del pantalán
ctx.fillStyle = '#cbd5e1';
for (let x = dock.x + 15; x < dock.x + dock.width; x += 35) {
ctx.beginPath();
ctx.arc(x, dock.y + 3, 2.5, 0, Math.PI * 2);
ctx.fill();
}
}
// Dibujar barco estático (gris) amarrado al pantalán
function drawStaticBoat(b) {
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.angle);
// Casco del barco estático
ctx.fillStyle = '#64748b'; // Gris
ctx.strokeStyle = '#334155';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(20, 0); // Proa
ctx.lineTo(-15, -12); // Aleta Babor
ctx.lineTo(-15, 12); // Aleta Estribor
ctx.closePath();
ctx.fill();
ctx.stroke();
// Cubierta
ctx.fillStyle = '#94a3b8';
ctx.fillRect(-8, -6, 12, 12);
ctx.restore();
// Amarras al pantalán
ctx.strokeStyle = '#f59e0b';
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
ctx.beginPath();
ctx.moveTo(b.x - 10, b.y + 12);
ctx.lineTo(b.x - 10, dock.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(b.x + 10, b.y + 12);
ctx.lineTo(b.x + 10, dock.y);
ctx.stroke();
ctx.setLineDash([]);
}
function drawBoat() {
ctx.save();
ctx.translate(boat.x, boat.y);
ctx.rotate(boat.angle);
// Estela de la hélice
if (boat.engine === 'FORWARD') {
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.beginPath();
ctx.arc(-22, 0, 6, 0, Math.PI * 2);
ctx.fill();
} else if (boat.engine === 'REVERSE') {
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.beginPath();
ctx.arc(22, 0, 6, 0, Math.PI * 2);
ctx.fill();
}
// Casco del barco
ctx.fillStyle = '#f1f5f9';
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(20, 0); // Proa
ctx.lineTo(-15, -12); // Aleta Babor
ctx.lineTo(-15, 12); // Aleta Estribor
ctx.closePath();
ctx.fill();
ctx.stroke();
// Luces de navegación
ctx.fillStyle = '#ef4444'; // Babor
ctx.beginPath(); ctx.arc(5, -9, 2.5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#22c55e'; // Estribor
ctx.beginPath(); ctx.arc(5, 9, 2.5, 0, Math.PI * 2); ctx.fill();
// Pala del Timón
ctx.save();
ctx.translate(-15, 0);
ctx.rotate((boat.rudderAngle * Math.PI) / 180);
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-10, 0);
ctx.stroke();
ctx.restore();
ctx.restore();
}
function loop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid();
// Dibujar Pantalán y Barcos Amarrados
drawDock();
dockedBoats.forEach(drawStaticBoat);
updatePhysics();
drawBoat();
requestAnimationFrame(loop);
}
loop();
</script>
<div class="instruciones">
<p><br><br>Código fuente completo en un único archivo <strong>HTML5</strong> interactivo. Incluye los gráficos en Canvas.<br>La física de navegación (corriente de expulsión, efecto de avante/atrás e inercia) y la interfaz de mandos.</p>
<p><br></p>
<strong>Instrucciones para guardarlo y usarlo offline:</strong>
<ul>
<li>Pulsa botón derecho y escoge <i>Guardar como...</i></li>
<li>Guárdalo en tu ordenador en un archivo de texto con el nombre <code>simulador_per.html</code>.</li>
<li>En tu equipo simplemente doble clic sobre el archivo para abrirlo en cualquier navegador.</li>
<li>Usa las <strong>flechas del teclado</strong> (Arriba: Avante | Abajo: Atrás | Izquierda/Derecha: Timón) o los botones en pantalla.</li>
</ul>
</div>
<div class="footer">
Simulador didáctico para la preparación del título PER (Patrón de Embarcaciones Recreativas).<br>(c) SoloConLinux - Luis Gutiérrez López
</div>
</body>
</html>