This commit is contained in:
sergio
2026-05-12 18:55:29 +00:00
parent 6596c81271
commit 52acaabcf4
23 changed files with 1774 additions and 0 deletions
+31
View File
@@ -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",
]
+77
View File
@@ -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 `<a href="#aire|fuego|tierra|agua">` 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
```
+75
View File
@@ -0,0 +1,75 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>GioSer · En el centro, el ser</title>
<link rel="stylesheet" href="./styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Inter:wght@300;500&display=swap">
</head>
<body>
<canvas id="gioser-canvas" aria-hidden="true"></canvas>
<header class="brand">
<h1 class="brand-title">Gio<span class="brand-dot">·</span>Ser</h1>
<p class="brand-sub">EN EL CENTRO · EL SER</p>
</header>
<main id="tips" aria-label="Cuatro elementos">
<a id="tip-aire" class="tip tip-aire" href="#aire" aria-label="Aire">
<svg viewBox="0 0 40 40" class="tip-glyph">
<path d="M6 14 q 7 -10 14 0 t 14 0" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
<path d="M6 22 q 7 -10 14 0 t 14 0" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" opacity="0.75"/>
<circle cx="32" cy="9" r="2.2" fill="none" stroke="currentColor" stroke-width="1.2"/>
</svg>
<span class="tip-label">AIRE</span>
<span class="tip-sub">Software · IA</span>
</a>
<a id="tip-fuego" class="tip tip-fuego" href="#fuego" aria-label="Fuego">
<svg viewBox="0 0 40 40" class="tip-glyph">
<path d="M20 4 q -10 10 -5 20 q 3 -5 5 -5 q 1 8 5 10 q 8 -8 0 -18 q -4 5 -5 -7 z"
fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
<circle cx="20" cy="22" r="2.5" fill="currentColor" opacity="0.55"/>
</svg>
<span class="tip-label">FUEGO</span>
<span class="tip-sub">Inspiración</span>
</a>
<a id="tip-tierra" class="tip tip-tierra" href="#tierra" aria-label="Tierra">
<svg viewBox="0 0 40 40" class="tip-glyph">
<path d="M4 30 l 10 -14 l 6 7 l 5 -10 l 11 17 z"
fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
<path d="M20 30 v 6 M16 32 l -3 4 M24 32 l 3 4 M20 36 v 1"
fill="none" stroke="currentColor" stroke-width="1.1" opacity="0.7"/>
</svg>
<span class="tip-label">TIERRA</span>
<span class="tip-sub">Cuerpo</span>
</a>
<a id="tip-agua" class="tip tip-agua" href="#agua" aria-label="Agua">
<svg viewBox="0 0 40 40" class="tip-glyph">
<path d="M5 20 q 5 -8 10 0 t 10 0 t 10 0" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
<path d="M5 27 q 5 -8 10 0 t 10 0 t 10 0" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" opacity="0.75"/>
<path d="M5 34 q 5 -8 10 0 t 10 0 t 10 0" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.5"/>
</svg>
<span class="tip-label">AGUA</span>
<span class="tip-sub">Espiritualidad</span>
</a>
</main>
<footer class="quote">aire · agua · fuego · tierra</footer>
<script type="module">
import init from "./pkg/gioser_web.js";
init().catch(err => {
console.error(err);
document.body.insertAdjacentHTML("beforeend",
'<pre style="color:#f59056;position:fixed;left:1rem;bottom:1rem;max-width:90vw;white-space:pre-wrap;font-family:monospace">' +
String(err) + '</pre>');
});
</script>
</body>
</html>
+149
View File
@@ -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<RefCell<Renderer>>,
) -> Result<(), JsValue> {
let canvas = canvas.clone();
let win2 = window.clone();
let r = renderer.clone();
let cb = Closure::<dyn FnMut()>::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<RefCell<Renderer>>,
) -> Result<(), JsValue> {
let canvas = canvas.clone();
let r = renderer.clone();
let cb = Closure::<dyn FnMut(MouseEvent)>::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<RefCell<Renderer>>,
) {
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut(f64)>>));
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::<dyn FnMut(f64)>::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::<HtmlElement>() {
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));
}));
});
}
+191
View File
@@ -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; }
}