/* franja.js — la franja de los sitios de gioser: gioser.net, tawasuyu.net y takana-os.net. * * UNA sola fuente. Vive en gioser-web (`franja.js`) y se copia TAL CUAL a tawasuyu * (`web/tawasuyu-web/franja.js`) y a takana (`web/takana-os.net/franja.js`): esos dos sitios no le * piden nada a terceros, así que no la pueden pedir a gioser.net. `scripts/franja-copias.sh` * compara las tres y dice cuál se separó. Antes eran tres copias escritas a mano, y un mismo * defecto —el enlace al inicio que le robaba el clic a la primera pestaña— hubo que arreglarlo * tres veces, una de ellas de otra manera (14-oct). * * Lo que hace, igual en los tres: * · el símbolo sube desde la portada con el primer scroll y se planta en la franja; * · la franja se pinta clara sobre la portada y con el color del texto sobre el cuerpo; * · lo que se pasa por debajo de la franja (la tarjeta de la portada) se apaga antes de tocarla; * · cuando las pestañas llegan arriba se encogen y se acomodan en la franja: el menú son ellas; * · se pasa de una pestaña a otra deslizando, como un PageView de Flutter (el panel sigue al * dedo, entra el vecino desde ese lado, rebota en el borde; lo que se desplaza de costado * manda hasta su borde; el gesto se reclama para que el navegador no lo tome como «volver»); * · elegir una pestaña a mano manda una señal mínima a `/visto?t=` (sin cookies ni * identificadores: el `#` de la dirección nunca llega al servidor, y sin esto no hay forma de * saber qué se lee). Caddy contesta 204 y lo anota en su registro. * * Lo que es de cada sitio entra por las opciones: qué elementos, cómo se elige una pestaña (en * tawasuyu hay que traer el catálogo primero), qué panel está a la vista. */ (function () { 'use strict'; var suave = function (t) { return t * t * (3 - 2 * t); }; var RAMPA = 90; // px de scroll en que las pestañas se vuelven menú function Franja(o) { var quieto = matchMedia('(prefers-reduced-motion: reduce)').matches; var sc = o.scroller || null; // el que se desplaza; null = la ventana var pos = function () { return sc ? sc.scrollTop : scrollY; }; var ir = function (top, suavecito) { var op = { top: top, behavior: (suavecito && !quieto) ? 'smooth' : 'auto' }; if (sc) sc.scrollTo(op); else scrollTo(op); }; var franja = o.franja, marca = o.marca, grande = o.grande, glifos = o.glifos; var barra = o.barra, tabs = o.tabs, apagar = o.apagar || []; var esc = o.escala || [0.55, 0.92], margen = o.margen || 104; var geo = null, menu = null, p = 0; document.documentElement.classList.add('js'); function medir() { var s = pos(), alto = franja.offsetHeight; if (marca && grande) { marca.style.transform = 'none'; var h = grande.getBoundingClientRect(), b = marca.getBoundingClientRect(); geo = { hx: h.left, hy: h.top + s, hw: h.width, bx: b.left, by: b.top, bw: b.width }; } barra.style.setProperty('--p', 0); tabs.forEach(function (t) { t.style.transform = 'none'; }); var top = alto - barra.offsetHeight; // pegada, asoma sólo su franja de abajo barra.style.setProperty(o.varTop, top + 'px'); // un sticky reporta su posición PEGADA: el lugar natural de la barra lo dice el sitio var docTop = s + o.inicioBarra(); var br = barra.getBoundingClientRect(); var rot = tabs.map(function (t) { var l = t.querySelector(o.rotulo).getBoundingClientRect(), b = t.getBoundingClientRect(); return { cx: l.left + l.width / 2, w: l.width, cy: l.top + l.height / 2 - br.top, ox: l.left + l.width / 2 - b.left, oy: l.top + l.height / 2 - b.top }; }); // dónde caben en la franja: entre el símbolo (con su palabra) y los glifos var izq = (marca ? marca.getBoundingClientRect().right : 0) + (innerWidth < 720 ? 14 : margen); var der = (glifos ? glifos.getBoundingClientRect().left : innerWidth) - 14; var gap = innerWidth < 720 ? 12 : 22; var suma = rot.reduce(function (a, l) { return a + l.w; }, 0); var k = Math.max(esc[0], Math.min(esc[1], (der - izq - gap * (rot.length - 1)) / suma)); var total = suma * k + gap * (rot.length - 1); var x0 = innerWidth / 2 - total / 2; if (x0 < izq) x0 = izq; if (x0 + total > der) x0 = Math.max(izq, der - total); var x = x0; rot.forEach(function (l) { l.dx = x + l.w * k / 2 - l.cx; x += l.w * k + gap; }); menu = { alto: alto, top: top, pegado: docTop - top, rot: rot, k: k }; tabs.forEach(function (t, i) { t.style.transformOrigin = rot[i].ox + 'px ' + rot[i].oy + 'px'; }); } function volar(s) { if (!geo) return; // el símbolo sube con la portada hasta la altura de la franja, y en ese trayecto se // corre a su lugar y se achica var t = Math.min(1, Math.max(0, s / Math.max(1, geo.hy - geo.by))), e = suave(t); var x = geo.hx + (geo.bx - geo.hx) * e, y = Math.max(geo.by, geo.hy - s); var k = geo.hw / geo.bw + (1 - geo.hw / geo.bw) * e; marca.style.transform = 'translate(' + (x - geo.bx).toFixed(1) + 'px, ' + (y - geo.by).toFixed(1) + 'px) scale(' + k.toFixed(3) + ')'; franja.classList.toggle('llego', t > 0.98); } function encoger(s) { if (!menu) return; var t = Math.min(1, Math.max(0, (s - (menu.pegado - RAMPA)) / RAMPA)); p = suave(t); barra.style.setProperty('--p', p.toFixed(3)); var falta = Math.max(0, menu.pegado - s); tabs.forEach(function (tab, i) { var l = menu.rot[i], dy = menu.alto / 2 - (menu.top + l.cy + falta), k = 1 - (1 - menu.k) * p; tab.style.transform = p === 0 ? 'none' : 'translate(' + (l.dx * p).toFixed(1) + 'px, ' + (dy * p).toFixed(1) + 'px) scale(' + k.toFixed(3) + ')'; }); franja.classList.toggle('con-menu', p > 0.5); } var tick = false; function pintar() { tick = false; var s = pos(), alto = franja.offsetHeight; volar(s); encoger(s); apagar.forEach(function (el) { var tp = el.getBoundingClientRect().top; el.style.opacity = Math.min(1, Math.max(0, (tp - alto) / (alto * 1.5))).toFixed(3); }); if (o.cuerpo) franja.classList.toggle('sobre-portada', o.cuerpo.getBoundingClientRect().top > alto); } (sc || window).addEventListener('scroll', function () { if (!tick) { tick = true; requestAnimationFrame(pintar); } }, { passive: true }); addEventListener('resize', function () { medir(); pintar(); }, { passive: true }); function irALista() { if (menu) ir(Math.round(menu.pegado), true); } // ── elegir: lo de cada sitio, más la señal de lectura ── var indice = function () { return tabs.findIndex(function (t) { return t.getAttribute('aria-selected') === 'true'; }); }; function senal(id) { if (!o.senal || !navigator.sendBeacon) return; try { navigator.sendBeacon('/visto?t=' + encodeURIComponent(id)); } catch (e) { /* sin señal */ } } function elegir(id, como) { // como: {foco, comoMenu, desde: 'der'|'izq'} como = como || {}; return Promise.resolve(o.elegir(id)).then(function () { senal(id); if (como.foco) { var t = tabs.find(function (x) { return o.id(x) === id; }); if (t) t.focus(); } if (como.desde) { var nuevo = o.panel(); if (nuevo) { nuevo.classList.remove('desde-der', 'desde-izq'); void nuevo.offsetWidth; nuevo.classList.add('desde-' + como.desde); nuevo.addEventListener('animationend', function () { nuevo.classList.remove('desde-der', 'desde-izq'); }, { once: true }); } } if (como.comoMenu) irALista(); }); } tabs.forEach(function (t, i) { t.addEventListener('click', function () { elegir(o.id(t), { comoMenu: p > 0.5 }); }); t.addEventListener('keydown', function (e) { var n = tabs.length; var j = e.key === 'ArrowRight' ? (i + 1) % n : e.key === 'ArrowLeft' ? (i - 1 + n) % n : e.key === 'Home' ? 0 : e.key === 'End' ? n - 1 : -1; if (j >= 0) { e.preventDefault(); elegir(o.id(tabs[j]), { foco: true }); } }); }); if (o.inicio) o.inicio.addEventListener('click', function (e) { e.preventDefault(); ir(0, true); }); // ── deslizar entre pestañas (como un PageView) ── var zona = o.zonaGesto; if (zona) { var vecino = function (dir) { var j = indice() + dir; return (j >= 0 && j < tabs.length) ? o.id(tabs[j]) : null; }; var corre = function (el) { for (; el && el !== zona; el = el.parentElement) { var ox = getComputedStyle(el).overflowX; if ((ox === 'auto' || ox === 'scroll') && el.scrollWidth > el.clientWidth + 2) return el; } return null; }; var x0 = null, y0 = 0, dx = 0, eje = null, panel = null, rueda = null; zona.addEventListener('touchstart', function (e) { if (e.touches.length !== 1) { x0 = null; return; } rueda = corre(e.target); x0 = e.touches[0].clientX; y0 = e.touches[0].clientY; dx = 0; eje = null; panel = o.panel(); }, { passive: true }); zona.addEventListener('touchmove', function (e) { if (x0 === null || !panel) return; var mx = e.touches[0].clientX - x0, my = e.touches[0].clientY - y0; if (!eje) { if (Math.abs(mx) < 10 && Math.abs(my) < 10) return; eje = Math.abs(mx) > Math.abs(my) * 1.3 ? 'x' : 'y'; // como un PageView anidado: el que se desplaza de costado manda, salvo que esté en su borde if (eje === 'x' && rueda) { var finD = rueda.scrollLeft + rueda.clientWidth >= rueda.scrollWidth - 2, finI = rueda.scrollLeft <= 2; if (!(mx < 0 ? finD : finI)) eje = 'rueda'; } } if (eje !== 'x') return; // el gesto horizontal es de la página: sin esto el navegador lo toma como «volver atrás» // (en gioser.net el que se desplaza es un `.layer` fijo, donde overscroll-behavior no alcanza) if (e.cancelable) e.preventDefault(); dx = vecino(mx < 0 ? 1 : -1) ? mx : mx * 0.25; // en el borde resiste, como el rebote panel.style.transition = 'none'; panel.style.transform = 'translateX(' + (dx * 0.6).toFixed(1) + 'px)'; panel.style.opacity = (1 - Math.min(0.45, Math.abs(dx) / innerWidth)).toFixed(3); }, { passive: false }); zona.addEventListener('touchend', function () { if (x0 === null || !panel) return; x0 = null; var p0 = panel, d = dx, dir = d < 0 ? 1 : -1, id = vecino(dir); p0.style.transition = 'transform .2s ease, opacity .2s ease'; p0.style.transform = ''; p0.style.opacity = ''; setTimeout(function () { p0.style.transition = ''; }, 220); if (eje !== 'x' || !id || Math.abs(d) < Math.min(90, innerWidth * 0.22)) return; elegir(id, { desde: dir > 0 ? 'der' : 'izq', comoMenu: p > 0.5 }); }); } medir(); pintar(); // las fuentes y la portada llegan tarde y mueven las medidas (la primera vez trastabillaba) if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { medir(); pintar(); }); addEventListener('load', function () { medir(); pintar(); }); return { medir: medir, pintar: pintar, irALista: irALista, p: function () { return p; }, elegir: function (id, como) { return elegir(id, como); } }; } window.Franja = Franja; })();