Calculadora de pintura de Andrés Merino
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Calculadora de pintura de Andrés Merino</title>
<style>
:root{
--brand:#046BD2;
--brand-2:#1E88FF; /* botón y acentos (armoniza con el azul principal) */
--brand-3:#0E5FB8; /* hover */
--card:#ffffff;
--text:#0b0f18;
--muted:#556070;
--border:rgba(11,15,24,.14);
--shadow:0 18px 45px rgba(0,0,0,.18);
--radius:18px;
}
*{box-sizing:border-box}
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background:var(--brand);
color:#fff;
padding:34px 14px 56px;
}
.wrap{max-width:980px; margin:0 auto;}
header{
display:flex;
flex-direction:column;
align-items:center;
gap:10px;
margin-bottom:18px;
text-align:center;
}
h1{
margin:0;
font-size:28px;
letter-spacing:.2px;
font-weight:800;
}
.subtitle{
margin:0;
opacity:.9;
font-size:14px;
}
.card{
background:var(--card);
color:var(--text);
border-radius:var(--radius);
box-shadow:var(--shadow);
padding:22px;
border:1px solid rgba(255,255,255,.12);
}
/* GRID PROLIJO: evita que algunas columnas “empujen” y desalineen */
.grid{
display:grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap:14px 16px;
margin-top:16px;
align-items:start;
}
/* Cada campo tiene wrapper para mantener alturas y márgenes coherentes */
.field{display:flex; flex-direction:column; min-width:0;}
.span2{grid-column:1 / -1;}
label{
display:block;
font-size:13px;
color:var(--muted);
margin-bottom:6px;
font-weight:700;
}
input, select{
width:100%;
padding:12px 12px;
border-radius:12px;
border:1px solid var(--border);
font-size:15px;
outline:none;
background:#fff;
height:44px; /* fuerza misma altura en todos */
}
input:focus, select:focus{
border-color: rgba(30,136,255,.7);
box-shadow: 0 0 0 3px rgba(30,136,255,.18);
}
.hint{
margin-top:6px;
font-size:12px;
color:var(--muted);
opacity:.9;
line-height:1.25;
}
.btn{
margin-top:16px;
width:100%;
padding:14px 16px;
border:none;
border-radius:14px;
background: var(--brand-2);
color:#fff;
font-size:16px;
font-weight:800;
cursor:pointer;
transition: transform .06s ease, filter .15s ease, background .15s ease;
height:48px;
}
.btn:hover{background:var(--brand-3);}
.btn:active{transform: translateY(1px);}
.result{
margin-top:16px;
background:#fff;
color:var(--text);
border-radius:var(--radius);
box-shadow: var(--shadow);
padding:20px;
border:1px solid rgba(255,255,255,.12);
}
.result h2{
margin:0 0 12px;
font-size:16px;
color:#111;
}
.rows{
display:grid;
grid-template-columns: 1fr auto;
gap:10px 14px;
align-items:center;
font-size:15px;
}
.rows .k{color:var(--muted)}
.rows .v{font-weight:900}
.note{
margin-top:10px;
font-size:12px;
color:var(--muted);
line-height:1.35;
}
.error{
margin-top:12px;
padding:12px 12px;
border-radius:12px;
background: rgba(255, 76, 76, .10);
border:1px solid rgba(255, 76, 76, .28);
color:#8a1111;
font-size:13px;
display:none;
}
@media (max-width: 760px){
.grid{grid-template-columns:1fr}
h1{font-size:24px}
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Calculadora de pintura</h1>
<p class="subtitle">de Andrés Merino</p>
</header>
<section class="card" aria-label="Formulario calculadora">
<div class="grid">
<div class="field span2">
<label for="tipo">¿Qué vas a pintar?</label>
<select id="tipo">
<option value="paredes" selected>Paredes</option>
<option value="techo">Techo</option>
<option value="fachada">Fachada</option>
</select>
</div>
<div class="field">
<label for="alto">Alto (m)</label>
<input id="alto" type="number" inputmode="decimal" step="0.01" value="2.50" min="0" />
</div>
<div class="field">
<label for="manos">N° de manos</label>
<input id="manos" type="number" inputmode="numeric" step="1" value="3" min="1" />
</div>
<div class="field" id="field-perimetro">
<label for="perimetro">Perímetro total (m)</label>
<input id="perimetro" type="number" inputmode="decimal" step="0.01" value="12" min="0" />
</div>
<div class="field" id="field-aberturas">
<label for="aberturas">Aberturas (m²)</label>
<input id="aberturas" type="number" inputmode="decimal" step="0.01" value="2" min="0" />
</div>
<div class="field" id="field-superficie-techo" style="display:none;">
<label for="superficieTecho">Superficie techo (m²)</label>
<input id="superficieTecho" type="number" inputmode="decimal" step="0.01" value="20" min="0" />
</div>
<div class="field">
<label for="rendimiento">Rendimiento (m² por litro)</label>
<input id="rendimiento" type="number" inputmode="decimal" step="0.1" value="10" min="0.1" />
</div>
<div class="field">
<label for="merma">Extra por merma (%)</label>
<input id="merma" type="number" inputmode="decimal" step="1" value="10" min="0" />
</div>
<div class="field span2">
<label for="presentaciones">Presentaciones disponibles (litros)</label>
<input id="presentaciones" type="text" value="1,4,10,20" />
<div class="hint">Separadas por coma. Ej: 1,4,10,20</div>
</div>
<div class="span2">
<button class="btn" id="btnCalcular" type="button">Calcular pintura necesaria</button>
<div class="error" id="errorBox"></div>
</div>
</div>
</section>
<section class="result" id="resultBox" style="display:none;" aria-label="Resultado">
<h2>Resultado</h2>
<div class="rows">
<div class="k">Superficie base</div><div class="v" id="outSuperficie">—</div>
<div class="k">Litros estimados</div><div class="v" id="outLitros">—</div>
<div class="k">Sugerencia de compra</div><div class="v" id="outSugerencia">—</div>
</div>
<div class="note" id="outNota"></div>
</section>
</div>
<script>
const $ = (id) => document.getElementById(id);
function parsePresentaciones(raw){
const arr = raw
.split(',')
.map(s => s.trim().replace(',', '.'))
.filter(Boolean)
.map(Number)
.filter(n => Number.isFinite(n) && n > 0);
// unique + orden asc
return [...new Set(arr)].sort((a,b)=>a-b);
}
function formatM2(n){ return `${n.toFixed(2)} m²`; }
function formatL(n){ return `${n.toFixed(2)} L`; }
// Sugerencia simple: greedy desde la presentación más grande
function sugerirCompra(litrosNecesarios, presentaciones){
let restante = litrosNecesarios;
const picks = [];
const sizes = [...presentaciones].sort((a,b)=>b-a);
for (const s of sizes){
if (s <= 0) continue;
const qty = Math.floor(restante / s);
if (qty > 0){
picks.push([qty, s]);
restante -= qty * s;
}
}
// Si falta algo, completar con la más chica para cubrir
if (restante > 1e-9){
const smallest = Math.min(...presentaciones);
const qty = Math.ceil(restante / smallest);
picks.push([qty, smallest]);
restante = 0;
}
const total = picks.reduce((acc,[q,s]) => acc + q*s, 0);
const text = picks.map(([q,s]) => `${q} x ${s}L`).join(' + ');
return { text: `${text} (Total: ${total.toFixed(1)} L)`, total };
}
function updateFieldsByTipo(){
const tipo = $('tipo').value;
const isTecho = (tipo === 'techo');
$('field-perimetro').style.display = isTecho ? 'none' : 'flex';
$('field-aberturas').style.display = isTecho ? 'none' : 'flex';
$('field-superficie-techo').style.display = isTecho ? 'flex' : 'none';
}
function showError(msg){
const box = $('errorBox');
box.textContent = msg;
box.style.display = 'block';
$('resultBox').style.display = 'none';
}
function clearError(){
const box = $('errorBox');
box.textContent = '';
box.style.display = 'none';
}
function calcular(){
clearError();
const tipo = $('tipo').value;
const manos = Number($('manos').value);
const rendimiento = Number($('rendimiento').value);
const mermaPct = Number($('merma').value);
const pres = parsePresentaciones($('presentaciones').value);
if (!Number.isFinite(manos) || manos < 1) return showError('Ingresá un número de manos válido (mínimo 1).');
if (!Number.isFinite(rendimiento) || rendimiento <= 0) return showError('Ingresá un rendimiento válido (mayor a 0).');
if (!Number.isFinite(mermaPct) || mermaPct < 0) return showError('Ingresá una merma válida (0 o más).');
if (pres.length === 0) return showError('Ingresá al menos una presentación válida, separada por comas (ej: 1,4,10,20).');
let superficieBase = 0;
if (tipo === 'techo'){
const supTecho = Number($('superficieTecho').value);
if (!Number.isFinite(supTecho) || supTecho <= 0) return showError('Ingresá una superficie de techo válida (mayor a 0).');
superficieBase = supTecho;
} else {
const alto = Number($('alto').value);
const perimetro = Number($('perimetro').value);
const aberturas = Number($('aberturas').value);
if (!Number.isFinite(alto) || alto <= 0) return showError('Ingresá un alto válido (mayor a 0).');
if (!Number.isFinite(perimetro) || perimetro <= 0) return showError('Ingresá un perímetro válido (mayor a 0).');
if (!Number.isFinite(aberturas) || aberturas < 0) return showError('Ingresá aberturas válidas (0 o más).');
superficieBase = (alto * perimetro) - aberturas;
if (superficieBase <= 0) return showError('La superficie base quedó en 0 o menos. Revisá perímetro, alto y aberturas.');
}
const superficieTotal = superficieBase * manos;
const litrosSinMerma = superficieTotal / rendimiento;
const litrosFinal = litrosSinMerma * (1 + mermaPct/100);
const sug = sugerirCompra(litrosFinal, pres);
$('outSuperficie').textContent = formatM2(superficieBase);
$('outLitros').textContent = formatL(litrosFinal);
$('outSugerencia').textContent = sug.text;
$('outNota').textContent =
`Cálculo con ${manos} mano(s), rendimiento ${rendimiento} m²/L y ${mermaPct}% de merma.`;
$('resultBox').style.display = 'block';
}
// Eventos
$('tipo').addEventListener('change', updateFieldsByTipo);
$('btnCalcular').addEventListener('click', calcular);
// Init
updateFieldsByTipo();
</script>
</body>
</html>
<iframe src="/calc/" style="width:100%; height:900px; border:0; border-radius:12px;" loading="lazy"></iframe>
