gioser
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user