Spaces:
Running
Running
File size: 7,723 Bytes
4255d2e d01b9eb 4255d2e d01b9eb 4255d2e d01b9eb 4255d2e d01b9eb 4255d2e d01b9eb 4255d2e d01b9eb 4255d2e d01b9eb 4255d2e d01b9eb 4255d2e d01b9eb fbbe335 d01b9eb 4255d2e d01b9eb 4255d2e 113e479 d01b9eb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | /* ============================================================================
CIELO — estrellas generativas
----------------------------------------------------------------------------
No hay ni una estrella escrita a mano. Se generan al cargar, leyendo de
tokens.css cuántas, de qué brillo y con qué semilla. Cambiar la densidad o
el brillo del cielo es cambiar un número en tokens.css.
Se usa una semilla fija para que el cielo sea el mismo en cada visita (y
para que las capturas de los tests no cambien sin motivo). Cambia
--cielo-semilla y tendrás otro cielo.
========================================================================== */
/** Generador determinista: misma semilla, mismas estrellas. */
function azar(semilla) {
let a = semilla >>> 0;
return function () {
a = (a + 0x6d2b79f5) >>> 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function leerNumero(estilos, nombre, porDefecto) {
const valor = parseFloat(estilos.getPropertyValue(nombre));
return Number.isFinite(valor) ? valor : porDefecto;
}
/* Cuánto se siembra más allá de la pantalla, por cada lado. El cielo gira, y
sin este margen el giro dejaría a la vista una esquina sin estrellas. */
const MARGEN = 0.6;
/** Las posiciones de una capa, antes de convertirlas en box-shadow. */
function generar(dado, cantidad, ancho, alto, alfaMin, alfaMax) {
const estrellas = [];
for (let i = 0; i < cantidad; i += 1) {
const x = Math.round((dado() * (1 + MARGEN * 2) - MARGEN) * ancho);
const y = Math.round((dado() * (1 + MARGEN * 2) - MARGEN) * alto);
const alfa = alfaMin + dado() * (alfaMax - alfaMin);
estrellas.push({ x, y, alfa });
}
return estrellas;
}
function sembrar(estrellas) {
// El color sale del token --c-estrella: aquí no se escribe ningún color.
return estrellas
.map(({ x, y, alfa }) => `${x}px ${y}px 0 rgb(var(--c-estrella) / ${alfa.toFixed(2)})`)
.join(", ");
}
/**
* Los destellos no son estrellas nuevas: son algunas de las estrellas
* CERCANAS ya sembradas, elegidas al azar (las lejanas son demasiadas y
* demasiado chicas para que un brillo grande tenga sentido ahí). Solo qué
* posiciones son "candidatas" sale de la semilla (mismo campo en cada
* visita); CUÁNDO y CUÁL destella en cada momento se decide aparte, en
* `encenderCielo`, con temporizadores al azar — un período fijo, por más
* que varíe, siempre se termina percibiendo como un ciclo.
*/
function elegirAlAzar(dado, lista, cantidad) {
const indices = lista.map((_, i) => i);
const tope = Math.min(cantidad, indices.length);
const elegidas = [];
for (let i = 0; i < tope; i += 1) {
const j = i + Math.floor(dado() * (indices.length - i));
[indices[i], indices[j]] = [indices[j], indices[i]];
elegidas.push(lista[indices[i]]);
}
return elegidas;
}
/**
* Dibuja el cielo y lo redibuja cuando cambia el tamaño de la ventana.
* Devuelve una función para dejar de escuchar (la usan los tests).
*/
export function encenderCielo(raiz = document.documentElement) {
const nodoDestellos = raiz.querySelector?.(".cielo__capa--destellos")
?? document.getElementById("estrellas-destello");
let nodosDestello = [];
let proximoDestello;
const pintar = () => {
const estilos = getComputedStyle(raiz);
const ancho = Math.max(window.innerWidth, 360);
const alto = Math.max(window.innerHeight, 640);
const semilla = leerNumero(estilos, "--cielo-semilla", 7);
const dado = azar(semilla);
const lejanas = generar(
dado, Math.round(leerNumero(estilos, "--cielo-lejanas", 150) * (1 + MARGEN * 2) ** 2 / 2),
ancho, alto,
leerNumero(estilos, "--cielo-alfa-lejana-min", 0.14),
leerNumero(estilos, "--cielo-alfa-lejana-max", 0.4)
);
raiz.style.setProperty("--estrellas-lejanas", sembrar(lejanas));
const cercanas = generar(
dado, Math.round(leerNumero(estilos, "--cielo-cercanas", 34) * (1 + MARGEN * 2) ** 2 / 2),
ancho, alto,
leerNumero(estilos, "--cielo-alfa-cercana-min", 0.5),
leerNumero(estilos, "--cielo-alfa-cercana-max", 0.9)
);
raiz.style.setProperty("--estrellas-cercanas", sembrar(cercanas));
if (nodoDestellos) {
const candidatas = elegirAlAzar(
dado, cercanas, Math.round(leerNumero(estilos, "--cielo-destellos", 14))
);
nodosDestello = candidatas.map(({ x, y }) => {
const nodo = document.createElement("i");
nodo.className = "destello";
nodo.style.left = `${x}px`;
nodo.style.top = `${y}px`;
return nodo;
});
nodoDestellos.replaceChildren(...nodosDestello);
}
};
pintar();
// Cada destello se dispara solo, en una estrella al azar, con una espera
// también al azar hasta el siguiente. Nada de período: por eso no se
// sincronizan ni se sienten mecánicos.
const encender = (nodo) => {
nodo.classList.remove("destello--activo");
// Un solo requestAnimationFrame no alcanza: ese callback corre ANTES de
// que el navegador recalcule estilos para el fotograma, así que a veces
// nunca llega a "ver" la clase quitada, y la animación no se reinicia
// (la estrella destellaría una vez y después no más). Con dos, entre
// medio ya hubo un recálculo de estilos real.
requestAnimationFrame(() => {
requestAnimationFrame(() => nodo.classList.add("destello--activo"));
});
};
const encenderUnDestello = () => {
if (nodosDestello.length > 0) {
encender(nodosDestello[Math.floor(Math.random() * nodosDestello.length)]);
// De tanto en tanto, que le toque a dos casi al mismo tiempo: en un
// cielo real no destellan de a una por turno.
if (nodosDestello.length > 1 && Math.random() < 0.25) {
const otro = nodosDestello[Math.floor(Math.random() * nodosDestello.length)];
setTimeout(() => encender(otro), Math.random() * 150);
}
}
const estilos = getComputedStyle(raiz);
const esperaMin = leerNumero(estilos, "--cielo-destello-espera-min", 0.6) * 1000;
const esperaMax = leerNumero(estilos, "--cielo-destello-espera-max", 3.5) * 1000;
proximoDestello = setTimeout(encenderUnDestello, esperaMin + Math.random() * (esperaMax - esperaMin));
};
encenderUnDestello();
let espera;
const alRedimensionar = () => {
clearTimeout(espera);
espera = setTimeout(pintar, 200);
};
window.addEventListener("resize", alRedimensionar);
return () => {
clearTimeout(espera);
clearTimeout(proximoDestello);
window.removeEventListener("resize", alRedimensionar);
};
}
/**
* El grano del vidrio: un lienzo de 128×128 con ruido al azar, convertido a
* imagen y puesto en --grano para que css/tokens.css lo use como textura
* (ver --vidrio-fondo). Se hace una sola vez, al arrancar. Si el dispositivo
* no puede crear el lienzo, no se pone nada: el vidrio se queda sin grano,
* no es imprescindible.
*/
export function aplicarGrano(raiz = document.documentElement) {
const lienzo = document.createElement("canvas");
lienzo.width = 128;
lienzo.height = 128;
const ctx = lienzo.getContext("2d");
if (!ctx) return;
const imagen = ctx.createImageData(lienzo.width, lienzo.height);
for (let i = 0; i < imagen.data.length; i += 4) {
const v = Math.floor(Math.random() * 256);
imagen.data[i] = v;
imagen.data[i + 1] = v;
imagen.data[i + 2] = v;
imagen.data[i + 3] = 255;
}
ctx.putImageData(imagen, 0, 0);
raiz.style.setProperty("--grano", `url(${lienzo.toDataURL()})`);
}
export const _paraPruebas = { azar, generar, sembrar, elegirAlAzar };
|