feat(gioser): sol detrás, título central, drawers MD + pluma agnóstico

Visual de la chacana retrabajado contra chakana.png de referencia:
- Sol detrás (gauss + corona, masked al interior de la chacana — sólo
  asoma por la superficie de la cruz, no se cuela afuera).
- Doble outline dorado (línea principal + paralela offset 0.020), color
  CHACANA_LINE pasa de cyan helado a dorado-ámbar del logo.
- Interior con niebla violeta-noche (u_dark_color) y rayos radiales
  sutiles desde el centro, modulados por sin(t * 0.3).
- Aro doble exterior: ring fino interior + ring grueso con 4 grupos de
  3 puntos cardinales (calculados angularmente, no rayos largos).
- WORLD_SCALE 1.45→1.05, MAX_TILT 35°→28° (más sólido, menos caricaturesco).

Título "GioSer" centrado dentro de la superficie de la chacana, sin
subtítulo. Se inclina junto con la chacana vía CSS perspective +
rotateX/rotateY desde u-tilt-x/y inyectadas cada frame por WASM.

Botones (4 tips):
- Reposicionados a `arm_extent * 1.32` (entre punta y aro grueso).
- Bigger: min-width 168px, glyph 54px, label Cinzel 0.95rem.
- Doble anillo en hover (::before con border + glow).
- Cuando un drawer se abre, fade-out de tips + canvas + brand.

Drawers MD (uno por elemento):
- `<aside class="drawer drawer-{element}">` con transform-origin desde
  CSS vars (--origin-x/y) seteadas por WASM al click — crece desde la
  posición exacta del botón hasta fullscreen en 700ms con cubic-bezier.
- Ambience por elemento: AIRE (radial drift), FUEGO (flicker keyframe),
  AGUA (tide vertical), TIERRA (warm earth gradient).
- Cerrado con botón X, Escape o data-close-drawer.
- Carga MD desde ./md/{element}.md via spawn_local + Reader::open_url.

Pluma (visor MD agnóstico, dos crates nuevos):
- `crates/modules/pluma/pluma-md` — wrapper sobre pulldown-cmark 0.12.
  API: to_html(), to_themed_html(md, theme) con sanitización del theme,
  events() para AST stream. GFM completo. No deps web. 5 tests.
- `crates/modules/pluma/pluma-reader-web` — toma HtmlElement, expone
  open_url async (fetch via wasm-bindgen-futures), render_md sync,
  show_loading/show_error. NO inyecta CSS — el host estiliza
  `.pluma-doc[data-pluma-theme="..."]` con sus colores.

CSS pluma-doc completo: h1/h2/h3, code/pre con border-left accent,
blockquote, tables, lists, hr gradient. Loader spinner + error state.

Placeholders en md/{aire,fuego,tierra,agua}.md con texto seed.

