Version correcta + Carta nautica L105 (A3)

This commit is contained in:
luisgulo committed 2026-11-01 02:58:37 +01:00
1 parent 093a7d0d65
commit 23dc59db62
2 files changed
+694

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

+694
View File
@@ -0,0 +1,694 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Regla Portland Plotter - Visor Pro Avanzado</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
input[type=range]::-webkit-slider-thumb {
background: #f59e0b;
border: 2px solid #0f172a;
height: 18px;
width: 18px;
border-radius: 50%;
cursor: pointer;
}
.cursor-grab-custom { cursor: grab; }
.cursor-grab-custom:active { cursor: grabbing; }
.cursor-crosshair-custom { cursor: crosshair; }
</style>
</head>
<body class="bg-slate-950 text-slate-100 h-screen w-screen flex flex-col font-sans select-none overflow-hidden m-0 p-0">
<!-- Header -->
<header class="bg-slate-900 border-b border-slate-800 p-2.5 shadow-xl z-20 flex-shrink-0">
<div class="max-w-full px-4 mx-auto flex flex-col lg:flex-row justify-between items-center gap-2">
<div class="flex items-center gap-3">
<div class="bg-amber-500/10 p-1.5 rounded-xl border border-amber-500/30">
<i class="fa-solid fa-compass text-amber-400 text-lg"></i>
</div>
<div>
<h1 class="text-sm font-bold text-white tracking-wide flex items-center gap-2">
Regla Portland Plotter
<span class="text-[9px] bg-amber-500/20 text-amber-300 border border-amber-500/30 px-2 py-0.5 rounded-full font-mono">Modo Rutas Múltiples & Guía</span>
</h1>
<p class="text-[10px] text-slate-400">Simulador Profesional de Alta Fidelidad</p>
</div>
</div>
<!-- Panel de Datos y Acciones -->
<div class="flex flex-wrap items-center gap-3">
<div class="flex items-center gap-3 bg-slate-950 px-3 py-1 rounded-lg border border-slate-800 font-mono text-xs">
<div><span class="text-slate-500">Rv:</span> <span id="readout-rv" class="text-amber-400 font-bold">090°</span></div>
<div class="h-3.5 w-[1px] bg-slate-800"></div>
<div><span class="text-slate-500">Ct:</span> <span id="readout-err" class="text-cyan-400 font-bold">0° E</span></div>
<div class="h-3.5 w-[1px] bg-slate-800"></div>
<div><span class="text-slate-500">Rm/Ra:</span> <span id="readout-rm" class="text-emerald-400 font-bold">090°</span></div>
</div>
<!-- Botón Ayuda -->
<button onclick="toggleHelpModal(true)" class="bg-indigo-600 hover:bg-indigo-500 text-white font-bold px-3 py-1 rounded-lg text-xs transition-all flex items-center gap-1.5 shadow-md" title="Guía de uso de la regla Portland">
<i class="fa-solid fa-circle-question"></i> Ayuda
</button>
<!-- Botón Centrar Regla -->
<button onclick="centerRulerToView()" class="bg-sky-600 hover:bg-sky-500 text-white font-bold px-3 py-1 rounded-lg text-xs transition-all flex items-center gap-1.5 shadow-md" title="Traer la regla a tu vista actual sin mover el mapa">
<i class="fa-solid fa-crosshairs"></i> Centrar Regla
</button>
<!-- Botón Trazar Puntos Múltiples -->
<button id="btn-mode-ab" onclick="toggleRouteMode()" class="bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold px-3.5 py-1 rounded-lg text-xs transition-all flex items-center gap-2 shadow-md">
<i class="fa-solid fa-pencil"></i> <span id="btn-ab-text">Añadir Puntos Ruta</span>
</button>
<!-- Botón Borrar Puntos -->
<button onclick="clearAllRoutes()" class="bg-rose-600 hover:bg-rose-500 text-white font-bold px-3 py-1 rounded-lg text-xs transition-all flex items-center gap-1.5 shadow-md" title="Eliminar todos los puntos y líneas">
<i class="fa-solid fa-trash"></i> Borrar Todo
</button>
</div>
</div>
</header>
<!-- Contenido Principal -->
<main class="flex-grow flex flex-col lg:flex-row w-full p-2 gap-2 overflow-hidden" style="height: calc(100vh - 58px);">
<!-- PANEL DE CONTROLES (Izquierda) -->
<div class="lg:w-72 bg-slate-900 border border-slate-800 rounded-xl p-3 shadow-2xl flex flex-col gap-3 overflow-y-auto flex-shrink-0 text-xs">
<div class="border-b border-slate-800 pb-1.5">
<h2 class="text-[11px] font-bold text-amber-400 uppercase tracking-wider flex items-center gap-2">
<i class="fa-solid fa-map"></i> Carta y Zoom
</h2>
</div>
<!-- Selector de archivo local -->
<div class="bg-slate-950 p-2.5 rounded-lg border border-slate-800 space-y-2">
<label class="block text-slate-300 font-semibold">Cargar Imagen de Carta:</label>
<input type="file" id="chart-file-input" accept="image/*" class="w-full text-[10px] text-slate-400 file:mr-2 file:py-1 file:px-2 file:rounded-md file:border-0 file:text-[10px] file:font-semibold file:bg-amber-500 file:text-slate-950 hover:file:bg-amber-400 cursor-pointer">
<button onclick="resetChartImage()" class="w-full bg-slate-800 hover:bg-slate-700 text-slate-300 py-1 rounded text-[10px] transition-all">Restaurar Carta por Defecto</button>
</div>
<!-- Control de Zoom -->
<div class="bg-slate-950 p-2.5 rounded-lg border border-slate-800 space-y-2">
<div class="flex justify-between items-center">
<label class="text-slate-300 font-semibold">Zoom de Carta</label>
<span id="zoom-val" class="font-mono font-bold text-amber-400">100%</span>
</div>
<div class="flex items-center gap-2">
<button onclick="adjustZoom(-0.1)" class="bg-slate-800 hover:bg-slate-700 text-white px-2.5 py-1 rounded font-bold text-sm">-</button>
<input type="range" id="zoom-slider" min="0.5" max="3.0" step="0.05" value="1.0" class="w-full accent-amber-500">
<button onclick="adjustZoom(0.1)" class="bg-slate-800 hover:bg-slate-700 text-white px-2.5 py-1 rounded font-bold text-sm">+</button>
</div>
</div>
<div class="border-b border-slate-800 pb-1.5 pt-1">
<h2 class="text-[11px] font-bold text-amber-400 uppercase tracking-wider flex items-center gap-2">
<i class="fa-solid fa-sliders"></i> Ajustes del Plotter
</h2>
</div>
<!-- Control de Giro del Disco -->
<div class="bg-slate-950 p-2.5 rounded-lg border border-slate-800 space-y-1.5">
<div class="flex justify-between items-center">
<label class="text-slate-300 font-semibold">Giro Rosa (Brújula N)</label>
<span id="disk-angle-val" class="font-mono font-bold text-amber-400">000°</span>
</div>
<input type="range" id="disk-angle-slider" min="0" max="360" value="0" class="w-full">
</div>
<!-- Control de Ángulo de la Regla -->
<div class="bg-slate-950 p-2.5 rounded-lg border border-slate-800 space-y-1.5">
<div class="flex justify-between items-center">
<label class="text-slate-300 font-semibold">Orientación Regla</label>
<span id="ruler-angle-val" class="font-mono font-bold text-slate-300">000°</span>
</div>
<input type="range" id="ruler-angle-slider" min="0" max="360" value="0" class="w-full">
</div>
<!-- Control de Error Total -->
<div class="bg-slate-950 p-2.5 rounded-lg border border-slate-800 space-y-1.5">
<div class="flex justify-between items-center">
<label class="text-slate-300 font-semibold">Ct = (±dm) + (±Δ)</label>
<span id="error-val" class="font-mono font-bold text-cyan-400">0° E</span>
</div>
<input type="range" id="error-slider" min="-30" max="30" value="0" class="w-full">
</div>
</div>
<!-- ÁREA DEL CANVAS CON SCROLL (Derecha) -->
<div id="canvas-container" class="flex-grow bg-slate-900 border border-slate-800 rounded-xl shadow-2xl relative overflow-auto h-full flex">
<canvas id="plotterCanvas" class="cursor-grab-custom block m-auto"></canvas>
<!-- Indicador de estado flotante -->
<div id="status-badge" class="absolute top-3 left-3 bg-slate-950/90 border border-amber-500/50 text-amber-300 text-xs px-3 py-1.5 rounded-lg shadow-lg pointer-events-none hidden z-10">
<i class="fa-solid fa-crosshairs mr-1.5 animate-pulse"></i> <span id="status-text">Haz clic para añadir puntos en la carta (A -> B -> C...)</span>
</div>
<div class="absolute bottom-3 right-3 bg-slate-950/80 border border-slate-800 text-[9.5px] text-slate-400 px-2.5 py-1 rounded-md backdrop-blur-sm pointer-events-none z-10">
Usa el zoom o las barras de desplazamiento para explorar la carta
</div>
</div>
</main>
<!-- MODAL DE AYUDA / INSTRUCCIONES -->
<div id="help-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden">
<div class="bg-slate-900 border border-slate-800 rounded-2xl max-w-lg w-full p-6 shadow-2xl space-y-4 text-slate-300 text-xs">
<div class="flex justify-between items-center border-b border-slate-800 pb-3">
<h3 class="text-sm font-bold text-amber-400 flex items-center gap-2">
<i class="fa-solid fa-book-open"></i> Guía de Uso: Regla Portland Plotter
</h3>
<button onclick="toggleHelpModal(false)" class="text-slate-400 hover:text-white text-base">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="space-y-2.5 leading-relaxed">
<p><strong>1. Medir o Trazar un Rumbo Verdadero (Rv):</strong></p>
<ul class="list-disc list-inside space-y-1 text-slate-400 pl-2">
<li>Coloca el borde recto de la regla uniendo tu origen y tu destino. Asegúrate de que la <strong>Gran Flecha Azul</strong> apunte en el sentido de la navegación.</li>
<li>Gira el disco central de la brújula (Rosa de los Vientos) hasta que las líneas de su cuadrícula interna queden perfectamente paralelas a los <strong>meridianos (líneas Norte-Sur)</strong> de la carta.</li>
<li>Lee directamente en el transportador el <strong>Rumbo Verdadero (Rv)</strong> indicado por la marca de lectura fija.</li>
</ul>
<p><strong>2. Corrección Total (Ct) y Rumbo de Aguja (Ra):</strong></p>
<ul class="list-disc list-inside space-y-1 text-slate-400 pl-2">
<li>Ajusta la corrección total Ct = (±dm) + (±Δ) en el deslizador lateral.</li>
<li>Obtén el Rumbo Magnético o de Aguja (Rm/Ra) aplicando la fórmula clásica de corrección.</li>
</ul>
<p><strong>3. Trazado de Rutas Múltiples (A ➞ B ➞ C ➞ ...):</strong></p>
<ul class="list-disc list-inside space-y-1 text-slate-400 pl-2">
<li>Haz clic en <strong>"Añadir Puntos Ruta"</strong> para marcar secuencialmente los waypoints en la carta. El primer tramo (A ➞ B) orientará la regla automáticamente; para los siguientes tramos (B ➞ C ➞ ...), el usuario debe orientar y mover la regla manualmente sobre la línea trazada.</li>
</ul>
</div>
<div class="pt-2 flex justify-end">
<button onclick="toggleHelpModal(false)" class="bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold px-4 py-1.5 rounded-lg text-xs">
Entendido
</button>
</div>
</div>
</div>
<script>
const state = {
rulerX: 0,
rulerY: 0,
rulerAngle: 0,
diskAngle: 0,
totalError: 0,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
zoom: 1.0,
mode: 'MOVE',
points: [],
customChartImg: null,
baseWidth: 1400,
baseHeight: 900
};
const canvas = document.getElementById('plotterCanvas');
const ctx = canvas.getContext('2d');
const container = document.getElementById('canvas-container');
function initCanvasSize() {
canvas.width = state.baseWidth * state.zoom;
canvas.height = state.baseHeight * state.zoom;
if (state.rulerX === 0 && state.rulerY === 0) {
centerRulerAbsolute();
}
draw();
}
function centerRulerAbsolute() {
state.rulerX = canvas.width / 2;
state.rulerY = canvas.height / 2;
container.scrollTo({
left: (canvas.width - container.clientWidth) / 2,
top: (canvas.height - container.clientHeight) / 2,
behavior: 'smooth'
});
draw();
}
function centerRulerToView() {
state.rulerX = container.scrollLeft + (container.clientWidth / 2);
state.rulerY = container.scrollTop + (container.clientHeight / 2);
draw();
}
function toggleHelpModal(show) {
const modal = document.getElementById('help-modal');
if (show) modal.classList.remove('hidden');
else modal.classList.add('hidden');
}
document.getElementById('chart-file-input').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
state.customChartImg = img;
const maxW = 1800;
const scale = img.width > maxW ? maxW / img.width : 1;
state.baseWidth = img.width * scale;
state.baseHeight = img.height * scale;
initCanvasSize();
centerRulerAbsolute();
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
});
function resetChartImage() {
state.customChartImg = null;
document.getElementById('chart-file-input').value = '';
state.baseWidth = 1400;
state.baseHeight = 900;
state.zoom = 1.0;
document.getElementById('zoom-slider').value = 1.0;
initCanvasSize();
centerRulerAbsolute();
}
function adjustZoom(delta) {
let newZoom = Math.max(0.5, Math.min(3.0, state.zoom + delta));
setZoom(newZoom);
}
function setZoom(val) {
state.zoom = val;
document.getElementById('zoom-slider').value = val;
document.getElementById('zoom-val').innerText = `${Math.round(val * 100)}%`;
initCanvasSize();
}
document.getElementById('zoom-slider').addEventListener('input', (e) => {
setZoom(parseFloat(e.target.value));
});
function drawChart() {
if (state.customChartImg) {
ctx.drawImage(state.customChartImg, 0, 0, canvas.width, canvas.height);
} else {
ctx.fillStyle = '#cbe0f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#e5d9a5';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(canvas.width * 0.4, 0);
ctx.bezierCurveTo(canvas.width * 0.3, canvas.height * 0.3, canvas.width * 0.1, canvas.height * 0.5, 0, canvas.height * 0.7);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#b5a469';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#1e293b';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('CARTA NÁUTICA DE REFERENCIA (ESTRECHO DE GIBRALTAR)', 40, 50);
ctx.strokeStyle = '#64748b';
ctx.lineWidth = 0.8;
ctx.setLineDash([4, 4]);
const step = 120 * state.zoom;
for (let x = step; x < canvas.width; x += step) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = step; y < canvas.height; y += step) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
ctx.setLineDash([]);
}
// Dibujar líneas de ruta encadenadas (A -> B -> C -> ...)
if (state.points.length > 1) {
ctx.strokeStyle = '#f59e0b';
ctx.lineWidth = 3;
ctx.setLineDash([6, 4]);
ctx.beginPath();
ctx.moveTo(state.points[0].x, state.points[0].y);
for (let i = 1; i < state.points.length; i++) {
ctx.lineTo(state.points[i].x, state.points[i].y);
}
ctx.stroke();
ctx.setLineDash([]);
}
// Dibujar marcadores de puntos
state.points.forEach(p => {
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#0f172a';
ctx.font = 'bold 11px sans-serif';
ctx.fillText(`Punto ${p.label}`, p.x + 10, p.y + 4);
});
}
function drawPlotter() {
ctx.save();
ctx.translate(state.rulerX, state.rulerY);
ctx.rotate((state.rulerAngle * Math.PI) / 180);
const rw = 860;
const rh = 160;
const rx = -rw / 2;
const ry = -rh / 2;
const radius = 68;
// Cuerpo translúcido de la regla principal
ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
ctx.strokeStyle = 'rgba(71, 85, 105, 0.8)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(rx, ry, rw, rh, 10);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
ctx.lineWidth = 1;
ctx.strokeRect(rx + 2, ry + 2, rw - 4, rh - 4);
// Gran flecha azul "DIRECTION OF BEARING" (hacia la derecha en el sistema local)
ctx.fillStyle = 'rgba(2, 132, 199, 0.85)';
ctx.beginPath();
ctx.moveTo(rx + rw - 40, 0);
ctx.lineTo(rx + rw - 100, -35);
ctx.lineTo(rx + rw - 100, 35);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('DIRECTION OF BEARING', rx + rw - 140, 4);
// --- CUADRÍCULA DE LA REGLA FIJA ---
ctx.strokeStyle = 'rgba(15, 23, 42, 0.35)';
ctx.lineWidth = 0.8;
const gridStep = 20;
// Líneas Verticales (omitiendo el círculo central)
for (let x = rx + 30; x <= rx + rw - 30; x += gridStep) {
if (Math.abs(x) >= radius + 2) {
ctx.beginPath();
ctx.moveTo(x, ry + 15);
ctx.lineTo(x, ry + rh - 15);
ctx.stroke();
}
}
// Líneas Horizontales (omitiendo el círculo central)
for (let y = ry + 15; y <= ry + rh - 15; y += gridStep) {
ctx.beginPath();
ctx.moveTo(rx + 30, y);
ctx.lineTo(-radius - 2, y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(radius + 2, y);
ctx.lineTo(rx + rw - 30, y);
ctx.stroke();
}
// Marcas milimetradas/graduadas en los bordes superiores e inferiores
ctx.fillStyle = '#0f172a';
ctx.font = '8px monospace';
ctx.textAlign = 'center';
for (let i = 0; i <= 80; i++) {
const xMark = rx + 30 + i * 10;
if (xMark < rx + rw - 30) {
const hMark = (i % 5 === 0) ? 10 : 5;
ctx.beginPath();
ctx.moveTo(xMark, ry);
ctx.lineTo(xMark, ry + hMark);
ctx.moveTo(xMark, ry + rh);
ctx.lineTo(xMark, ry + rh - hMark);
ctx.stroke();
if (i % 5 === 0) {
ctx.fillText(i, xMark, ry + 18);
ctx.fillText(i, xMark, ry + rh - 12);
}
}
}
// --- DISCO CENTRAL GIRATORIO (TOTALMENTE TRANSPARENTE) ---
ctx.save();
const diskAngleRad = (state.diskAngle * Math.PI) / 180;
ctx.rotate(diskAngleRad);
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.stroke();
// Cuadrícula propia del disco giratorio
ctx.strokeStyle = 'rgba(15, 23, 42, 0.4)';
ctx.lineWidth = 0.8;
const innerStep = 14;
for (let x = -radius + 14; x <= radius - 14; x += innerStep) {
const limitY = Math.sqrt(Math.max(0, radius * radius - x * x));
ctx.beginPath();
ctx.moveTo(x, -limitY);
ctx.lineTo(x, limitY);
ctx.stroke();
}
for (let y = -radius + 14; y <= radius - 14; y += innerStep) {
const limitX = Math.sqrt(Math.max(0, radius * radius - y * y));
ctx.beginPath();
ctx.moveTo(-limitX, y);
ctx.lineTo(limitX, y);
ctx.stroke();
}
// Marcas de grados del transportador
for (let deg = 0; deg < 360; deg += 2) {
const rad = (deg * Math.PI) / 180;
const isMajor = deg % 10 === 0;
const isMedium = deg % 5 === 0;
const len = isMajor ? 10 : (isMedium ? 7 : 4);
const x1 = Math.sin(rad) * radius;
const y1 = -Math.cos(rad) * radius;
const x2 = Math.sin(rad) * (radius - len);
const y2 = -Math.cos(rad) * (radius - len);
ctx.strokeStyle = isMajor ? '#0f172a' : '#475569';
ctx.lineWidth = isMajor ? 1.5 : 0.8;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
if (isMajor) {
const tx = Math.sin(rad) * (radius - 18);
const ty = -Math.cos(rad) * (radius - 18);
ctx.fillStyle = '#0f172a';
ctx.font = 'bold 8px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(deg, tx, ty);
}
}
// TRIÁNGULO ROJO EN EL NORTE (0º) DEL DISCO
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.moveTo(0, -radius + 12);
ctx.lineTo(-7, -radius + 24);
ctx.lineTo(7, -radius + 24);
ctx.closePath();
ctx.fill();
ctx.restore(); // Fin de rotación del disco
// Perno central de la brújula
ctx.fillStyle = '#f59e0b';
ctx.beginPath();
ctx.arc(0, 0, 7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#0f172a';
ctx.beginPath();
ctx.arc(0, 0, 2, 0, Math.PI * 2);
ctx.fill();
// --- MARCA DE LECTURA FIJA (Índice triangular negro a la derecha) ---
ctx.save();
const markerX = radius;
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.moveTo(markerX, 0);
ctx.lineTo(markerX + 8, -4);
ctx.lineTo(markerX + 8, 4);
ctx.closePath();
ctx.fill();
ctx.restore();
ctx.restore(); // Fin de la regla principal
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawChart();
drawPlotter();
updateReadouts();
}
function updateReadouts() {
// Cálculo preciso unificado del Rumbo Verdadero (Rv) considerando la diferencia entre regla y brújula
let baseReferenceAngle = (360 - state.rulerAngle) % 360;
let rv = (state.rulerAngle + 90 - (state.diskAngle - baseReferenceAngle)) % 360;
if (rv < 0) rv += 360;
let rm = (rv - state.totalError) % 360;
if (rm < 0) rm += 360;
document.getElementById('readout-rv').innerText = `${Math.round(rv).toString().padStart(3, '0')}°`;
document.getElementById('readout-err').innerText = `${Math.abs(state.totalError)}° ${state.totalError < 0 ? 'W' : 'E'}`;
document.getElementById('readout-rm').innerText = `${Math.round(rm).toString().padStart(3, '0')}°`;
document.getElementById('disk-angle-val').innerText = `${Math.round(state.diskAngle).toString().padStart(3, '0')}°`;
document.getElementById('ruler-angle-val').innerText = `${Math.round(state.rulerAngle).toString().padStart(3, '0')}°`;
document.getElementById('error-val').innerText = `${Math.abs(state.totalError)}° ${state.totalError < 0 ? 'W' : 'E'}`;
}
function toggleRouteMode() {
const btn = document.getElementById('btn-mode-ab');
const badge = document.getElementById('status-badge');
const statusText = document.getElementById('status-text');
if (state.mode === 'MOVE') {
state.mode = 'ADD_POINTS';
canvas.className = "cursor-crosshair-custom block m-auto";
btn.className = "bg-rose-500 hover:bg-rose-400 text-slate-950 font-bold px-3.5 py-1 rounded-lg text-xs transition-all flex items-center gap-2 shadow-md";
document.getElementById('btn-ab-text').innerText = "Finalizar Puntos";
badge.classList.remove('hidden');
statusText.innerText = "Haz clic en la carta para añadir puntos (A -> B -> C...)";
} else {
resetToMoveMode();
}
draw();
}
function resetToMoveMode() {
state.mode = 'MOVE';
canvas.className = "cursor-grab-custom block m-auto";
const btn = document.getElementById('btn-mode-ab');
btn.className = "bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold px-3.5 py-1 rounded-lg text-xs transition-all flex items-center gap-2 shadow-md";
document.getElementById('btn-ab-text').innerText = "Añadir Puntos Ruta";
document.getElementById('status-badge').classList.add('hidden');
}
function clearAllRoutes() {
state.points = [];
resetToMoveMode();
draw();
}
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
if (state.mode === 'ADD_POINTS') {
const nextChar = String.fromCharCode(65 + state.points.length);
state.points.push({ x: clickX, y: clickY, label: nextChar });
if (state.points.length === 2) {
const pA = state.points[0];
const pB = state.points[1];
const dx = pB.x - pA.x;
const dy = pB.y - pA.y;
let angleRad = Math.atan2(dy, dx);
let angleDeg = (angleRad * 180) / Math.PI;
if (angleDeg < 0) angleDeg += 360;
state.rulerAngle = Math.round(angleDeg);
// Configuración automática de la rosa según la orientación de la regla
state.diskAngle = (360 - state.rulerAngle) % 360;
document.getElementById('ruler-angle-slider').value = state.rulerAngle;
document.getElementById('disk-angle-slider').value = state.diskAngle;
state.rulerX = (pA.x + pB.x) / 2;
state.rulerY = (pA.y + pB.y) / 2;
}
draw();
return;
}
const dist = Math.hypot(clickX - state.rulerX, clickY - state.rulerY);
if (dist < 250) {
state.isDragging = true;
state.dragStartX = clickX - state.rulerX;
state.dragStartY = clickY - state.rulerY;
canvas.classList.remove('cursor-grab-custom');
canvas.classList.add('cursor-grabbing');
}
});
canvas.addEventListener('mousemove', (e) => {
if (state.isDragging && state.mode === 'MOVE') {
const rect = canvas.getBoundingClientRect();
state.rulerX = (e.clientX - rect.left) - state.dragStartX;
state.rulerY = (e.clientY - rect.top) - state.dragStartY;
draw();
}
});
window.addEventListener('mouseup', () => {
if (state.isDragging) {
state.isDragging = false;
canvas.classList.remove('cursor-grabbing');
canvas.classList.add('cursor-grab-custom');
}
});
document.getElementById('disk-angle-slider').addEventListener('input', (e) => {
state.diskAngle = parseFloat(e.target.value);
draw();
});
document.getElementById('ruler-angle-slider').addEventListener('input', (e) => {
state.rulerAngle = parseFloat(e.target.value);
draw();
});
document.getElementById('error-slider').addEventListener('input', (e) => {
state.totalError = parseFloat(e.target.value);
draw();
});
window.addEventListener('load', () => {
initCanvasSize();
});
</script>
</body>
</html>