Spaces:
Running
Running
Download js/cielo.js from Javiai/radionicapp: direct link, hf CLI and curl.
- Browser
- Download file 7.72 kB
-
https://huggingface.co/spaces/Javiai/radionicapp/resolve/main/js/cielo.js
- Command line
-
hf download hf://spaces/Javiai/radionicapp/js/cielo.js
-
curl -L -o cielo.js https://huggingface.co/spaces/Javiai/radionicapp/resolve/main/js/cielo.js
7.72 kB
| /* ============================================================================ | |
| 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 }; | |