gioser
This commit is contained in:
Generated
+46
@@ -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"
|
||||
|
||||
+16
@@ -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)
|
||||
# ============================================================
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@@ -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; }
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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 `<canvas>`, 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<Self, String> {
|
||||
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<WebGlShader, String> {
|
||||
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<Self, JsValue> {
|
||||
let gl = canvas
|
||||
.get_context("webgl2")?
|
||||
.ok_or_else(|| JsValue::from_str("WebGL2 no soportado"))?
|
||||
.dyn_into::<GL>()?;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "gioser-geom"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "gioser-palette"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
@@ -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(""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "gioser-physics"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
@@ -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<const N: usize> {
|
||||
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<const N: usize> SpringDamper<N> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "gioser-shaders"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -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
|
||||
Reference in New Issue
Block a user