Workspace verde + 18 tests (6 geom + 4 palette + 3 physics + 5 pluma-md).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-13 23:38:37 +00:00
parent 3dbdfb357b
commit fce630c8d0
17 changed files with 1132 additions and 243 deletions
+8 -2
View File
@@ -11,7 +11,9 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
gioser-canvas-web = { path = "../../modules/gioser/gioser-canvas-web" }
pluma-reader-web = { path = "../../modules/pluma/pluma-reader-web" }
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
js-sys.workspace = true
[dependencies.web-sys]
@@ -23,9 +25,13 @@ features = [
"HtmlElement",
"HtmlCanvasElement",
"CssStyleDeclaration",
"MouseEvent",
"DomTokenList",
"DomRect",
"Event",
"EventTarget",
"MouseEvent",
"KeyboardEvent",
"NodeList",
"Performance",
"console",
]
+69 -26
View File
@@ -7,67 +7,110 @@
<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">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Inter:wght@300;500;600&family=JetBrains+Mono:wght@400;600&display=swap">
</head>
<body>
<canvas id="gioser-canvas" aria-hidden="true"></canvas>
<header class="brand">
<header id="brand" 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"/>
<a id="tip-aire" class="tip tip-aire" href="#aire" data-md="./md/aire.md" aria-label="Aire — Software e IA">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<path d="M6 18 q 9 -12 18 0 t 18 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path d="M6 28 q 9 -12 18 0 t 18 0" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" opacity="0.75"/>
<path d="M6 38 q 9 -12 18 0 t 18 0" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" opacity="0.45"/>
</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"/>
<a id="tip-fuego" class="tip tip-fuego" href="#fuego" data-md="./md/fuego.md" aria-label="Fuego — Inspiración">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<path d="M24 4 q -12 12 -6 24 q 3 -6 6 -6 q 1 10 6 12 q 10 -10 0 -22 q -4 6 -6 -8 z"
fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
<circle cx="24" cy="28" r="3" fill="currentColor" opacity="0.5"/>
</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"/>
<a id="tip-tierra" class="tip tip-tierra" href="#tierra" data-md="./md/tierra.md" aria-label="Tierra — Cuerpo">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<path d="M4 36 l 12 -16 l 8 9 l 6 -12 l 14 19 z"
fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
<path d="M24 36 v 8 M19 38 l -4 5 M29 38 l 4 5 M24 44 v 2"
fill="none" stroke="currentColor" stroke-width="1.3" 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"/>
<a id="tip-agua" class="tip tip-agua" href="#agua" data-md="./md/agua.md" aria-label="Agua — Espiritualidad aplicada">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<path d="M6 22 q 6 -10 12 0 t 12 0 t 12 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path d="M6 30 q 6 -10 12 0 t 12 0 t 12 0" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" opacity="0.75"/>
<path d="M6 38 q 6 -10 12 0 t 12 0 t 12 0" fill="none" stroke="currentColor" stroke-width="1.3" 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>
<!-- DRAWERS: uno por elemento. Cada uno crece desde la posición del tip
clickeado (origin set via CSS vars desde WASM) hasta fullscreen. -->
<aside id="drawer-aire" class="drawer drawer-aire" data-element="aire" aria-hidden="true">
<button class="drawer-close" data-close-drawer aria-label="Cerrar Aire">×</button>
<div class="drawer-ambience" aria-hidden="true"></div>
<header class="drawer-head">
<span class="drawer-mark">aire</span>
<h2 class="drawer-title">Aire</h2>
<span class="drawer-tag">Software · IA · Aspiración</span>
</header>
<section class="drawer-content" id="drawer-aire-content"></section>
</aside>
<aside id="drawer-fuego" class="drawer drawer-fuego" data-element="fuego" aria-hidden="true">
<button class="drawer-close" data-close-drawer aria-label="Cerrar Fuego">×</button>
<div class="drawer-ambience" aria-hidden="true"></div>
<header class="drawer-head">
<span class="drawer-mark">fuego</span>
<h2 class="drawer-title">Fuego</h2>
<span class="drawer-tag">Inspiración</span>
</header>
<section class="drawer-content" id="drawer-fuego-content"></section>
</aside>
<aside id="drawer-tierra" class="drawer drawer-tierra" data-element="tierra" aria-hidden="true">
<button class="drawer-close" data-close-drawer aria-label="Cerrar Tierra">×</button>
<div class="drawer-ambience" aria-hidden="true"></div>
<header class="drawer-head">
<span class="drawer-mark">tierra</span>
<h2 class="drawer-title">Tierra</h2>
<span class="drawer-tag">Cuerpo</span>
</header>
<section class="drawer-content" id="drawer-tierra-content"></section>
</aside>
<aside id="drawer-agua" class="drawer drawer-agua" data-element="agua" aria-hidden="true">
<button class="drawer-close" data-close-drawer aria-label="Cerrar Agua">×</button>
<div class="drawer-ambience" aria-hidden="true"></div>
<header class="drawer-head">
<span class="drawer-mark">agua</span>
<h2 class="drawer-title">Agua</h2>
<span class="drawer-tag">Espiritualidad aplicada</span>
</header>
<section class="drawer-content" id="drawer-agua-content"></section>
</aside>
<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">' +
'<pre style="color:#f59056;position:fixed;left:1rem;bottom:1rem;max-width:90vw;white-space:pre-wrap;font-family:monospace;z-index:9999">' +
String(err) + '</pre>');
});
</script>
+25
View File
@@ -0,0 +1,25 @@
# Agua
> *Lo que fluye. Lo que une dentro y afuera.*
El **Agua** es el dominio de la **espiritualidad aplicada**: las
prácticas, lecturas y tradiciones que sostienen la atención y dan
sentido al hacer. No es decoración mística: es la práctica concreta
de mantenerse permeable, vivo, conectado.
## Espiritualidad aplicada
Aplicada significa que no se queda en libros: pasa por la práctica
diaria — la lectura, la meditación, la ceremonia, la conversación
honda. El agua moja todos los otros ejes.
## Lo que vive acá
- Notas de lectura sobre filosofía, mística, sabiduría andina.
- Diario de prácticas (meditación, ceremonias, retiros).
- Conversaciones con maestros y comunidades.
## Próximamente
*Acá se va a ir armando una bitácora de lecturas y prácticas. Por
ahora el placeholder verifica el render bajo el tema **agua**.*
+26
View File
@@ -0,0 +1,26 @@
# Aire
> *Lo que respira el sistema. Lo que sube.*
El **Aire** es el dominio del **software público y la IA**. Es la capa
intangible que transporta pensamiento — los bits que vuelan entre
máquinas, las inferencias que destilan sentido del ruido, las APIs
que conversan sin verse.
## Aspiración
El Aire **aspira**: empuja hacia arriba. Es el movimiento de subir el
nivel de abstracción, de hacer que una cosa difícil parezca obvia, de
regalarle al usuario una herramienta que no le pesa.
## Lo que vive acá
- Herramientas open source que **GioSer** publica y mantiene.
- Modelos de IA que asisten al ciclo de creación.
- Documentación, ensayos, manifiestos.
## Próximamente
*Esta sección se va a llenar con los proyectos concretos del eje aire.*
Por ahora, este placeholder vive en `md/aire.md` y se renderiza vía
`pluma-md` con tema *aire*.
+25
View File
@@ -0,0 +1,25 @@
# Fuego
> *Lo que enciende. Lo que transforma.*
El **Fuego** es el dominio de la **inspiración**. Es la chispa que
convierte una idea en gesto, una frase en ritual, un problema en
prototipo. Sin fuego, los otros tres elementos se enfrían y se quedan
contemplándose.
## Inspiración
El fuego no se planea, se **atiende**. Llega — y la respuesta es no
dejarlo pasar. Acá viven los ensayos, los videos, los manifiestos y
los experimentos que nacieron porque algo prendió.
## Lo que vive acá
- Charlas, ensayos cortos, posts crudos.
- Bocetos visuales, exploraciones tipográficas.
- Documentos de manifiesto sobre cómo trabajar y para qué.
## Próximamente
*Voy a ir enlazando archivos `.md` específicos acá. Por ahora este
texto sirve para verificar el render bajo el tema **fuego**.*
+26
View File
@@ -0,0 +1,26 @@
# Tierra
> *El cuerpo. La materia. Lo que sostiene.*
La **Tierra** es el dominio del **cuerpo**. Es lo que se toca, lo que
huele, lo que se siembra. El eje terrestre de GioSer recuerda que
todo proyecto —por muy abstracto que parezca— pasa por un cuerpo que
respira, come, descansa y se conmueve.
## Cuerpo
El cuerpo no es una metáfora: es donde aterriza el aire, donde el
agua se vuelve vida, donde el fuego deja huella. Cuidarlo es parte
del trabajo.
## Lo que vive acá
- Prácticas, rutinas, recetas.
- Materialidad: objetos, lugares, oficios.
- Salud y reposo como infraestructura.
## Próximamente
*Esta sección va a recibir notas, fotos y enlaces a oficios y
prácticas concretas. Por ahora el placeholder verifica el tema
**tierra**.*
+151 -8
View File
@@ -1,17 +1,29 @@
//! 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.
//! Responsabilidades:
//! - Montar canvas WebGL2 + listeners de mouse/resize/RAF loop.
//! - Reposicionar los 4 tips DOM (botones cardinales) sobre las posiciones
//! proyectadas del aro de la chacana cada frame.
//! - Inclinar el título "GioSer" central inyectando CSS vars de tilt.
//! - Manejar click sobre cada tip → animar drawer expandiéndose desde la
//! posición del botón hasta fullscreen, cargar el .md asociado vía
//! `pluma-reader-web` y renderearlo themed por elemento.
//! - Cerrar drawer con close button, Escape o backdrop click.
use std::cell::RefCell;
use std::rc::Rc;
use gioser_canvas_web::{tips, Renderer};
use pluma_reader_web::Reader;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Document, HtmlCanvasElement, HtmlElement, MouseEvent, Window};
use web_sys::{
Document, Event, HtmlCanvasElement, HtmlElement, KeyboardEvent, MouseEvent, Window,
};
/// Factor radial sobre `arm_extent` donde se anclan los botones DOM.
/// Queda entre la punta de la chacana y el aro grueso.
const BUTTON_RADIUS_FACTOR: f32 = 1.32;
#[wasm_bindgen(start)]
pub fn boot() -> Result<(), JsValue> {
@@ -34,6 +46,7 @@ pub fn boot() -> Result<(), JsValue> {
install_resize(&window, &canvas, &renderer)?;
install_mouse(&document, &canvas, &renderer)?;
install_drawer_handlers(&document)?;
install_raf(&window, &document, &canvas, &renderer);
Ok(())
@@ -66,7 +79,6 @@ fn install_mouse(
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);
@@ -76,6 +88,116 @@ fn install_mouse(
Ok(())
}
fn install_drawer_handlers(document: &Document) -> Result<(), JsValue> {
let doc = document.clone();
// Clicks en los 4 tips → open drawer.
let tips = document.query_selector_all(".tip[data-md]")?;
for i in 0..tips.length() {
let Some(node) = tips.item(i) else { continue };
let Ok(el) = node.dyn_into::<HtmlElement>() else { continue };
let id = el.id();
let element = id.strip_prefix("tip-").unwrap_or("").to_string();
let md_url = el.get_attribute("data-md").unwrap_or_default();
let d = doc.clone();
let el_for_rect = el.clone();
let cb = Closure::<dyn FnMut(Event)>::new(move |e: Event| {
e.prevent_default();
open_drawer(&d, &element, &el_for_rect, &md_url);
});
el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
cb.forget();
}
// Cualquier elemento marcado con `data-close-drawer` cierra.
let closes = document.query_selector_all("[data-close-drawer]")?;
for i in 0..closes.length() {
let Some(node) = closes.item(i) else { continue };
let Ok(el) = node.dyn_into::<HtmlElement>() else { continue };
let d = doc.clone();
let cb = Closure::<dyn FnMut(Event)>::new(move |e: Event| {
e.stop_propagation();
close_drawers(&d);
});
el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
cb.forget();
}
// Escape cierra.
let d = doc.clone();
let kcb = Closure::<dyn FnMut(KeyboardEvent)>::new(move |e: KeyboardEvent| {
if e.key() == "Escape" {
close_drawers(&d);
}
});
document.add_event_listener_with_callback("keydown", kcb.as_ref().unchecked_ref())?;
kcb.forget();
Ok(())
}
fn open_drawer(doc: &Document, element: &str, button: &HtmlElement, md_url: &str) {
let rect = button.get_bounding_client_rect();
let cx = rect.left() + rect.width() / 2.0;
let cy = rect.top() + rect.height() / 2.0;
let drawer_id = format!("drawer-{}", element);
let Some(drawer_el) = doc.get_element_by_id(&drawer_id) else {
return;
};
let drawer: HtmlElement = drawer_el.unchecked_into();
let _ = drawer
.style()
.set_property("--origin-x", &format!("{:.1}px", cx));
let _ = drawer
.style()
.set_property("--origin-y", &format!("{:.1}px", cy));
let _ = drawer.class_list().add_1("open");
drawer.set_attribute("aria-hidden", "false").ok();
if let Some(body) = doc.body() {
let _ = body.class_list().add_1("drawer-active");
let _ = body
.class_list()
.add_1(&format!("drawer-active-{}", element));
}
// Carga del .md en background.
let content_id = format!("drawer-{}-content", element);
if let Some(content_el) = doc.get_element_by_id(&content_id) {
let content: HtmlElement = content_el.unchecked_into();
let reader = Reader::new(content);
let element_owned = element.to_string();
let md_url_owned = md_url.to_string();
wasm_bindgen_futures::spawn_local(async move {
if let Err(e) = reader.open_url(&md_url_owned, &element_owned).await {
web_sys::console::warn_1(&e);
}
});
}
}
fn close_drawers(doc: &Document) {
let Ok(drawers) = doc.query_selector_all(".drawer.open") else {
return;
};
for i in 0..drawers.length() {
let Some(node) = drawers.item(i) else { continue };
let Ok(el) = node.dyn_into::<HtmlElement>() else {
continue;
};
let _ = el.class_list().remove_1("open");
let _ = el.set_attribute("aria-hidden", "true");
}
if let Some(body) = doc.body() {
let _ = body.class_list().remove_1("drawer-active");
for e in ["aire", "fuego", "tierra", "agua"] {
let _ = body
.class_list()
.remove_1(&format!("drawer-active-{}", e));
}
}
}
fn install_raf(
window: &Window,
document: &Document,
@@ -89,8 +211,14 @@ fn install_raf(
let document = document.clone();
let window2 = window.clone();
*g.borrow_mut() = Some(Closure::<dyn FnMut(f64)>::new(move |time_ms: f64| {
let r = renderer.borrow_mut();
// r es Mut, ojo: el render necesita mut, lo hacemos antes de paint.
drop(r);
renderer.borrow_mut().render(time_ms);
position_tips(&document, &canvas, &renderer.borrow());
let r = renderer.borrow();
position_tips(&document, &canvas, &r);
update_tilt_css(&document, &r);
drop(r);
if let Some(cb) = f.borrow().as_ref() {
let _ = window2.request_animation_frame(cb.as_ref().unchecked_ref());
}
@@ -119,7 +247,7 @@ fn fit_canvas(canvas: &HtmlCanvasElement, window: &Window) {
}
fn position_tips(document: &Document, canvas: &HtmlCanvasElement, renderer: &Renderer) {
let clips = renderer.tips_ndc();
let clips = renderer.cardinal_positions_ndc(BUTTON_RADIUS_FACTOR);
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() {
@@ -138,6 +266,21 @@ fn position_tips(document: &Document, canvas: &HtmlCanvasElement, renderer: &Ren
}
}
fn update_tilt_css(document: &Document, renderer: &Renderer) {
let (pitch, yaw) = renderer.tilt_degrees();
if let Some(brand) = document.get_element_by_id("brand") {
if let Ok(brand) = brand.dyn_into::<HtmlElement>() {
// CSS rotateX usa el mismo signo que nuestra pitch (mouse up tilts top toward viewer).
let _ = brand
.style()
.set_property("--tilt-x", &format!("{:.2}deg", pitch));
let _ = brand
.style()
.set_property("--tilt-y", &format!("{:.2}deg", yaw));
}
}
}
fn install_panic_hook() {
static SET: std::sync::Once = std::sync::Once::new();
SET.call_once(|| {
+396 -89
View File
@@ -1,11 +1,16 @@
/* === Tokens === */
:root {
--bg: #06050d;
--fg: #e8eaf5;
--gold: #d8a85d;
--gold-deep: #b77e34;
--aire: #d0dbff;
--agua: #6cd0f3;
--fuego: #f59056;
--tierra: #d49873;
--rim: #d8a85d;
--ease-emerge: cubic-bezier(0.22, 0.61, 0.20, 1);
--ease-magma: cubic-bezier(0.32, 0, 0.05, 1);
}
* { box-sizing: border-box; }
@@ -20,9 +25,9 @@ html, body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 300;
overflow: hidden;
cursor: default;
}
/* === Canvas WebGL === */
#gioser-canvas {
position: fixed;
inset: 0;
@@ -30,47 +35,47 @@ html, body {
height: 100vh;
display: block;
z-index: 0;
transition: opacity 600ms var(--ease-emerge), filter 600ms var(--ease-emerge);
}
/* === Marca central: GioSer sobre la superficie de la chacana === */
.brand {
position: fixed;
top: 4vh;
top: 50%;
left: 50%;
transform: translateX(-50%);
text-align: center;
z-index: 10;
z-index: 6;
pointer-events: none;
user-select: none;
transform-style: preserve-3d;
/* perspective hace que la rotación del título dé sensación 3D igual que la chacana */
transform:
translate(-50%, -50%)
perspective(900px)
rotateX(var(--tilt-x, 0deg))
rotateY(var(--tilt-y, 0deg));
transition: transform 30ms linear;
}
.brand-title {
font-family: 'Cinzel', serif;
font-weight: 700;
font-size: clamp(2.4rem, 5vw, 4.4rem);
font-size: clamp(2.6rem, 6vw, 5.4rem);
margin: 0;
letter-spacing: 0.08em;
color: var(--fg);
color: #f4eedf;
text-shadow:
0 0 18px rgba(108, 208, 243, 0.35),
0 0 40px rgba(216, 168, 93, 0.18);
0 0 22px rgba(216, 168, 93, 0.55),
0 0 50px rgba(183, 126, 52, 0.30),
0 0 80px rgba(216, 168, 93, 0.15);
}
.brand-dot {
color: var(--rim);
color: var(--gold);
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;
text-shadow:
0 0 14px var(--gold),
0 0 32px rgba(245, 144, 86, 0.55);
}
/* === Tips (botones cardinales) === */
#tips {
position: fixed;
inset: 0;
@@ -85,107 +90,409 @@ html, body {
text-decoration: none;
color: var(--fg);
user-select: none;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
padding: 0.7rem 1.0rem 0.55rem;
min-width: 120px;
gap: 0.55rem;
padding: 1.1rem 1.6rem 0.95rem;
min-width: 168px;
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);
background:
radial-gradient(ellipse at top, rgba(255, 255, 255, 0.04), transparent 65%),
rgba(8, 6, 22, 0.55);
backdrop-filter: blur(12px) saturate(140%);
-webkit-backdrop-filter: blur(12px) saturate(140%);
border-radius: 20px;
border: 1px solid rgba(216, 168, 93, 0.25);
box-shadow:
0 12px 36px rgba(0, 0, 0, 0.40),
inset 0 0 0 1px rgba(255, 255, 255, 0.04);
transition:
box-shadow 260ms ease,
border-color 260ms ease,
background 260ms ease,
letter-spacing 260ms ease;
box-shadow 350ms var(--ease-emerge),
border-color 350ms var(--ease-emerge),
background 350ms var(--ease-emerge),
opacity 300ms 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;
inset: -10px;
border-radius: 26px;
border: 1px solid currentColor;
opacity: 0;
transition: opacity 400ms ease, inset 400ms var(--ease-emerge);
pointer-events: none;
opacity: 0.7;
}
.tip:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(20, 14, 40, 0.6);
border-color: currentColor;
background:
radial-gradient(ellipse at top, rgba(255, 255, 255, 0.07), transparent 65%),
rgba(20, 14, 40, 0.72);
}
.tip:hover::before {
opacity: 0.45;
inset: -16px;
}
.tip-glyph {
width: 42px;
height: 42px;
width: 54px;
height: 54px;
color: currentColor;
filter: drop-shadow(0 0 6px currentColor);
transition: filter 240ms ease;
filter: drop-shadow(0 0 6px currentColor) drop-shadow(0 0 16px currentColor);
transition: filter 320ms ease, transform 350ms var(--ease-emerge);
}
.tip:hover .tip-glyph {
filter: drop-shadow(0 0 12px currentColor) drop-shadow(0 0 20px currentColor);
filter: drop-shadow(0 0 14px currentColor) drop-shadow(0 0 28px currentColor);
transform: translateY(-3px);
}
.tip-label {
font-family: 'Cinzel', serif;
font-size: 0.74rem;
letter-spacing: 0.36em;
font-size: 0.95rem;
letter-spacing: 0.42em;
font-weight: 600;
text-indent: 0.36em;
text-indent: 0.42em;
margin-top: 0.15rem;
}
.tip-sub {
font-family: 'Inter', sans-serif;
font-size: 0.62rem;
letter-spacing: 0.18em;
font-size: 0.7rem;
letter-spacing: 0.22em;
font-weight: 300;
color: rgba(232, 234, 245, 0.55);
color: rgba(232, 234, 245, 0.62);
text-transform: uppercase;
text-indent: 0.22em;
}
.tip-aire { color: var(--aire); }
.tip-fuego { color: var(--fuego); }
.tip-agua { color: var(--agua); }
.tip-tierra { color: var(--tierra); }
/* Cuando un drawer está abierto, los tips se ocultan suavemente. */
body.drawer-active .tip {
opacity: 0;
pointer-events: none;
transition: opacity 250ms ease;
}
body.drawer-active #gioser-canvas {
opacity: 0.35;
filter: blur(4px) saturate(80%);
}
body.drawer-active .brand {
opacity: 0;
transition: opacity 250ms ease;
}
.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 {
/* === Drawers (visor MD full-screen) === */
.drawer {
position: fixed;
bottom: 3vh;
left: 50%;
transform: translateX(-50%);
font-family: 'Cinzel', serif;
inset: 0;
z-index: 100;
pointer-events: none;
opacity: 0;
visibility: hidden;
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
transform: scale(0.0);
background:
radial-gradient(ellipse at center, var(--drawer-glow, rgba(216, 168, 93, 0.15)), transparent 65%),
rgba(6, 5, 13, 0.96);
backdrop-filter: blur(28px) saturate(140%);
-webkit-backdrop-filter: blur(28px) saturate(140%);
transition:
transform 700ms var(--ease-magma),
opacity 450ms ease,
visibility 0s 700ms;
overflow: hidden;
}
.drawer.open {
pointer-events: auto;
opacity: 1;
visibility: visible;
transform: scale(1);
transition:
transform 700ms var(--ease-magma),
opacity 450ms ease,
visibility 0s;
}
.drawer-aire { --drawer-glow: rgba(208, 219, 255, 0.22); --drawer-accent: var(--aire); }
.drawer-fuego { --drawer-glow: rgba(245, 144, 86, 0.28); --drawer-accent: var(--fuego); }
.drawer-agua { --drawer-glow: rgba(108, 208, 243, 0.22); --drawer-accent: var(--agua); }
.drawer-tierra { --drawer-glow: rgba(212, 152, 115, 0.24); --drawer-accent: var(--tierra); }
/* Ambience: capa decorativa con efectos según elemento. */
.drawer-ambience {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
}
.drawer-aire .drawer-ambience {
background:
radial-gradient(circle at 18% 22%, rgba(208, 219, 255, 0.20), transparent 38%),
radial-gradient(circle at 78% 68%, rgba(208, 219, 255, 0.14), transparent 40%),
radial-gradient(circle at 45% 90%, rgba(180, 200, 255, 0.10), transparent 45%);
animation: aire-drift 28s ease-in-out infinite alternate;
}
.drawer-fuego .drawer-ambience {
background:
radial-gradient(circle at 50% 100%, rgba(245, 144, 86, 0.35), transparent 55%),
radial-gradient(circle at 25% 80%, rgba(255, 90, 40, 0.18), transparent 35%),
radial-gradient(circle at 80% 85%, rgba(255, 140, 60, 0.18), transparent 35%);
animation: fuego-flicker 5s ease-in-out infinite;
}
.drawer-agua .drawer-ambience {
background:
radial-gradient(ellipse at 50% 95%, rgba(60, 160, 230, 0.30), transparent 60%),
radial-gradient(ellipse at 20% 70%, rgba(108, 208, 243, 0.15), transparent 50%),
radial-gradient(ellipse at 80% 75%, rgba(108, 208, 243, 0.12), transparent 50%);
animation: agua-tide 14s ease-in-out infinite alternate;
}
.drawer-tierra .drawer-ambience {
background:
radial-gradient(ellipse at 50% 100%, rgba(120, 80, 40, 0.40), transparent 60%),
radial-gradient(ellipse at 22% 88%, rgba(180, 130, 80, 0.20), transparent 45%),
radial-gradient(ellipse at 78% 88%, rgba(150, 100, 60, 0.22), transparent 45%);
}
@keyframes aire-drift {
from { transform: translate(-4%, -1%); }
to { transform: translate(4%, 2%); }
}
@keyframes fuego-flicker {
0%, 100% { opacity: 0.85; transform: scaleY(1.00); }
35% { opacity: 1.00; transform: scaleY(1.04); }
60% { opacity: 0.92; transform: scaleY(0.98); }
}
@keyframes agua-tide {
from { transform: translateY(0); }
to { transform: translateY(-3%); }
}
/* === Drawer head + close === */
.drawer-head {
position: relative;
z-index: 2;
text-align: center;
padding: 7vh 8vw 2vh;
color: var(--drawer-accent);
}
.drawer-mark {
display: inline-block;
font-family: 'Inter', sans-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-transform: uppercase;
color: rgba(232, 234, 245, 0.6);
margin-bottom: 0.4rem;
text-indent: 0.55em;
}
.drawer-title {
font-family: 'Cinzel', serif;
font-weight: 700;
font-size: clamp(2.4rem, 6vw, 4.6rem);
margin: 0;
letter-spacing: 0.08em;
color: var(--drawer-accent);
text-shadow: 0 0 28px currentColor, 0 0 56px rgba(255, 255, 255, 0.10);
}
.drawer-tag {
display: block;
font-family: 'Inter', sans-serif;
font-size: 0.78rem;
letter-spacing: 0.32em;
text-transform: uppercase;
color: rgba(232, 234, 245, 0.55);
margin-top: 0.7rem;
text-indent: 0.32em;
}
.drawer-close {
position: absolute;
top: 2.4vh;
right: 2.4vw;
z-index: 5;
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--drawer-accent);
font-size: 1.6rem;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 200ms ease, transform 250ms var(--ease-emerge), border-color 200ms ease;
font-family: 'Inter', sans-serif;
}
.drawer-close:hover {
background: rgba(255, 255, 255, 0.10);
border-color: currentColor;
transform: rotate(90deg);
}
/* === Drawer content (MD render via pluma-reader-web) === */
.drawer-content {
position: relative;
z-index: 2;
max-height: calc(100vh - 22vh);
overflow-y: auto;
padding: 1vh 10vw 8vh;
opacity: 0;
transition: opacity 500ms ease 250ms;
}
.drawer.open .drawer-content {
opacity: 1;
}
.drawer-content::-webkit-scrollbar { width: 6px; }
.drawer-content::-webkit-scrollbar-thumb {
background: var(--drawer-accent);
border-radius: 3px;
opacity: 0.4;
}
/* === pluma-doc: estilos del visor MD === */
.pluma-doc {
max-width: 760px;
margin: 0 auto;
font-family: 'Inter', sans-serif;
font-weight: 300;
font-size: 1.05rem;
line-height: 1.78;
color: rgba(232, 234, 245, 0.92);
}
.pluma-doc > * + * { margin-top: 1.0em; }
.pluma-doc h1 {
font-family: 'Cinzel', serif;
font-weight: 700;
font-size: clamp(1.7rem, 3vw, 2.4rem);
color: var(--drawer-accent);
text-shadow: 0 0 18px currentColor;
letter-spacing: 0.04em;
margin-top: 1.4em;
}
.pluma-doc h2 {
font-family: 'Cinzel', serif;
font-weight: 500;
font-size: 1.5rem;
color: var(--drawer-accent);
letter-spacing: 0.04em;
margin-top: 1.6em;
padding-bottom: 0.3em;
border-bottom: 1px solid rgba(255, 255, 255, 0.10);
}
.pluma-doc h3 {
font-family: 'Inter', sans-serif;
font-weight: 600;
font-size: 1.18rem;
color: rgba(255, 255, 255, 0.92);
letter-spacing: 0.03em;
margin-top: 1.6em;
}
.pluma-doc p { margin: 0; }
.pluma-doc a {
color: var(--drawer-accent);
text-decoration: none;
border-bottom: 1px solid currentColor;
transition: opacity 200ms ease;
}
.pluma-doc a:hover { opacity: 0.7; }
.pluma-doc strong { color: rgba(255, 255, 255, 0.98); font-weight: 600; }
.pluma-doc em { color: rgba(255, 255, 255, 0.92); }
.pluma-doc code {
font-family: 'JetBrains Mono', ui-monospace, monospace;
background: rgba(255, 255, 255, 0.06);
padding: 0.12em 0.45em;
border-radius: 4px;
font-size: 0.92em;
color: var(--drawer-accent);
}
.pluma-doc pre {
background: rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 255, 255, 0.08);
border-left: 3px solid var(--drawer-accent);
border-radius: 8px;
padding: 1rem 1.2rem;
overflow-x: auto;
font-size: 0.92rem;
}
.pluma-doc pre code {
background: transparent;
color: rgba(232, 234, 245, 0.92);
padding: 0;
}
.pluma-doc blockquote {
border-left: 3px solid var(--drawer-accent);
padding: 0.4em 1.2em;
color: rgba(232, 234, 245, 0.75);
font-style: italic;
background: rgba(255, 255, 255, 0.03);
border-radius: 0 6px 6px 0;
}
.pluma-doc ul, .pluma-doc ol { padding-left: 1.6em; }
.pluma-doc li { margin: 0.4em 0; }
.pluma-doc li::marker { color: var(--drawer-accent); }
.pluma-doc hr {
border: none;
height: 1px;
background: linear-gradient(to right, transparent, var(--drawer-accent), transparent);
margin: 2em 0;
}
.pluma-doc table {
border-collapse: collapse;
width: 100%;
font-size: 0.95rem;
}
.pluma-doc th, .pluma-doc td {
padding: 0.55em 0.9em;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
text-align: left;
}
.pluma-doc th {
color: var(--drawer-accent);
font-weight: 600;
letter-spacing: 0.04em;
}
.pluma-loading, .pluma-error {
display: flex;
align-items: center;
justify-content: center;
min-height: 30vh;
color: rgba(232, 234, 245, 0.55);
font-family: 'Cinzel', serif;
letter-spacing: 0.4em;
font-size: 0.9rem;
}
.pluma-loading::before {
content: "";
width: 28px;
height: 28px;
margin-right: 1rem;
border: 1px solid var(--drawer-accent);
border-top-color: transparent;
border-radius: 50%;
animation: pluma-spin 1s linear infinite;
}
.pluma-error {
color: var(--fuego);
font-style: italic;
}
@keyframes pluma-spin {
to { transform: rotate(360deg); }
}
/* === Responsive === */
@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 { min-width: 110px; padding: 0.7rem 0.9rem; }
.tip-glyph { width: 36px; height: 36px; }
.tip-label { font-size: 0.72rem; }
.tip-sub { display: none; }
.brand-title { font-size: clamp(2rem, 10vw, 3rem); }
.drawer-head { padding: 5vh 5vw 1vh; }
.drawer-content { padding: 0 5vw 5vh; }
}