diff --git a/crates/apps/gioser-web/README.md b/crates/apps/gioser-web/README.md index 5f39900..9880686 100644 --- a/crates/apps/gioser-web/README.md +++ b/crates/apps/gioser-web/README.md @@ -35,32 +35,127 @@ se enchufa al runtime equivalente a `yahweh_launcher::launch_app`. - **Botones:** DOM real (accesibles, navegables por teclado, deep-link por hash) proyectados desde 3D al viewport cada frame. -## Build +--- -Requiere el target `wasm32-unknown-unknown` y `wasm-bindgen-cli`. +## Requisitos + +- **Rust** con el target `wasm32-unknown-unknown` instalado. + - Con `rustup`: `rustup target add wasm32-unknown-unknown`. + - En Artix/Arch con Rust del sistema: el target suele venir incluido en + `/usr/lib/rustlib/wasm32-unknown-unknown` (verificá con + `ls /usr/lib/rustlib | grep wasm`). Si falta: `pacman -S rust-wasm` + (no existe — el target viene con el paquete `rust` base). +- **wasm-bindgen-cli** (versión exacta 0.2.121, debe matchear la dep del + `Cargo.lock`). Verificá con + `grep -A1 '^name = "wasm-bindgen"$' Cargo.lock | head` antes de instalar. ```sh -# Una vez (si no los tenés): -rustup target add wasm32-unknown-unknown -cargo install wasm-bindgen-cli --version 0.2.99 - -# Build: -cargo build -p gioser-web --release --target wasm32-unknown-unknown - -# Generar bindings JS: -wasm-bindgen \ - target/wasm32-unknown-unknown/release/gioser_web.wasm \ - --out-dir crates/apps/gioser-web/pkg \ - --target web - -# Servir (cualquier static server vale): -python3 -m http.server -d crates/apps/gioser-web 8080 -# → http://localhost:8080/ +# Una sola vez: +cargo install wasm-bindgen-cli --version 0.2.121 --locked ``` -Si usás `rust-toolchain.toml` con Rust del sistema (Artix/Arch), instalá -el target con tu package manager (`pacman -S rust-wasm` o equivalente) o -montá rustup en un perfil aparte. +> La versión del CLI **debe** coincidir con la del crate `wasm-bindgen` +> en `Cargo.lock`. Si no coincide, el output JS no carga el `.wasm` +> generado. Si actualizás el workspace y wasm-bindgen sube de versión, +> reinstalá el CLI con la nueva versión. + +- Un static server para probar local: `python3 -m http.server` alcanza. + +--- + +## Flujo rápido (un comando) + +Hay un wrapper que hace cargo build + wasm-bindgen + copia salida: + +```sh +# Dev — sin optimización, build rápido (~10 s). +./scripts/build-gioser-web.sh dev + +# Release — opt-level=3, lto, strip, ~30 s pero binario pequeño. +./scripts/build-gioser-web.sh release +``` + +El output queda en `crates/apps/gioser-web/pkg/`: +``` +pkg/ +├── gioser_web.js ← bindings JS (referenciados por index.html) +├── gioser_web_bg.wasm ← binario WASM +└── gioser_web.d.ts ← typings (no se usan en runtime, son para IDE) +``` + +--- + +## Probarlo local + +```sh +./scripts/build-gioser-web.sh dev +python3 -m http.server -d crates/apps/gioser-web 8080 +# Abrir http://localhost:8080/ +``` + +Si cambiás Rust: re-ejecutar el script y refrescar el browser. +Si cambiás `index.html` / `styles.css`: alcanza con refrescar. + +--- + +## Build release y deploy + +```sh +./scripts/build-gioser-web.sh release +``` + +El binario release optimiza: +- `opt-level = 3`, `lto = "thin"`, `codegen-units = 1`, + `panic = "abort"`, `strip = "symbols"` (del perfil `[profile.release]` + del workspace). +- WASM resultante típico: **~120 KB** (canvas + shaders + bindings) sin + comprimir, **~50 KB** gzippeado. wasm-bindgen pasa por + `wasm-opt` automáticamente si lo encontrás en `$PATH` (instalable via + `binaryen`). + +Para deploy, los **artefactos a subir al host estático** son sólo +cuatro archivos: + +``` +crates/apps/gioser-web/ +├── index.html ← entry point +├── styles.css ← estilos +└── pkg/ + ├── gioser_web.js + └── gioser_web_bg.wasm +``` + +Funciona en cualquier static host (Nginx, GitHub Pages, S3+CloudFront, +Caddy, netlify, fly, Vercel static). **Importante:** + +- El server debe servir `.wasm` con `Content-Type: application/wasm`. + Nginx/Caddy lo hacen por default; algunos hosts muy viejos no — fijate. +- `index.html` referencia `./pkg/gioser_web.js` con `type="module"`, + o sea que el browser usa el ES module loader. Eso requiere servir por + HTTP/HTTPS (no `file://`). +- Fonts de Google: el `` apunta a `fonts.googleapis.com`. Para uso + offline o sin tracking, descargá las fuentes y servilas locales. + +### Comando deploy "tar + scp" + +```sh +./scripts/build-gioser-web.sh release +tar czf gioser-web-dist.tar.gz \ + -C crates/apps/gioser-web \ + index.html styles.css pkg/ + +# Subir al server: +scp gioser-web-dist.tar.gz user@host:/var/www/gioser/ +ssh user@host 'cd /var/www/gioser && tar xzf gioser-web-dist.tar.gz' +``` + +### GitHub Pages / gitea pages + +Configurá la branch de pages a apuntar a un directorio que contenga +sólo los 4 archivos. Un workflow CI que corra el script y commitee el +`pkg/` a la branch de deploy hace el trabajo. + +--- ## Routing @@ -68,10 +163,35 @@ Los `` apuntan a anchors locales. Cuando definamos rutas reales (otras páginas, sub-apps, etc.), basta con cambiar el `href` en `index.html` o interceptar el click desde JS. -## Tests +--- -Los crates agnósticos tienen tests unitarios: +## Tests de los crates agnósticos + +Los cuatro crates sin gpui/web tienen tests unitarios estándar: ```sh -cargo test -p gioser-geom -p gioser-physics -p gioser-palette +cargo test -p gioser-geom -p gioser-physics -p gioser-palette -p gioser-shaders ``` + +`gioser-canvas-web` no tiene tests (depende de WebGL2 que sólo existe +en browser). + +--- + +## Troubleshooting + +**Pantalla en blanco + error en consola "no link/binding found"** +→ Versión de `wasm-bindgen-cli` no coincide con la del `Cargo.lock`. +Reinstalá con `cargo install wasm-bindgen-cli --version ` +donde `` sale de `grep '^version' Cargo.lock` cerca de la entrada +`wasm-bindgen`. + +**"WebGL2 not supported"** en algunos navegadores viejos +→ No hay fallback. WebGL2 es soporte universal en navegadores modernos +(Chrome/Edge/Firefox/Safari desde 2017). Para targets ancestrales habría +que escribir un renderer WebGL1, no contemplado por ahora. + +**Build muy lento por las deps de web-sys** +→ Las features de web-sys están minimizadas en el Cargo.toml; sólo se +importan las que el renderer usa. El primer build sí es lento (~1 min), +los incrementales son rápidos. diff --git a/crates/modules/gioser/gioser-canvas-web/src/lib.rs b/crates/modules/gioser/gioser-canvas-web/src/lib.rs index 19af3a2..3b6656d 100644 --- a/crates/modules/gioser/gioser-canvas-web/src/lib.rs +++ b/crates/modules/gioser/gioser-canvas-web/src/lib.rs @@ -31,17 +31,22 @@ use web_sys::{ }; const RAD: f32 = core::f32::consts::PI / 180.0; -const MAX_TILT_DEG: f32 = 22.0; +/// Inclinación máxima en cada eje. 35° hace que las puntas se desplacen +/// visiblemente en pantalla — los botones DOM cabalgan ese movimiento. +const MAX_TILT_DEG: f32 = 35.0; +/// Escala mundo→viewport: la chacana clásica con arm_extent ≈ 0.65 ocupa +/// ~94% del eje vertical del viewport, dejando aire para aros y glow. +const WORLD_SCALE: f32 = 1.45; /// Re-export para apps: identidad (id, color, label) de cada punta cardinal, /// en el orden `[N, E, S, W]` de `ChacanaSpec::tips()`. pub mod tips { use gioser_palette::{elements, Rgb}; pub const ORDER: [(&str, Rgb, &str); 4] = [ - ("aire", elements::AIRE, "AIRE"), // N - ("fuego", elements::FUEGO, "FUEGO"), // E + ("aire", elements::AIRE, "AIRE"), // N + ("fuego", elements::FUEGO, "FUEGO"), // E ("tierra", elements::TIERRA, "TIERRA"), // S - ("agua", elements::AGUA, "AGUA"), // W + ("agua", elements::AGUA, "AGUA"), // W ]; } @@ -73,7 +78,6 @@ impl Program { let program = gl.create_program().ok_or("create_program failed")?; gl.attach_shader(&program, &vs); gl.attach_shader(&program, &fs); - // Atributo `a_pos` siempre en location 0. gl.bind_attrib_location(&program, 0, "a_pos"); gl.link_program(&program); let linked = gl @@ -126,8 +130,8 @@ fn upload_quad( gl.bind_vertex_array(Some(&vao)); let buf = gl.create_buffer().ok_or("create_buffer failed")?; gl.bind_buffer(GL::ARRAY_BUFFER, Some(&buf)); - // SAFETY: `Float32Array::view` apunta directamente a la memoria del WASM linear, - // que no podemos mover durante este scope (sin allocs intermedias). + // SAFETY: `Float32Array::view` apunta a memoria WASM lineal; no la + // movemos durante este scope (no hay allocs intermedias). unsafe { let view = js_sys::Float32Array::view(verts); gl.buffer_data_with_array_buffer_view(GL::ARRAY_BUFFER, &view, GL::STATIC_DRAW); @@ -171,6 +175,7 @@ impl Renderer { "u_mvp", "u_time", "u_thickness", + "u_center_half", "u_arm_extent", "u_line_color", "u_rim_color", @@ -181,11 +186,13 @@ impl Renderer { .map_err(JsValue::from)?; let (cosmos_vao, _) = upload_quad(&gl, &FULLSCREEN_QUAD, 0).map_err(JsValue::from)?; - let chacana_quad_verts = chacana_quad(chacana.arm_extent); + let chacana_quad_verts = chacana_quad(chacana.arm_extent()); let (chacana_vao, chacana_quad_count) = upload_quad(&gl, &chacana_quad_verts, 0).map_err(JsValue::from)?; - let tilt = SpringDamper2::new(2.2, 0.72); + // Spring sub-crítico, frecuencia baja: sensación de cuerpo pesado + // que se inclina hacia el cursor con overshoot orgánico (~10%). + let tilt = SpringDamper2::new(1.8, 0.62); Ok(Self { gl, @@ -209,8 +216,7 @@ impl Renderer { .viewport(0, 0, self.viewport.0 as i32, self.viewport.1 as i32); } - /// Recibe coords del mouse en pixeles, origen en el centro del canvas - /// (+x derecha, +y arriba). Setea el target de inclinación. + /// Mouse en pixeles desde el centro del canvas (+x derecha, +y arriba). pub fn set_mouse_px(&mut self, x: f32, y: f32) { let (w, h) = self.viewport; if h == 0 { @@ -223,13 +229,13 @@ impl Renderer { self.mouse = (mx, my); let max_tilt = MAX_TILT_DEG * RAD; - // Pitch (rot X): mouse arriba → top inclina hacia el viewer (+rotX). - // Yaw (rot Y): mouse derecha → right inclina hacia el viewer (-rotY). + // Pitch (+rotX) cuando mouse arriba: el tope se acerca al viewer. + // Yaw (-rotY) cuando mouse derecha: el lado derecho se acerca. let target = [my * max_tilt, -mx * max_tilt / aspect]; self.tilt.set_target(target); } - /// Posición proyectada (NDC `[-1, 1]²`) de cada tip cardinal en el orden N/E/S/W. + /// Posición proyectada (NDC `[-1, 1]²`) de cada tip cardinal en orden N/E/S/W. /// El caller la usa para anclar el DOM. pub fn tips_ndc(&self) -> [(f32, f32); 4] { let mvp = self.build_mvp(); @@ -246,6 +252,12 @@ impl Renderer { &self.chacana } + /// Posición normalizada del mouse en clip-space; útil para que el caller + /// añada parallax sutil a elementos DOM independientes de la chacana. + pub fn mouse_clip(&self) -> (f32, f32) { + self.mouse + } + fn build_mvp(&self) -> Mat4 { let (w, h) = self.viewport; let aspect = w as f32 / h as f32; @@ -253,7 +265,7 @@ impl Renderer { let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 2.6), Vec3::ZERO, Vec3::Y); let pitch = Mat4::from_rotation_x(self.tilt.position[0]); let yaw = Mat4::from_rotation_y(self.tilt.position[1]); - let scale = Mat4::from_scale(Vec3::splat(0.92)); + let scale = Mat4::from_scale(Vec3::splat(WORLD_SCALE)); proj * view * yaw * pitch * scale } @@ -265,7 +277,6 @@ impl Renderer { }; self.last_time_ms = time_ms; - // Subdividimos la física para mantener estabilidad con dt grandes. let sub = 4; let sub_dt = dt / sub as f32; for _ in 0..sub { @@ -313,8 +324,11 @@ impl Renderer { if let Some(u) = self.chacana_prog.u("u_thickness") { gl.uniform1f(Some(u), self.chacana.thickness); } + if let Some(u) = self.chacana_prog.u("u_center_half") { + gl.uniform1f(Some(u), self.chacana.center_half()); + } if let Some(u) = self.chacana_prog.u("u_arm_extent") { - gl.uniform1f(Some(u), self.chacana.arm_extent); + gl.uniform1f(Some(u), self.chacana.arm_extent()); } upload_rgb(gl, self.chacana_prog.u("u_line_color"), cosmos::CHACANA_LINE); upload_rgb(gl, self.chacana_prog.u("u_rim_color"), cosmos::CHACANA_RIM); diff --git a/crates/modules/gioser/gioser-geom/src/lib.rs b/crates/modules/gioser/gioser-geom/src/lib.rs index 90d7889..f466464 100644 --- a/crates/modules/gioser/gioser-geom/src/lib.rs +++ b/crates/modules/gioser/gioser-geom/src/lib.rs @@ -1,85 +1,66 @@ -//! Geometría de la chacana andina (cruz cuadrada escalonada). +//! Geometría de la chacana andina escalonada (cruz cuadrada de Tiwanaku). //! -//! Genera un polígono cerrado de 20 vértices: un cuadrado central, -//! cuatro escalones (uno por brazo cardinal) y cuatro puntas que -//! extienden hasta `arm_extent`. +//! Modelo paramétrico: un cuadrado central de lado `2 * center_half()`, +//! del que sobresalen cuatro brazos cardinales formados por `steps` +//! niveles. Cada nivel adelgaza al brazo en `thickness` por lado y lo +//! prolonga en `thickness` hacia afuera. //! -//! Convención: plano XY, centro en `(0, 0)`, +Y hacia el norte, -//! +X hacia el este. Toda la API es pura: ningún I/O, ninguna asignación -//! global; apta para ejecutar dentro de un shader-host, en un test, -//! o en una integración nativa. +//! Para `steps = 2` (clásica mística): +//! - Centro: cuadrado `6s × 6s` (donde `s = thickness`). +//! - Nivel 1: rectángulo perpendicular `4s × s` adosado a cada cara del centro. +//! - Nivel 2 (punta): rectángulo `2s × s` adosado al nivel 1. +//! +//! Resultado: bounding box `±5s` (cuadrado, no alargado como una cruz latina), +//! 9 rectángulos disjuntos triangulables, 4 tips cardinales. #[derive(Clone, Copy, Debug, PartialEq)] pub struct ChacanaSpec { - /// Distancia desde el centro hasta la punta del brazo. - pub arm_extent: f32, - /// Semi-grosor del brazo. El escalón mide `2 * thickness`. + /// Unidad base de la geometría. Cada paso aporta `thickness` de ancho + /// y `thickness` de profundidad. pub thickness: f32, + /// Cantidad de escalones por brazo (`>= 1`). La chacana mística clásica = `2`. + pub steps: u32, } impl ChacanaSpec { - /// Configuración canónica: brazo 1.0, grosor 0.18 (proporciones del logo). + /// Configuración canónica del logo GioSer: 2 escalones, thickness 0.13 + /// (bounding box ≈ 1.30 × 1.30 en unidades de mundo). pub const CLASSIC: Self = Self { - arm_extent: 1.0, - thickness: 0.18, + thickness: 0.13, + steps: 2, }; - pub const fn new(arm_extent: f32, thickness: f32) -> Self { - Self { - arm_extent, - thickness, - } + pub const fn new(thickness: f32, steps: u32) -> Self { + Self { thickness, steps } } - /// Las cuatro puntas cardinales en orden `[N, E, S, W]`. - /// Coordenadas listas para anclar UI sobre la chacana. + /// Semi-lado del cuadrado central — la parte **más ancha** de la chacana. + pub fn center_half(&self) -> f32 { + (self.steps as f32 + 1.0) * self.thickness + } + + /// Distancia desde el centro a la punta más externa. + pub fn arm_extent(&self) -> f32 { + self.center_half() + self.steps as f32 * self.thickness + } + + /// Las cuatro puntas cardinales `[N, E, S, W]`. pub fn tips(&self) -> [(f32, f32); 4] { - let l = self.arm_extent; + let l = self.arm_extent(); [(0.0, l), (l, 0.0), (0.0, -l), (-l, 0.0)] } - /// Bounding box axis-aligned `(min, max)`. pub fn aabb(&self) -> ((f32, f32), (f32, f32)) { - let l = self.arm_extent; + let l = self.arm_extent(); ((-l, -l), (l, l)) } - /// Perímetro cerrado en orden horario: 20 vértices, listo para `LINE_LOOP`. - pub fn perimeter(&self) -> Vec<(f32, f32)> { - let s = self.thickness; - let l = self.arm_extent; - let s2 = s * 2.0; - vec![ - (s, l), - (s, s2), - (s2, s2), - (s2, s), - (l, s), - (l, -s), - (s2, -s), - (s2, -s2), - (s, -s2), - (s, -l), - (-s, -l), - (-s, -s2), - (-s2, -s2), - (-s2, -s), - (-l, -s), - (-l, s), - (-s2, s), - (-s2, s2), - (-s, s2), - (-s, l), - ] - } - - /// Triangulación: 9 rectángulos (1 centro + 4 escalones + 4 puntas) = 54 vértices. - /// Listo para `GL_TRIANGLES`. + /// Triangulación: `1 + 4 * steps` rectángulos en `GL_TRIANGLES`. + /// Para `steps = 2`: 9 rects = 54 vértices. pub fn triangles(&self) -> Vec<(f32, f32)> { let s = self.thickness; - let l = self.arm_extent; - let s2 = s * 2.0; - let mut tri = Vec::with_capacity(9 * 6); + let c = self.center_half(); + let mut tri = Vec::with_capacity(6 * (1 + 4 * self.steps as usize)); let mut rect = |x0: f32, y0: f32, x1: f32, y1: f32| { tri.push((x0, y0)); tri.push((x1, y0)); @@ -88,23 +69,22 @@ impl ChacanaSpec { tri.push((x1, y1)); tri.push((x0, y1)); }; - // Cuadrado central - rect(-s, -s, s, s); - // Escalones (un rect 4s × s por brazo) - rect(-s2, s, s2, s2); // N - rect(-s2, -s2, s2, -s); // S - rect(s, -s2, s2, s2); // E - rect(-s2, -s2, -s, s2); // W - // Puntas (un rect 2s × (l - 2s) por brazo) - rect(-s, s2, s, l); // N - rect(-s, -l, s, -s2); // S - rect(s2, -s, l, s); // E - rect(-l, -s, -s2, s); // W + rect(-c, -c, c, c); + for k in 1..=self.steps { + // El k-ésimo nivel (1 = más cerca del centro, steps = punta) + // adelgaza a (steps - k + 1) * thickness de semi-ancho. + let hw = (self.steps - k + 1) as f32 * s; + let inner = c + (k - 1) as f32 * s; + let outer = c + k as f32 * s; + rect(-hw, inner, hw, outer); // N + rect(-hw, -outer, hw, -inner); // S + rect(inner, -hw, outer, hw); // E + rect(-outer, -hw, -inner, hw); // W + } tri } - /// Para un punto cualquiera, devuelve la punta más cercana y su distancia. - /// Útil para snapping de interacción. + /// Para un punto cualquiera, devuelve la punta más cercana y la distancia. pub fn closest_tip(&self, p: (f32, f32)) -> ((f32, f32), f32) { let tips = self.tips(); let mut best = (tips[0], f32::INFINITY); @@ -125,35 +105,55 @@ mod tests { use super::*; #[test] - fn perimeter_has_20_vertices() { - assert_eq!(ChacanaSpec::CLASSIC.perimeter().len(), 20); + fn classic_is_two_step_chacana() { + let c = ChacanaSpec::CLASSIC; + assert_eq!(c.steps, 2); + // center_half = 3 * 0.13 = 0.39; arm_extent = 0.65. + assert!((c.center_half() - 0.39).abs() < 1e-6); + assert!((c.arm_extent() - 0.65).abs() < 1e-6); } #[test] - fn triangles_form_9_rectangles() { - assert_eq!(ChacanaSpec::CLASSIC.triangles().len(), 9 * 6); + fn arm_extent_grows_with_steps() { + let c1 = ChacanaSpec::new(0.1, 1); + let c2 = ChacanaSpec::new(0.1, 2); + let c3 = ChacanaSpec::new(0.1, 3); + assert!(c1.arm_extent() < c2.arm_extent()); + assert!(c2.arm_extent() < c3.arm_extent()); + } + + #[test] + fn triangles_one_rect_plus_four_per_step() { + let c1 = ChacanaSpec::new(0.1, 1); + assert_eq!(c1.triangles().len(), 6 * (1 + 4 * 1)); + let c2 = ChacanaSpec::CLASSIC; + assert_eq!(c2.triangles().len(), 6 * (1 + 4 * 2)); + let c3 = ChacanaSpec::new(0.1, 3); + assert_eq!(c3.triangles().len(), 6 * (1 + 4 * 3)); } #[test] fn tips_match_cardinals() { - let c = ChacanaSpec::new(2.0, 0.3); + let c = ChacanaSpec::CLASSIC; + let l = c.arm_extent(); let tips = c.tips(); - assert_eq!(tips[0], (0.0, 2.0)); // N - assert_eq!(tips[1], (2.0, 0.0)); // E - assert_eq!(tips[2], (0.0, -2.0)); // S - assert_eq!(tips[3], (-2.0, 0.0)); // W + assert_eq!(tips[0], (0.0, l)); // N + assert_eq!(tips[1], (l, 0.0)); // E + assert_eq!(tips[2], (0.0, -l)); // S + assert_eq!(tips[3], (-l, 0.0)); // W } #[test] - fn closest_tip_to_upper_left_is_north() { + fn closest_tip_to_upper_point_is_north() { let c = ChacanaSpec::CLASSIC; - let (tip, _d) = c.closest_tip((-0.1, 0.95)); - assert_eq!(tip, (0.0, 1.0)); + let (tip, _d) = c.closest_tip((-0.1, 0.55)); + assert_eq!(tip, (0.0, c.arm_extent())); } #[test] fn aabb_matches_extent() { - let c = ChacanaSpec::new(1.5, 0.2); - assert_eq!(c.aabb(), ((-1.5, -1.5), (1.5, 1.5))); + let c = ChacanaSpec::new(0.12, 2); + let l = c.arm_extent(); + assert_eq!(c.aabb(), ((-l, -l), (l, l))); } } diff --git a/crates/modules/gioser/gioser-shaders/src/lib.rs b/crates/modules/gioser/gioser-shaders/src/lib.rs index d724be9..acf0994 100644 --- a/crates/modules/gioser/gioser-shaders/src/lib.rs +++ b/crates/modules/gioser/gioser-shaders/src/lib.rs @@ -20,9 +20,9 @@ void main() { } "; -/// Fragment del fondo cósmico: FBM en 3 capas + estrellas + viñeta. -/// Uniforms esperados: `u_resolution`, `u_time`, `u_parallax`, -/// `u_void`, `u_nebula_a`, `u_nebula_b`, `u_stardust`. +/// Fragment del fondo cósmico: nubes FBM en 3 capas, 3 estratos de +/// estrellas con titilación independiente, viñeta, y 4 meteoros +/// procedurales que cruzan el cielo periódicamente. pub const FS_COSMOS: &str = "#version 300 es precision highp float; in vec2 v_clip; @@ -39,6 +39,9 @@ uniform vec3 u_stardust; float hash21(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float hash11(float n) { + return fract(sin(n * 78.233) * 43758.5453); +} float vnoise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); @@ -54,45 +57,106 @@ float fbm(vec2 p) { float a = 0.55; for (int i = 0; i < 5; i++) { v += a * vnoise(p); - p *= 2.07; + p = p * 2.07 + vec2(11.3, 7.7); a *= 0.55; } return v; } + +// Meteoro procedural: trazo brillante con cola, vida 1.6s, respawnea solo. +float meteor(vec2 uv, float seed) { + float period = 6.5 + 4.0 * hash11(seed * 17.0); + float t_seeded = u_time + seed * 19.0; + float phase = mod(t_seeded, period); + float life = 1.6; + if (phase > life) return 0.0; + float t = phase / life; + + float epoch = floor(t_seeded / period); + vec2 origin = vec2( + hash21(vec2(seed, epoch)) * 2.6 - 1.3, + 0.55 + hash21(vec2(seed + 5.0, epoch)) * 0.55 + ); + vec2 dir = normalize(vec2( + hash21(vec2(seed + 1.0, epoch)) * 1.6 - 0.8, + -0.7 - hash21(vec2(seed + 2.0, epoch)) * 0.6 + )); + + vec2 head = origin + dir * t * 2.1; + vec2 tail = head - dir * 0.24; + + vec2 pa = uv - tail; + vec2 ba = head - tail; + float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0); + float dist = length(pa - ba * h); + + float perpGlow = exp(-dist * 420.0); + float trailFalloff = smoothstep(0.0, 1.0, h); + float headPulse = exp(-dist * 900.0); + float lifeFade = sin(t * 3.14159); + + return (perpGlow * trailFalloff + headPulse * 1.4) * lifeFade; +} + void main() { float aspect = u_resolution.x / max(u_resolution.y, 1.0); vec2 uv = v_clip; uv.x *= aspect; - vec2 d1 = vec2( u_time * 0.010, u_time * 0.004) + u_parallax * 0.08; - vec2 d2 = vec2(-u_time * 0.016, u_time * 0.011) + u_parallax * 0.18; - vec2 d3 = vec2( u_time * 0.024, -u_time * 0.019) + u_parallax * 0.34; + // === NUBES (drift visible, 5× más rápido que la versión anterior) === + vec2 d1 = vec2( u_time * 0.055, u_time * 0.022) + u_parallax * 0.10; + vec2 d2 = vec2(-u_time * 0.085, u_time * 0.058) + u_parallax * 0.22; + vec2 d3 = vec2( u_time * 0.130, -u_time * 0.095) + u_parallax * 0.40; - float n1 = fbm(uv * 0.9 + d1); - float n2 = fbm(uv * 2.1 + d2); - float n3 = fbm(uv * 4.5 + d3); + float n1 = fbm(uv * 0.85 + d1); + float n2 = fbm(uv * 2.05 + d2); + float n3 = fbm(uv * 4.40 + d3); vec3 color = u_void; - color = mix(color, u_nebula_a, pow(n1, 1.6) * 0.70); - color = mix(color, u_nebula_b, pow(n2, 2.0) * 0.55); - color += u_nebula_a * pow(n3, 3.2) * 0.22; + color = mix(color, u_nebula_a, pow(n1, 1.5) * 0.80); + color = mix(color, u_nebula_b, pow(n2, 1.85) * 0.62); + color += u_nebula_a * pow(n3, 3.0) * 0.28; + // Viñeta radial. float r = length(v_clip); - color *= 1.0 - smoothstep(0.55, 1.35, r) * 0.85; + color *= 1.0 - smoothstep(0.55, 1.40, r) * 0.85; - // Estrellas brillantes (pocas, titilan). - vec2 sgrid = uv * 90.0; - vec2 sid = floor(sgrid); - float sh = hash21(sid); - float twinkle = 0.4 + 0.6 * sin(u_time * 1.7 + sh * 28.0); - float starMask = smoothstep(0.997, 0.9985, sh); - color += u_stardust * starMask * twinkle * 0.95; + // === ESTRELLAS — 3 estratos con titilación distinta === + // Brillantes, pocas, titilan rápido. + { + vec2 sgrid = uv * 75.0; + vec2 sid = floor(sgrid); + float sh = hash21(sid); + float tw = 0.45 + 0.55 * sin(u_time * 2.6 + sh * 41.0); + float mask = smoothstep(0.9935, 0.999, sh); + color += u_stardust * mask * tw * 1.15; + } + // Medianas, densas, titilan lento. + { + vec2 sgrid = uv * 135.0 + vec2(7.0, 11.0); + vec2 sid = floor(sgrid); + float sh = hash21(sid); + float tw = 0.55 + 0.45 * sin(u_time * 1.1 + sh * 28.0); + float mask = smoothstep(0.987, 0.994, sh); + color += u_stardust * mask * tw * 0.75; + } + // Polvo de fondo, muchas, casi sin twinkle. + { + vec2 sgrid = uv * 260.0 + vec2(13.0, 3.0); + vec2 sid = floor(sgrid); + float sh = hash21(sid); + float tw = 0.7 + 0.3 * sin(u_time * 0.5 + sh * 15.0); + float mask = smoothstep(0.982, 0.989, sh); + color += u_stardust * mask * tw * 0.40; + } - // Polvo (muchas, débiles). - vec2 dgrid = uv * 220.0; - float dh = hash21(floor(dgrid)); - float dustMask = smoothstep(0.985, 0.992, dh); - color += u_stardust * dustMask * 0.25; + // === METEOROS (4 procedurales, respawn independiente) === + float meteors = 0.0; + meteors += meteor(uv, 0.31); + meteors += meteor(uv, 1.73); + meteors += meteor(uv, 4.29); + meteors += meteor(uv, 7.11); + color += vec3(1.0, 0.94, 0.78) * meteors * 1.1; fragColor = vec4(color, 1.0); } @@ -110,8 +174,9 @@ void main() { } "; -/// Fragment de la chacana: SDF de la cruz escalonada + glow + aro + sol pulsante. -/// Uniforms: `u_time`, `u_thickness`, `u_arm_extent`, +/// Fragment de la chacana mística: SDF de 2 escalones por brazo, +/// líneas glow + aro + rayos zodiacales + sol central pulsante. +/// Uniforms: `u_time`, `u_thickness` (s), `u_center_half` (c), `u_arm_extent`, /// `u_line_color`, `u_rim_color`, `u_sun_color`, `u_sun_pulse`. pub const FS_CHACANA: &str = "#version 300 es precision highp float; @@ -119,6 +184,7 @@ in vec2 v_world; out vec4 fragColor; uniform float u_time; uniform float u_thickness; +uniform float u_center_half; uniform float u_arm_extent; uniform vec3 u_line_color; uniform vec3 u_rim_color; @@ -129,65 +195,90 @@ float sdBox(vec2 p, vec2 b) { vec2 d = abs(p) - b; return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); } -float sdChacana(vec2 p, float s, float L) { - float s2 = s * 2.0; - float halfArm = max((L - s2) * 0.5, 0.0); - float armOff = s2 + halfArm; - float d = sdBox(p, vec2(s, s)); - d = min(d, sdBox(p - vec2(0.0, 1.5 * s), vec2(s2, 0.5 * s))); - d = min(d, sdBox(p - vec2(0.0, -1.5 * s), vec2(s2, 0.5 * s))); - d = min(d, sdBox(p - vec2( 1.5 * s, 0.0), vec2(0.5 * s, s2))); - d = min(d, sdBox(p - vec2(-1.5 * s, 0.0), vec2(0.5 * s, s2))); - d = min(d, sdBox(p - vec2(0.0, armOff), vec2(s, halfArm))); - d = min(d, sdBox(p - vec2(0.0, -armOff), vec2(s, halfArm))); - d = min(d, sdBox(p - vec2( armOff, 0.0), vec2(halfArm, s))); - d = min(d, sdBox(p - vec2(-armOff, 0.0), vec2(halfArm, s))); + +// Chacana de 2 escalones (mística clásica): centro 2c×2c + 4 brazos +// con 2 niveles. Inner level half-width = 2s, outer (tip) = s. +float sdChacana(vec2 p, float s, float c) { + float d = sdBox(p, vec2(c, c)); + float hd = 0.5 * s; + // Nivel interno (más ancho, pegado al centro). + float mid1 = c + 0.5 * s; + float hw1 = 2.0 * s; + d = min(d, sdBox(p - vec2(0.0, mid1), vec2(hw1, hd))); // N + d = min(d, sdBox(p - vec2(0.0, -mid1), vec2(hw1, hd))); // S + d = min(d, sdBox(p - vec2( mid1, 0.0), vec2(hd, hw1))); // E + d = min(d, sdBox(p - vec2(-mid1, 0.0), vec2(hd, hw1))); // W + // Punta (más angosta, externa). + float mid2 = c + 1.5 * s; + float hw2 = 1.0 * s; + d = min(d, sdBox(p - vec2(0.0, mid2), vec2(hw2, hd))); + d = min(d, sdBox(p - vec2(0.0, -mid2), vec2(hw2, hd))); + d = min(d, sdBox(p - vec2( mid2, 0.0), vec2(hd, hw2))); + d = min(d, sdBox(p - vec2(-mid2, 0.0), vec2(hd, hw2))); return d; } void main() { vec2 p = v_world; - float d = sdChacana(p, u_thickness, u_arm_extent); + float d = sdChacana(p, u_thickness, u_center_half); + float r = length(p); - // Línea: gaussiana alrededor del borde. - float lineW = 0.013; + // Línea principal: gaussiana sobre el borde de la chacana. + float lineW = 0.011; float line = exp(-(d * d) / (2.0 * lineW * lineW)); - // Glow exterior cae más suave. - float glow = exp(-max(d, 0.0) * 7.5) * 0.55; + // Glow exterior cae suave hacia el infinito. + float glow = exp(-max(d, 0.0) * 8.0) * 0.55; - // Fill interior tenue (ligera niebla cyan dentro). + // Fill interior, una niebla cyan muy tenue. float fill = smoothstep(0.0, -0.025, d); - // Aro exterior: gran círculo que envuelve la chacana. - float ringR = u_arm_extent * 1.18; - float ringD = abs(length(p) - ringR); - float ringW = 0.008; - float ring = exp(-(ringD * ringD) / (2.0 * ringW * ringW)) * 0.75; + // Aro circular que envuelve la chacana (rasgo del logo). + float ringR_outer = u_arm_extent * 1.32; + float ringD_outer = abs(r - ringR_outer); + float ring_outer = exp(-(ringD_outer * ringD_outer) / (2.0 * 0.008 * 0.008)) * 0.80; - // Rayos sutiles (12 divisiones del círculo, como husillos del calendario). + // Aro interior fino (segundo orbital). + float ringR_inner = u_arm_extent * 1.18; + float ringD_inner = abs(r - ringR_inner); + float ring_inner = exp(-(ringD_inner * ringD_inner) / (2.0 * 0.0035 * 0.0035)) * 0.42; + + // Ventana radial entre arm_extent y el aro exterior — para rayos y muescas. float ang = atan(p.y, p.x); - float rays = pow(abs(cos(ang * 6.0)), 80.0) - * smoothstep(u_arm_extent * 1.05, ringR * 0.97, length(p)) - * (0.18 + 0.10 * sin(u_time * 0.6)); + float band = smoothstep(u_arm_extent * 1.00, u_arm_extent * 1.10, r) + * (1.0 - smoothstep(ringR_outer * 0.92, ringR_outer * 1.00, r)); + + // Rayos: 12 divisiones (meses andinos / horas), modulados en el tiempo. + float rays = pow(abs(cos(ang * 6.0)), 24.0) * band + * (0.55 + 0.45 * sin(u_time * 0.7)); + + // Marcas cardinales (4 muescas finas) — exponente alto = picos angostos. + float card = pow(abs(cos(ang * 2.0)), 120.0) * band * 1.10; // Sol central: gauss tight + corona suave + pulso. - float sunR = u_thickness * 0.55; - float sunDist = length(p); + float sunR = u_thickness * 0.50; + float sunDist = r; float sun = exp(-(sunDist * sunDist) / (2.0 * sunR * sunR)); - float corR = sunR * 4.5; - float corona = exp(-(sunDist * sunDist) / (2.0 * corR * corR)) * 0.45; - float sunMix = sun + corona * (0.75 + 0.25 * u_sun_pulse); + float corR = sunR * 5.0; + float corona = exp(-(sunDist * sunDist) / (2.0 * corR * corR)) * 0.50; + float sunMix = sun * (1.0 + 0.2 * u_sun_pulse) + corona * (0.7 + 0.3 * u_sun_pulse); + + // Halo del centro: cuadrado oscuro detrás de la chacana para profundidad. + float coreShadow = smoothstep(u_center_half * 0.95, u_center_half * 0.3, max(abs(p.x), abs(p.y))) * 0.20; vec3 col = vec3(0.0); - col += u_line_color * line * 1.45; + col += u_line_color * line * 1.55; col += u_rim_color * glow * 1.05; - col += u_line_color * ring * 0.95; - col += u_rim_color * rays * 1.40; - col += u_sun_color * sunMix * 1.35; - col += vec3(0.04, 0.06, 0.12) * fill * 0.55; + col += u_line_color * ring_outer * 1.00; + col += u_rim_color * ring_inner * 1.15; + col += u_rim_color * rays * 1.20; + col += u_line_color * card * 1.30; + col += u_sun_color * sunMix * 1.45; + col += vec3(0.05, 0.08, 0.14) * (fill + coreShadow) * 0.6; - float alpha = clamp(line * 1.2 + glow + ring + rays + sunMix + fill * 0.5, 0.0, 1.0); + float alpha = clamp( + line * 1.2 + glow + ring_outer + ring_inner + rays + card + sunMix + fill * 0.5, + 0.0, 1.0); fragColor = vec4(col, alpha); } "; @@ -197,8 +288,10 @@ pub const FULLSCREEN_QUAD: [f32; 12] = [ -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, ]; -/// Quad ligeramente mayor que la chacana para no recortar el glow ni el aro. +/// Quad ligeramente mayor que la chacana para no recortar aros ni glow. +/// `arm_extent` es la distancia centro→punta; multiplicamos por un factor +/// que cubre el aro exterior (1.32×) más halo. pub fn chacana_quad(arm_extent: f32) -> [f32; 12] { - let e = arm_extent * 1.45; + let e = arm_extent * 1.65; [-e, -e, e, -e, e, e, -e, -e, e, e, -e, e] }