diff --git a/Cargo.lock b/Cargo.lock index a0f255d..4cb9aed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3914,6 +3914,46 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "gioser-canvas-web" +version = "0.1.0" +dependencies = [ + "gioser-geom", + "gioser-palette", + "gioser-physics", + "gioser-shaders", + "glam", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gioser-geom" +version = "0.1.0" + +[[package]] +name = "gioser-palette" +version = "0.1.0" + +[[package]] +name = "gioser-physics" +version = "0.1.0" + +[[package]] +name = "gioser-shaders" +version = "0.1.0" + +[[package]] +name = "gioser-web" +version = "0.1.0" +dependencies = [ + "gioser-canvas-web", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "git2" version = "0.20.4" @@ -3927,6 +3967,12 @@ dependencies = [ "url", ] +[[package]] +name = "glam" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" + [[package]] name = "glob" version = "0.3.3" diff --git a/Cargo.toml b/Cargo.toml index 454787c..b85535f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,15 @@ members = [ "crates/modules/shipote/shipote-discern", "crates/modules/shipote/shipote-core", + # ============================================================ + # modules/gioser/ — landing WASM (chacana + 4 elementos) + # ============================================================ + "crates/modules/gioser/gioser-geom", + "crates/modules/gioser/gioser-physics", + "crates/modules/gioser/gioser-palette", + "crates/modules/gioser/gioser-shaders", + "crates/modules/gioser/gioser-canvas-web", + # ============================================================ # apps/ — apps que consumen el protocolo (yahweh modules+shell) # ============================================================ @@ -111,6 +120,7 @@ members = [ "crates/apps/shipote-cli", "crates/apps/shipote-gateway", "crates/apps/shipote-shell", + "crates/apps/gioser-web", ] [workspace.package] @@ -200,6 +210,12 @@ gpui = "0.2" # === Filesystem helpers === directories = "5" +# === WASM web (gioser) === +wasm-bindgen = "0.2" +js-sys = "0.3" +web-sys = "0.3" +glam = "0.30" + # ============================================================ # Intra-workspace deps de yahweh (referenciadas por workspace = true) # ============================================================ diff --git a/ChatGPT Image May 12, 2026, 02_15_08 PM (1).png b/ChatGPT Image May 12, 2026, 02_15_08 PM (1).png new file mode 100644 index 0000000..57ab7ff Binary files /dev/null and b/ChatGPT Image May 12, 2026, 02_15_08 PM (1).png differ diff --git a/ChatGPT Image May 12, 2026, 02_15_09 PM (2).png b/ChatGPT Image May 12, 2026, 02_15_09 PM (2).png new file mode 100644 index 0000000..561a617 Binary files /dev/null and b/ChatGPT Image May 12, 2026, 02_15_09 PM (2).png differ diff --git a/ChatGPT Image May 12, 2026, 02_26_38 PM.png b/ChatGPT Image May 12, 2026, 02_26_38 PM.png new file mode 100644 index 0000000..c346ba3 Binary files /dev/null and b/ChatGPT Image May 12, 2026, 02_26_38 PM.png differ diff --git a/c5b5c86a-9e98-406d-b664-7e79bf031940.png b/c5b5c86a-9e98-406d-b664-7e79bf031940.png new file mode 100644 index 0000000..57ab7ff Binary files /dev/null and b/c5b5c86a-9e98-406d-b664-7e79bf031940.png differ diff --git a/crates/apps/gioser-web/Cargo.toml b/crates/apps/gioser-web/Cargo.toml new file mode 100644 index 0000000..0b2f3d9 --- /dev/null +++ b/crates/apps/gioser-web/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "gioser-web" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +gioser-canvas-web = { path = "../../modules/gioser/gioser-canvas-web" } +wasm-bindgen.workspace = true +js-sys.workspace = true + +[dependencies.web-sys] +workspace = true +features = [ + "Window", + "Document", + "Element", + "HtmlElement", + "HtmlCanvasElement", + "CssStyleDeclaration", + "MouseEvent", + "EventTarget", + "Performance", + "console", +] + diff --git a/crates/apps/gioser-web/README.md b/crates/apps/gioser-web/README.md new file mode 100644 index 0000000..5f39900 --- /dev/null +++ b/crates/apps/gioser-web/README.md @@ -0,0 +1,77 @@ +# gioser-web + +Landing page de **GioSer · En el centro, el ser**: chacana animada con +shaders WebGL2, nebulosa procedural y cuatro botones cardinales +(AIRE · FUEGO · TIERRA · AGUA). + +## Arquitectura + +``` +crates/modules/gioser/ +├── gioser-geom/ geometría agnóstica de la chacana (20 vértices) +├── gioser-physics/ resorte-amortiguador N-dim crítico-amortiguado +├── gioser-palette/ 4 elementos + cosmos en RGB lineal +├── gioser-shaders/ sources GLSL ES 3.00 (FBM cósmico + SDF chacana) +└── gioser-canvas-web/ renderer WebGL2 que compone todo + +crates/apps/gioser-web/ cdylib WASM + index.html + styles.css +``` + +Los cuatro primeros son agnósticos del runtime (compilan en cualquier +target). `gioser-canvas-web` agrega la dependencia de WebGL2 / web-sys. +Cuando exista `yahweh-web`, los agnósticos siguen tal cual y el renderer +se enchufa al runtime equivalente a `yahweh_launcher::launch_app`. + +## Cómo se ve + +- **Fondo:** vacío violeta-noche con tres capas de FBM (5 octavas) en + parallax con el mouse + estrellas titilantes + viñeta radial. +- **Chacana:** SDF de la cruz escalonada con outline gaussiano cyan, + glow ámbar exterior, aro circular envolvente y rayos sutiles + (calendario andino). +- **Sol central:** gauss + corona, late con sin(t). +- **Tilt físico:** spring-damper sub-crítico (ζ=0.72, 2.2 Hz) que apunta + hacia el mouse — overshoot suave, settle de ~600 ms. +- **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`. + +```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/ +``` + +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. + +## Routing + +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: + +```sh +cargo test -p gioser-geom -p gioser-physics -p gioser-palette +``` diff --git a/crates/apps/gioser-web/index.html b/crates/apps/gioser-web/index.html new file mode 100644 index 0000000..abb54b9 --- /dev/null +++ b/crates/apps/gioser-web/index.html @@ -0,0 +1,75 @@ + + + + + + GioSer · En el centro, el ser + + + + + + + + +
+

Gio·Ser

+

EN EL CENTRO · EL SER

+
+ +
+ + + + + + + AIRE + Software · IA + + + + + + + + FUEGO + Inspiración + + + + + + + + TIERRA + Cuerpo + + + + + + + + + AGUA + Espiritualidad + +
+ + + + + + diff --git a/crates/apps/gioser-web/src/lib.rs b/crates/apps/gioser-web/src/lib.rs new file mode 100644 index 0000000..034f554 --- /dev/null +++ b/crates/apps/gioser-web/src/lib.rs @@ -0,0 +1,149 @@ +//! Entrypoint WASM de la landing GioSer. +//! +//! Monta el canvas, instala listeners de mouse y resize, corre el loop +//! con `requestAnimationFrame`, y reposiciona los 4 botones DOM +//! `#tip-{aire|fuego|tierra|agua}` sobre las puntas proyectadas +//! de la chacana cada frame. + +use std::cell::RefCell; +use std::rc::Rc; + +use gioser_canvas_web::{tips, Renderer}; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use web_sys::{Document, HtmlCanvasElement, HtmlElement, MouseEvent, Window}; + +#[wasm_bindgen(start)] +pub fn boot() -> Result<(), JsValue> { + install_panic_hook(); + let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; + let document = window + .document() + .ok_or_else(|| JsValue::from_str("no document"))?; + + let canvas: HtmlCanvasElement = document + .get_element_by_id("gioser-canvas") + .ok_or_else(|| JsValue::from_str("no canvas#gioser-canvas"))? + .dyn_into()?; + + fit_canvas(&canvas, &window); + let renderer = Rc::new(RefCell::new(Renderer::new(&canvas)?)); + renderer + .borrow_mut() + .resize(canvas.width(), canvas.height()); + + install_resize(&window, &canvas, &renderer)?; + install_mouse(&document, &canvas, &renderer)?; + install_raf(&window, &document, &canvas, &renderer); + + Ok(()) +} + +fn install_resize( + window: &Window, + canvas: &HtmlCanvasElement, + renderer: &Rc>, +) -> Result<(), JsValue> { + let canvas = canvas.clone(); + let win2 = window.clone(); + let r = renderer.clone(); + let cb = Closure::::new(move || { + fit_canvas(&canvas, &win2); + r.borrow_mut().resize(canvas.width(), canvas.height()); + }); + window.add_event_listener_with_callback("resize", cb.as_ref().unchecked_ref())?; + cb.forget(); + Ok(()) +} + +fn install_mouse( + document: &Document, + canvas: &HtmlCanvasElement, + renderer: &Rc>, +) -> Result<(), JsValue> { + let canvas = canvas.clone(); + let r = renderer.clone(); + let cb = Closure::::new(move |e: MouseEvent| { + let w = canvas.client_width().max(1) as f32; + let h = canvas.client_height().max(1) as f32; + // Origen en el centro del canvas, +y arriba. + let x = e.client_x() as f32 - w * 0.5; + let y = h * 0.5 - e.client_y() as f32; + r.borrow_mut().set_mouse_px(x, y); + }); + document.add_event_listener_with_callback("mousemove", cb.as_ref().unchecked_ref())?; + cb.forget(); + Ok(()) +} + +fn install_raf( + window: &Window, + document: &Document, + canvas: &HtmlCanvasElement, + renderer: &Rc>, +) { + let f = Rc::new(RefCell::new(None::>)); + let g = f.clone(); + let renderer = renderer.clone(); + let canvas = canvas.clone(); + let document = document.clone(); + let window2 = window.clone(); + *g.borrow_mut() = Some(Closure::::new(move |time_ms: f64| { + renderer.borrow_mut().render(time_ms); + position_tips(&document, &canvas, &renderer.borrow()); + if let Some(cb) = f.borrow().as_ref() { + let _ = window2.request_animation_frame(cb.as_ref().unchecked_ref()); + } + })); + let _ = window.request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref()); +} + +fn fit_canvas(canvas: &HtmlCanvasElement, window: &Window) { + let dpr = window.device_pixel_ratio() as f32; + let w = window + .inner_width() + .ok() + .and_then(|v| v.as_f64()) + .unwrap_or(1280.0) as f32; + let h = window + .inner_height() + .ok() + .and_then(|v| v.as_f64()) + .unwrap_or(720.0) as f32; + canvas.set_width((w * dpr) as u32); + canvas.set_height((h * dpr) as u32); + let el: &HtmlElement = canvas.unchecked_ref(); + let style = el.style(); + let _ = style.set_property("width", &format!("{}px", w)); + let _ = style.set_property("height", &format!("{}px", h)); +} + +fn position_tips(document: &Document, canvas: &HtmlCanvasElement, renderer: &Renderer) { + let clips = renderer.tips_ndc(); + let cw = canvas.client_width().max(1) as f32; + let ch = canvas.client_height().max(1) as f32; + for (i, (id, _color, _label)) in tips::ORDER.iter().enumerate() { + let (nx, ny) = clips[i]; + let px = (nx + 1.0) * 0.5 * cw; + let py = (1.0 - (ny + 1.0) * 0.5) * ch; + let sel = format!("tip-{}", id); + if let Some(el) = document.get_element_by_id(&sel) { + if let Ok(el) = el.dyn_into::() { + let _ = el.style().set_property( + "transform", + &format!("translate({:.2}px, {:.2}px) translate(-50%, -50%)", px, py), + ); + } + } + } +} + +fn install_panic_hook() { + static SET: std::sync::Once = std::sync::Once::new(); + SET.call_once(|| { + std::panic::set_hook(Box::new(|info| { + let msg = format!("{}", info); + web_sys::console::error_1(&JsValue::from_str(&msg)); + })); + }); +} diff --git a/crates/apps/gioser-web/styles.css b/crates/apps/gioser-web/styles.css new file mode 100644 index 0000000..aaa9b5c --- /dev/null +++ b/crates/apps/gioser-web/styles.css @@ -0,0 +1,191 @@ +:root { + --bg: #06050d; + --fg: #e8eaf5; + --aire: #d0dbff; + --agua: #6cd0f3; + --fuego: #f59056; + --tierra: #d49873; + --rim: #d8a85d; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + background: var(--bg); + color: var(--fg); + font-family: 'Inter', system-ui, -apple-system, sans-serif; + font-weight: 300; + overflow: hidden; + cursor: default; +} + +#gioser-canvas { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + display: block; + z-index: 0; +} + +.brand { + position: fixed; + top: 4vh; + left: 50%; + transform: translateX(-50%); + text-align: center; + z-index: 10; + pointer-events: none; + user-select: none; +} + +.brand-title { + font-family: 'Cinzel', serif; + font-weight: 700; + font-size: clamp(2.4rem, 5vw, 4.4rem); + margin: 0; + letter-spacing: 0.08em; + color: var(--fg); + text-shadow: + 0 0 18px rgba(108, 208, 243, 0.35), + 0 0 40px rgba(216, 168, 93, 0.18); +} + +.brand-dot { + color: var(--rim); + margin: 0 0.05em; + text-shadow: 0 0 14px var(--rim), 0 0 28px rgba(245, 144, 86, 0.5); +} + +.brand-sub { + font-family: 'Inter', sans-serif; + font-weight: 300; + letter-spacing: 0.5em; + font-size: clamp(0.65rem, 0.9vw, 0.8rem); + margin: 0.55rem 0 0; + color: rgba(232, 234, 245, 0.6); + text-indent: 0.5em; +} + +#tips { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 5; +} + +.tip { + position: absolute; + top: 0; left: 0; + pointer-events: auto; + text-decoration: none; + color: var(--fg); + user-select: none; + + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + padding: 0.7rem 1.0rem 0.55rem; + min-width: 120px; + + background: rgba(8, 6, 22, 0.42); + backdrop-filter: blur(8px) saturate(120%); + -webkit-backdrop-filter: blur(8px) saturate(120%); + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.07); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03); + + transition: + box-shadow 260ms ease, + border-color 260ms ease, + background 260ms ease, + letter-spacing 260ms ease; +} + +.tip::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1px; + background: linear-gradient(135deg, + rgba(255,255,255,0.18), + rgba(255,255,255,0.0) 40%, + rgba(216, 168, 93, 0.18)); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + opacity: 0.7; +} + +.tip:hover { + border-color: rgba(255, 255, 255, 0.22); + background: rgba(20, 14, 40, 0.6); +} + +.tip-glyph { + width: 42px; + height: 42px; + color: currentColor; + filter: drop-shadow(0 0 6px currentColor); + transition: filter 240ms ease; +} +.tip:hover .tip-glyph { + filter: drop-shadow(0 0 12px currentColor) drop-shadow(0 0 20px currentColor); +} + +.tip-label { + font-family: 'Cinzel', serif; + font-size: 0.74rem; + letter-spacing: 0.36em; + font-weight: 600; + text-indent: 0.36em; +} + +.tip-sub { + font-family: 'Inter', sans-serif; + font-size: 0.62rem; + letter-spacing: 0.18em; + font-weight: 300; + color: rgba(232, 234, 245, 0.55); + text-transform: uppercase; +} + +.tip-aire { color: var(--aire); } +.tip-aire:hover { box-shadow: 0 0 32px rgba(208, 219, 255, 0.45); } +.tip-fuego { color: var(--fuego); } +.tip-fuego:hover { box-shadow: 0 0 32px rgba(245, 144, 86, 0.45); } +.tip-agua { color: var(--agua); } +.tip-agua:hover { box-shadow: 0 0 32px rgba(108, 208, 243, 0.45); } +.tip-tierra { color: var(--tierra); } +.tip-tierra:hover { box-shadow: 0 0 32px rgba(212, 152, 115, 0.45); } + +.quote { + position: fixed; + bottom: 3vh; + left: 50%; + transform: translateX(-50%); + font-family: 'Cinzel', serif; + font-size: 0.7rem; + letter-spacing: 0.55em; + color: rgba(232, 234, 245, 0.42); + pointer-events: none; + user-select: none; + z-index: 10; + text-indent: 0.55em; +} + +@media (max-width: 720px) { + .tip { min-width: 90px; padding: 0.5rem 0.7rem; } + .tip-glyph { width: 32px; height: 32px; } + .tip-label { font-size: 0.6rem; } + .tip-sub { display: none; } +} diff --git a/crates/modules/gioser/gioser-canvas-web/Cargo.toml b/crates/modules/gioser/gioser-canvas-web/Cargo.toml new file mode 100644 index 0000000..7a16819 --- /dev/null +++ b/crates/modules/gioser/gioser-canvas-web/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "gioser-canvas-web" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true + +[dependencies] +gioser-geom = { path = "../gioser-geom" } +gioser-physics = { path = "../gioser-physics" } +gioser-palette = { path = "../gioser-palette" } +gioser-shaders = { path = "../gioser-shaders" } +wasm-bindgen.workspace = true +js-sys.workspace = true +glam.workspace = true + +[dependencies.web-sys] +workspace = true +features = [ + "Window", + "Document", + "HtmlCanvasElement", + "WebGl2RenderingContext", + "WebGlShader", + "WebGlProgram", + "WebGlBuffer", + "WebGlUniformLocation", + "WebGlVertexArrayObject", + "console", +] + diff --git a/crates/modules/gioser/gioser-canvas-web/src/lib.rs b/crates/modules/gioser/gioser-canvas-web/src/lib.rs new file mode 100644 index 0000000..19af3a2 --- /dev/null +++ b/crates/modules/gioser/gioser-canvas-web/src/lib.rs @@ -0,0 +1,335 @@ +//! Renderer WebGL2 que compone geometría + física + paleta + shaders en pantalla. +//! +//! Es agnóstico del DOM: el caller monta el ``, pasa eventos de mouse, +//! y llama `render(time_ms)` desde un `requestAnimationFrame`. +//! +//! ```ignore +//! let mut r = Renderer::new(&canvas)?; +//! r.resize(w, h); +//! // por cada mousemove: +//! r.set_mouse_px(dx, dy); +//! // por cada frame: +//! r.render(time_ms); +//! ``` +//! +//! El layout sigue el patrón de `yahweh_launcher::launch_app(title, size, factory)`: +//! una sola línea para arrancar, escena auto-contenida. Cuando exista `yahweh-web`, +//! este renderer es el "factory" que recibirá. + +use gioser_geom::ChacanaSpec; +use gioser_palette::{cosmos, Rgb}; +use gioser_physics::SpringDamper2; +use gioser_shaders::{ + chacana_quad, FS_CHACANA, FS_COSMOS, FULLSCREEN_QUAD, VS_CHACANA, VS_FULLSCREEN, +}; +use glam::{Mat4, Vec3, Vec4}; +use std::collections::HashMap; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{ + HtmlCanvasElement, WebGl2RenderingContext as GL, WebGlProgram, WebGlShader, + WebGlUniformLocation, WebGlVertexArrayObject, +}; + +const RAD: f32 = core::f32::consts::PI / 180.0; +const MAX_TILT_DEG: f32 = 22.0; + +/// 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 + ("tierra", elements::TIERRA, "TIERRA"), // S + ("agua", elements::AGUA, "AGUA"), // W + ]; +} + +pub struct Renderer { + gl: GL, + cosmos_prog: Program, + chacana_prog: Program, + cosmos_vao: WebGlVertexArrayObject, + chacana_vao: WebGlVertexArrayObject, + chacana_quad_count: i32, + chacana: ChacanaSpec, + tilt: SpringDamper2, + sun_pulse: f32, + last_time_ms: f64, + viewport: (u32, u32), + /// Mouse en clip-space, x ∈ [-aspect, aspect], y ∈ [-1, 1]. + mouse: (f32, f32), +} + +struct Program { + program: WebGlProgram, + uniforms: HashMap<&'static str, WebGlUniformLocation>, +} + +impl Program { + fn new(gl: &GL, vs: &str, fs: &str, names: &[&'static str]) -> Result { + let vs = compile_shader(gl, GL::VERTEX_SHADER, vs)?; + let fs = compile_shader(gl, GL::FRAGMENT_SHADER, fs)?; + 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 + .get_program_parameter(&program, GL::LINK_STATUS) + .as_bool() + .unwrap_or(false); + if !linked { + return Err(gl + .get_program_info_log(&program) + .unwrap_or_else(|| "link failed".into())); + } + let mut uniforms = HashMap::new(); + for n in names { + if let Some(loc) = gl.get_uniform_location(&program, n) { + uniforms.insert(*n, loc); + } + } + Ok(Self { program, uniforms }) + } + + fn u(&self, name: &'static str) -> Option<&WebGlUniformLocation> { + self.uniforms.get(name) + } +} + +fn compile_shader(gl: &GL, ty: u32, src: &str) -> Result { + let s = gl.create_shader(ty).ok_or("create_shader failed")?; + gl.shader_source(&s, src); + gl.compile_shader(&s); + let ok = gl + .get_shader_parameter(&s, GL::COMPILE_STATUS) + .as_bool() + .unwrap_or(false); + if !ok { + return Err(gl + .get_shader_info_log(&s) + .unwrap_or_else(|| "compile failed".into())); + } + Ok(s) +} + +fn upload_quad( + gl: &GL, + verts: &[f32], + attr_loc: u32, +) -> Result<(WebGlVertexArrayObject, i32), String> { + let vao = gl + .create_vertex_array() + .ok_or("create_vertex_array failed")?; + 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). + unsafe { + let view = js_sys::Float32Array::view(verts); + gl.buffer_data_with_array_buffer_view(GL::ARRAY_BUFFER, &view, GL::STATIC_DRAW); + } + gl.vertex_attrib_pointer_with_i32(attr_loc, 2, GL::FLOAT, false, 0, 0); + gl.enable_vertex_attrib_array(attr_loc); + gl.bind_vertex_array(None); + Ok((vao, (verts.len() / 2) as i32)) +} + +impl Renderer { + pub fn new(canvas: &HtmlCanvasElement) -> Result { + let gl = canvas + .get_context("webgl2")? + .ok_or_else(|| JsValue::from_str("WebGL2 no soportado"))? + .dyn_into::()?; + + let chacana = ChacanaSpec::CLASSIC; + + let cosmos_prog = Program::new( + &gl, + VS_FULLSCREEN, + FS_COSMOS, + &[ + "u_resolution", + "u_time", + "u_parallax", + "u_void", + "u_nebula_a", + "u_nebula_b", + "u_stardust", + ], + ) + .map_err(JsValue::from)?; + + let chacana_prog = Program::new( + &gl, + VS_CHACANA, + FS_CHACANA, + &[ + "u_mvp", + "u_time", + "u_thickness", + "u_arm_extent", + "u_line_color", + "u_rim_color", + "u_sun_color", + "u_sun_pulse", + ], + ) + .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_vao, chacana_quad_count) = + upload_quad(&gl, &chacana_quad_verts, 0).map_err(JsValue::from)?; + + let tilt = SpringDamper2::new(2.2, 0.72); + + Ok(Self { + gl, + cosmos_prog, + chacana_prog, + cosmos_vao, + chacana_vao, + chacana_quad_count, + chacana, + tilt, + sun_pulse: 0.0, + last_time_ms: 0.0, + viewport: (canvas.width().max(1), canvas.height().max(1)), + mouse: (0.0, 0.0), + }) + } + + pub fn resize(&mut self, w: u32, h: u32) { + self.viewport = (w.max(1), h.max(1)); + self.gl + .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. + pub fn set_mouse_px(&mut self, x: f32, y: f32) { + let (w, h) = self.viewport; + if h == 0 { + return; + } + let aspect = w as f32 / h as f32; + let half_h = h as f32 * 0.5; + let mx = (x / half_h).clamp(-aspect, aspect); + let my = (y / half_h).clamp(-1.0, 1.0); + 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). + 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. + /// El caller la usa para anclar el DOM. + pub fn tips_ndc(&self) -> [(f32, f32); 4] { + let mvp = self.build_mvp(); + let mut out = [(0.0_f32, 0.0_f32); 4]; + for (i, t) in self.chacana.tips().iter().enumerate() { + let p = mvp * Vec4::new(t.0, t.1, 0.0, 1.0); + let w = if p.w == 0.0 { 1.0 } else { p.w }; + out[i] = (p.x / w, p.y / w); + } + out + } + + pub fn chacana(&self) -> &ChacanaSpec { + &self.chacana + } + + fn build_mvp(&self) -> Mat4 { + let (w, h) = self.viewport; + let aspect = w as f32 / h as f32; + let proj = Mat4::perspective_rh(45.0_f32.to_radians(), aspect, 0.1, 20.0); + 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)); + proj * view * yaw * pitch * scale + } + + pub fn render(&mut self, time_ms: f64) { + let dt = if self.last_time_ms == 0.0 { + 1.0 / 60.0 + } else { + ((time_ms - self.last_time_ms) as f32 / 1000.0).clamp(0.0, 1.0 / 15.0) + }; + 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 { + self.tilt.step(sub_dt); + } + let t = time_ms as f32 * 0.001; + self.sun_pulse = 0.5 + 0.5 * (t * 1.4).sin(); + + let gl = &self.gl; + gl.viewport(0, 0, self.viewport.0 as i32, self.viewport.1 as i32); + gl.disable(GL::DEPTH_TEST); + gl.enable(GL::BLEND); + gl.blend_func(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA); + gl.clear_color(0.02, 0.015, 0.04, 1.0); + gl.clear(GL::COLOR_BUFFER_BIT); + + // ---- Cosmos ---- + gl.use_program(Some(&self.cosmos_prog.program)); + if let Some(u) = self.cosmos_prog.u("u_resolution") { + gl.uniform2f(Some(u), self.viewport.0 as f32, self.viewport.1 as f32); + } + if let Some(u) = self.cosmos_prog.u("u_time") { + gl.uniform1f(Some(u), t); + } + if let Some(u) = self.cosmos_prog.u("u_parallax") { + gl.uniform2f(Some(u), self.mouse.0, self.mouse.1); + } + upload_rgb(gl, self.cosmos_prog.u("u_void"), cosmos::VOID); + upload_rgb(gl, self.cosmos_prog.u("u_nebula_a"), cosmos::NEBULA_A); + upload_rgb(gl, self.cosmos_prog.u("u_nebula_b"), cosmos::NEBULA_B); + upload_rgb(gl, self.cosmos_prog.u("u_stardust"), cosmos::STARDUST); + gl.bind_vertex_array(Some(&self.cosmos_vao)); + gl.draw_arrays(GL::TRIANGLES, 0, 6); + + // ---- Chacana (blend aditivo para que el glow sume luz) ---- + gl.blend_func(GL::SRC_ALPHA, GL::ONE); + gl.use_program(Some(&self.chacana_prog.program)); + let mvp = self.build_mvp(); + if let Some(u) = self.chacana_prog.u("u_mvp") { + gl.uniform_matrix4fv_with_f32_array(Some(u), false, &mvp.to_cols_array()); + } + if let Some(u) = self.chacana_prog.u("u_time") { + gl.uniform1f(Some(u), t); + } + 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_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); + upload_rgb(gl, self.chacana_prog.u("u_sun_color"), cosmos::SUN_CORE); + if let Some(u) = self.chacana_prog.u("u_sun_pulse") { + gl.uniform1f(Some(u), self.sun_pulse); + } + gl.bind_vertex_array(Some(&self.chacana_vao)); + gl.draw_arrays(GL::TRIANGLES, 0, self.chacana_quad_count); + gl.bind_vertex_array(None); + } +} + +fn upload_rgb(gl: &GL, loc: Option<&WebGlUniformLocation>, c: Rgb) { + if let Some(u) = loc { + gl.uniform3f(Some(u), c.0, c.1, c.2); + } +} diff --git a/crates/modules/gioser/gioser-geom/Cargo.toml b/crates/modules/gioser/gioser-geom/Cargo.toml new file mode 100644 index 0000000..c7e7e2d --- /dev/null +++ b/crates/modules/gioser/gioser-geom/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "gioser-geom" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true + diff --git a/crates/modules/gioser/gioser-geom/src/lib.rs b/crates/modules/gioser/gioser-geom/src/lib.rs new file mode 100644 index 0000000..90d7889 --- /dev/null +++ b/crates/modules/gioser/gioser-geom/src/lib.rs @@ -0,0 +1,159 @@ +//! Geometría de la chacana andina (cruz cuadrada escalonada). +//! +//! 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`. +//! +//! 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. + +#[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`. + pub thickness: f32, +} + +impl ChacanaSpec { + /// Configuración canónica: brazo 1.0, grosor 0.18 (proporciones del logo). + pub const CLASSIC: Self = Self { + arm_extent: 1.0, + thickness: 0.18, + }; + + pub const fn new(arm_extent: f32, thickness: f32) -> Self { + Self { + arm_extent, + thickness, + } + } + + /// Las cuatro puntas cardinales en orden `[N, E, S, W]`. + /// Coordenadas listas para anclar UI sobre la chacana. + pub fn tips(&self) -> [(f32, f32); 4] { + 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; + ((-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`. + 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 mut rect = |x0: f32, y0: f32, x1: f32, y1: f32| { + tri.push((x0, y0)); + tri.push((x1, y0)); + tri.push((x1, y1)); + tri.push((x0, y0)); + 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 + tri + } + + /// Para un punto cualquiera, devuelve la punta más cercana y su distancia. + /// Útil para snapping de interacción. + pub fn closest_tip(&self, p: (f32, f32)) -> ((f32, f32), f32) { + let tips = self.tips(); + let mut best = (tips[0], f32::INFINITY); + for t in tips.iter() { + let dx = t.0 - p.0; + let dy = t.1 - p.1; + let d = (dx * dx + dy * dy).sqrt(); + if d < best.1 { + best = (*t, d); + } + } + best + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn perimeter_has_20_vertices() { + assert_eq!(ChacanaSpec::CLASSIC.perimeter().len(), 20); + } + + #[test] + fn triangles_form_9_rectangles() { + assert_eq!(ChacanaSpec::CLASSIC.triangles().len(), 9 * 6); + } + + #[test] + fn tips_match_cardinals() { + let c = ChacanaSpec::new(2.0, 0.3); + 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 + } + + #[test] + fn closest_tip_to_upper_left_is_north() { + let c = ChacanaSpec::CLASSIC; + let (tip, _d) = c.closest_tip((-0.1, 0.95)); + assert_eq!(tip, (0.0, 1.0)); + } + + #[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))); + } +} diff --git a/crates/modules/gioser/gioser-palette/Cargo.toml b/crates/modules/gioser/gioser-palette/Cargo.toml new file mode 100644 index 0000000..2429fc2 --- /dev/null +++ b/crates/modules/gioser/gioser-palette/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "gioser-palette" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true + diff --git a/crates/modules/gioser/gioser-palette/src/lib.rs b/crates/modules/gioser/gioser-palette/src/lib.rs new file mode 100644 index 0000000..565077e --- /dev/null +++ b/crates/modules/gioser/gioser-palette/src/lib.rs @@ -0,0 +1,130 @@ +//! Paleta visual de GioSer: cuatro elementos + cosmos. +//! +//! Los colores se exponen en RGB lineal (rango `0..=1`). Para CSS, +//! convertir con `to_srgb_hex()`. Para shaders WebGL, pasar como `vec3`. +//! La separación lineal/sRGB es deliberada: el motor blending suma luz +//! en lineal y el ojo lee sRGB. + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Rgb(pub f32, pub f32, pub f32); + +impl Rgb { + pub const fn new(r: f32, g: f32, b: f32) -> Self { + Self(r, g, b) + } + + pub const fn array(self) -> [f32; 3] { + [self.0, self.1, self.2] + } + + /// Hex string sRGB `#rrggbb` en bytes ASCII (7 chars). + /// Hex en bytes evita allocs al pasar a CSS desde WASM. + pub fn to_srgb_hex(self) -> [u8; 7] { + fn encode(c: f32) -> u8 { + let g = if c <= 0.003_130_8 { + 12.92 * c + } else { + 1.055 * c.clamp(0.0, 1.0).powf(1.0 / 2.4) - 0.055 + }; + (g.clamp(0.0, 1.0) * 255.0 + 0.5) as u8 + } + fn nib(x: u8) -> u8 { + if x < 10 { + b'0' + x + } else { + b'a' + (x - 10) + } + } + let r = encode(self.0); + let g = encode(self.1); + let b = encode(self.2); + [ + b'#', + nib(r >> 4), + nib(r & 0x0f), + nib(g >> 4), + nib(g & 0x0f), + nib(b >> 4), + nib(b & 0x0f), + ] + } + + pub fn lerp(self, other: Rgb, t: f32) -> Rgb { + Rgb( + self.0 + (other.0 - self.0) * t, + self.1 + (other.1 - self.1) * t, + self.2 + (other.2 - self.2) * t, + ) + } +} + +/// Los cuatro elementos canónicos. +pub mod elements { + use super::Rgb; + /// Aire — azul-blanco luminoso. Software, IA, aspiración. + pub const AIRE: Rgb = Rgb(0.78, 0.86, 1.00); + /// Agua — cyan profundo. Espiritualidad aplicada. + pub const AGUA: Rgb = Rgb(0.28, 0.74, 0.95); + /// Fuego — ámbar/escarlata. Inspiración. + pub const FUEGO: Rgb = Rgb(0.98, 0.45, 0.18); + /// Tierra — ocre cálido. Cuerpo. + pub const TIERRA: Rgb = Rgb(0.82, 0.55, 0.28); + + pub const ALL: [(&str, Rgb); 4] = [ + ("aire", AIRE), + ("agua", AGUA), + ("fuego", FUEGO), + ("tierra", TIERRA), + ]; +} + +/// Fondo cósmico + elementos arquitectónicos. +pub mod cosmos { + use super::Rgb; + /// Vacío profundo, casi negro con tinte violeta. + pub const VOID: Rgb = Rgb(0.030, 0.025, 0.060); + /// Nebulosa interior — violeta tenue. + pub const NEBULA_A: Rgb = Rgb(0.220, 0.130, 0.380); + /// Nebulosa exterior — azul profundo. + pub const NEBULA_B: Rgb = Rgb(0.080, 0.180, 0.320); + /// Núcleo solar central. + pub const SUN_CORE: Rgb = Rgb(1.000, 0.860, 0.520); + /// Líneas de la chacana — cyan helado. + pub const CHACANA_LINE: Rgb = Rgb(0.55, 0.92, 1.00); + /// Aro de fuego del logo — dorado-ámbar. + pub const CHACANA_RIM: Rgb = Rgb(0.95, 0.65, 0.32); + /// Polvo de estrellas. + pub const STARDUST: Rgb = Rgb(0.85, 0.88, 1.00); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hex_white() { + assert_eq!(&Rgb(1.0, 1.0, 1.0).to_srgb_hex(), b"#ffffff"); + } + + #[test] + fn hex_black() { + assert_eq!(&Rgb(0.0, 0.0, 0.0).to_srgb_hex(), b"#000000"); + } + + #[test] + fn lerp_midpoint() { + let m = Rgb(0.0, 0.0, 0.0).lerp(Rgb(1.0, 0.5, 0.0), 0.5); + assert_eq!(m, Rgb(0.5, 0.25, 0.0)); + } + + #[test] + fn linear_to_srgb_midgray_lifts_brightness() { + // 0.5 lineal codifica a sRGB ≈ 0.735 → byte ≈ 187 (0xbb..0xbc segun redondeo de powf). + // Bandgap [0xb8, 0xbf] permite drift de implementaciones de powf entre plataformas. + let h = Rgb(0.5, 0.5, 0.5).to_srgb_hex(); + let lo = b"#b8b8b8"; + let hi = b"#bfbfbf"; + assert!(h.as_slice() >= lo.as_slice() && h.as_slice() <= hi.as_slice(), + "got {:?}", core::str::from_utf8(&h).unwrap_or("")); + } +} diff --git a/crates/modules/gioser/gioser-physics/Cargo.toml b/crates/modules/gioser/gioser-physics/Cargo.toml new file mode 100644 index 0000000..c13392b --- /dev/null +++ b/crates/modules/gioser/gioser-physics/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "gioser-physics" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true + diff --git a/crates/modules/gioser/gioser-physics/src/lib.rs b/crates/modules/gioser/gioser-physics/src/lib.rs new file mode 100644 index 0000000..0bbb37b --- /dev/null +++ b/crates/modules/gioser/gioser-physics/src/lib.rs @@ -0,0 +1,118 @@ +//! Resorte-amortiguador genérico de N dimensiones. +//! +//! Sirve para animaciones orgánicas: el "tilt" de la chacana hacia el mouse, +//! el pulso del sol, las transiciones de hover. La integración es semi-implícita +//! Euler — estable para `dt < 1/freq_hz`. Si `dt` real puede exceder ese límite, +//! el caller subdivide. +//! +//! `damping_ratio`: +//! - `1.0` crítico: settle en tiempo mínimo, sin overshoot. +//! - `0.7` sub-crítico: overshoot suave (≈4.6 %), se siente vivo. +//! - `< 0.5` muy oscilante (no recomendado fuera de FX). + +#![no_std] + +#[derive(Clone, Copy, Debug)] +pub struct SpringDamper { + pub position: [f32; N], + pub velocity: [f32; N], + pub target: [f32; N], + /// Frecuencia natural en Hz. + pub freq_hz: f32, + /// 1.0 = crítico, < 1.0 = oscila. + pub damping_ratio: f32, +} + +impl SpringDamper { + pub const fn new(freq_hz: f32, damping_ratio: f32) -> Self { + Self { + position: [0.0; N], + velocity: [0.0; N], + target: [0.0; N], + freq_hz, + damping_ratio, + } + } + + pub fn with_position(mut self, pos: [f32; N]) -> Self { + self.position = pos; + self.target = pos; + self + } + + pub fn set_target(&mut self, t: [f32; N]) { + self.target = t; + } + + /// Avanza la simulación. Caller suele pasarlo desde un `requestAnimationFrame`. + pub fn step(&mut self, dt: f32) { + // Tau = 2π. core::f32::consts::TAU está estable desde 1.47. + let omega = core::f32::consts::TAU * self.freq_hz; + let zeta = self.damping_ratio; + let k = omega * omega; + let c = 2.0 * zeta * omega; + let mut i = 0; + while i < N { + let dx = self.position[i] - self.target[i]; + let a = -k * dx - c * self.velocity[i]; + self.velocity[i] += a * dt; + self.position[i] += self.velocity[i] * dt; + i += 1; + } + } + + /// `true` cuando el sistema está esencialmente parado en el target. + pub fn at_rest(&self, eps_pos: f32, eps_vel: f32) -> bool { + let mut i = 0; + while i < N { + if (self.position[i] - self.target[i]).abs() > eps_pos + || self.velocity[i].abs() > eps_vel + { + return false; + } + i += 1; + } + true + } +} + +pub type SpringDamper1 = SpringDamper<1>; +pub type SpringDamper2 = SpringDamper<2>; +pub type SpringDamper3 = SpringDamper<3>; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn settles_at_target_when_critically_damped() { + let mut s = SpringDamper2::new(3.0, 1.0); + s.set_target([1.0, -0.5]); + for _ in 0..300 { + s.step(1.0 / 120.0); + } + assert!((s.position[0] - 1.0).abs() < 1e-3); + assert!((s.position[1] + 0.5).abs() < 1e-3); + assert!(s.at_rest(1e-3, 1e-3)); + } + + #[test] + fn underdamped_overshoots() { + let mut s = SpringDamper1::new(3.0, 0.3); + s.set_target([1.0]); + let mut peak = 0.0f32; + for _ in 0..240 { + s.step(1.0 / 240.0); + if s.position[0] > peak { + peak = s.position[0]; + } + } + assert!(peak > 1.0, "underdamped should overshoot, peak={}", peak); + } + + #[test] + fn at_rest_initially() { + let s: SpringDamper2 = SpringDamper::new(2.0, 1.0); + assert!(s.at_rest(1e-6, 1e-6)); + } +} diff --git a/crates/modules/gioser/gioser-shaders/Cargo.toml b/crates/modules/gioser/gioser-shaders/Cargo.toml new file mode 100644 index 0000000..7b5a638 --- /dev/null +++ b/crates/modules/gioser/gioser-shaders/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "gioser-shaders" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true + diff --git a/crates/modules/gioser/gioser-shaders/src/lib.rs b/crates/modules/gioser/gioser-shaders/src/lib.rs new file mode 100644 index 0000000..d724be9 --- /dev/null +++ b/crates/modules/gioser/gioser-shaders/src/lib.rs @@ -0,0 +1,204 @@ +//! Fuentes GLSL ES 3.00 para GioSer. +//! +//! Cada `const &str` es un shader completo, listo para pasar a +//! `gl.shaderSource()`. No dependemos de ningún backend; el cliente +//! decide cómo compilarlos. Convención: precision `highp float`, +//! atributo `a_pos`, varying `v_*`, uniforms `u_*`. + +#![no_std] + +/// Vertex shader para quads en clip-space `[-1, 1]²`. +pub const VS_FULLSCREEN: &str = "#version 300 es +precision highp float; +in vec2 a_pos; +out vec2 v_clip; +out vec2 v_uv; +void main() { + v_clip = a_pos; + v_uv = a_pos * 0.5 + 0.5; + gl_Position = vec4(a_pos, 0.0, 1.0); +} +"; + +/// 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`. +pub const FS_COSMOS: &str = "#version 300 es +precision highp float; +in vec2 v_clip; +in vec2 v_uv; +out vec4 fragColor; +uniform vec2 u_resolution; +uniform float u_time; +uniform vec2 u_parallax; +uniform vec3 u_void; +uniform vec3 u_nebula_a; +uniform vec3 u_nebula_b; +uniform vec3 u_stardust; + +float hash21(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} +float vnoise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + float a = hash21(i); + float b = hash21(i + vec2(1.0, 0.0)); + float c = hash21(i + vec2(0.0, 1.0)); + float d = hash21(i + vec2(1.0, 1.0)); + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} +float fbm(vec2 p) { + float v = 0.0; + float a = 0.55; + for (int i = 0; i < 5; i++) { + v += a * vnoise(p); + p *= 2.07; + a *= 0.55; + } + return v; +} +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; + + float n1 = fbm(uv * 0.9 + d1); + float n2 = fbm(uv * 2.1 + d2); + float n3 = fbm(uv * 4.5 + 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; + + float r = length(v_clip); + color *= 1.0 - smoothstep(0.55, 1.35, 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; + + // 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; + + fragColor = vec4(color, 1.0); +} +"; + +/// Vertex de la chacana: aplica MVP y pasa la posición de mundo al fragment. +pub const VS_CHACANA: &str = "#version 300 es +precision highp float; +in vec2 a_pos; +out vec2 v_world; +uniform mat4 u_mvp; +void main() { + v_world = a_pos; + gl_Position = u_mvp * vec4(a_pos, 0.0, 1.0); +} +"; + +/// Fragment de la chacana: SDF de la cruz escalonada + glow + aro + sol pulsante. +/// Uniforms: `u_time`, `u_thickness`, `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; +in vec2 v_world; +out vec4 fragColor; +uniform float u_time; +uniform float u_thickness; +uniform float u_arm_extent; +uniform vec3 u_line_color; +uniform vec3 u_rim_color; +uniform vec3 u_sun_color; +uniform float u_sun_pulse; + +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))); + return d; +} + +void main() { + vec2 p = v_world; + float d = sdChacana(p, u_thickness, u_arm_extent); + + // Línea: gaussiana alrededor del borde. + float lineW = 0.013; + 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; + + // Fill interior tenue (ligera niebla cyan dentro). + 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; + + // Rayos sutiles (12 divisiones del círculo, como husillos del calendario). + 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)); + + // Sol central: gauss tight + corona suave + pulso. + float sunR = u_thickness * 0.55; + float sunDist = length(p); + 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); + + vec3 col = vec3(0.0); + col += u_line_color * line * 1.45; + 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; + + float alpha = clamp(line * 1.2 + glow + ring + rays + sunMix + fill * 0.5, 0.0, 1.0); + fragColor = vec4(col, alpha); +} +"; + +/// Geometría del quad fullscreen: dos triángulos en clip-space. +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. +pub fn chacana_quad(arm_extent: f32) -> [f32; 12] { + let e = arm_extent * 1.45; + [-e, -e, e, -e, e, e, -e, -e, e, e, -e, e] +} diff --git a/gioser.pdf b/gioser.pdf new file mode 100644 index 0000000..59a282f Binary files /dev/null and b/gioser.pdf differ diff --git a/nohup.out b/nohup.out new file mode 100644 index 0000000..d47c5f7 --- /dev/null +++ b/nohup.out @@ -0,0 +1,179 @@ + +(process:793578): thunar-WARNING **: 17:54:12.970: thunar: Failed to initialize Xfconf: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:13.199: Name 'org.xfce.FileManager' lost on the message dbus. + +(thunar:793578): thunar-WARNING **: 17:54:13.199: Name 'org.freedesktop.FileManager1' lost on the message dbus. + +(thunar:793578): thunar-WARNING **: 17:54:13.199: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:13.347: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:14.127: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:14.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:14.934: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:15.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:15.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:15.734: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:16.527: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:16.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:17.328: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:17.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:17.833: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:18.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:18.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:20.313: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:21.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:22.896: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:23.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:34.088: ThunarThumbnailCache: Couldn't connect to bus service: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:34.253: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:35.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:38.242: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:39.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:43.436: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:44.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:54:48.887: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:54:49.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY +Failed to open display +WARNING: A restricted method in java.lang.System has been called +WARNING: java.lang.System::load has been called by com.sun.star.lib.util.NativeLibraryLoader in an unnamed module (file:/usr/lib/libreoffice/program/classes/libreoffice.jar) +WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module +WARNING: Restricted methods will be blocked in a future release unless native access is enabled + + +(thunar:793578): thunar-WARNING **: 17:55:03.519: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:55:04.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:55:04.868: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:55:05.730: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY +[796208:796208:0512/175510.110078:ERROR:ui/ozone/platform/x11/ozone_platform_x11.cc:256] Missing X server or $DISPLAY +[796208:796208:0512/175510.110090:ERROR:ui/aura/env.cc:246] The platform failed to initialize. Exiting. +[796244:796244:0512/175511.262023:ERROR:ui/ozone/platform/x11/ozone_platform_x11.cc:256] Missing X server or $DISPLAY +[796244:796244:0512/175511.262038:ERROR:ui/aura/env.cc:246] The platform failed to initialize. Exiting. +[796314:796314:0512/175515.462116:ERROR:ui/ozone/platform/x11/ozone_platform_x11.cc:256] Missing X server or $DISPLAY +[796314:796314:0512/175515.462134:ERROR:ui/aura/env.cc:246] The platform failed to initialize. Exiting. +Failed to open display +WARNING: A restricted method in java.lang.System has been called +WARNING: java.lang.System::load has been called by com.sun.star.lib.util.NativeLibraryLoader in an unnamed module (file:/usr/lib/libreoffice/program/classes/libreoffice.jar) +WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module +WARNING: Restricted methods will be blocked in a future release unless native access is enabled + + +(thunar:793578): thunar-WARNING **: 17:55:49.236: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:55:50.730: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:55:59.120: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:56:00.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:56:01.349: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:56:02.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:56:02.753: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:56:02.902: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:56:03.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:56:03.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:56:12.203: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:56:13.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:56:18.405: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:56:19.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:56:24.436: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:56:25.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:56:26.167: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:56:27.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:56:29.305: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:56:30.730: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:58:31.323: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:58:32.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:58:33.741: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:58:33.904: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:58:34.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:58:34.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:58:37.102: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:58:38.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY +Failed to open display +WARNING: A restricted method in java.lang.System has been called +WARNING: java.lang.System::load has been called by com.sun.star.lib.util.NativeLibraryLoader in an unnamed module (file:/usr/lib/libreoffice/program/classes/libreoffice.jar) +WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module +WARNING: Restricted methods will be blocked in a future release unless native access is enabled + +Failed to open display +WARNING: A restricted method in java.lang.System has been called +WARNING: java.lang.System::load has been called by com.sun.star.lib.util.NativeLibraryLoader in an unnamed module (file:/usr/lib/libreoffice/program/classes/libreoffice.jar) +WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module +WARNING: Restricted methods will be blocked in a future release unless native access is enabled + + +(thunar:793578): thunar-WARNING **: 17:59:34.682: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:59:34.806: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 17:59:35.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 17:59:35.728: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY +Failed to open display +WARNING: A restricted method in java.lang.System has been called +WARNING: java.lang.System::load has been called by com.sun.star.lib.util.NativeLibraryLoader in an unnamed module (file:/usr/lib/libreoffice/program/classes/libreoffice.jar) +WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module +WARNING: Restricted methods will be blocked in a future release unless native access is enabled + +"applications.menu" not found in QList("/etc/xdg/menus") + +(thunar:793578): thunar-WARNING **: 18:09:23.005: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 18:09:24.729: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY + +(thunar:793578): thunar-WARNING **: 18:49:45.891: Thumbnailer Proxy Failed ... starting attempt to re-initialize + +(thunar:793578): thunar-WARNING **: 18:49:46.732: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY