tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198629 Sutichaykuna

⎇ fce630c8d0a50843b6d447531efb106a627fd1c7 crates/apps/gioser-web/src/lib.rs Rust · 292 siq'ikuna · 10.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! Entrypoint WASM de la landing GioSer.
2//!
3//! Responsabilidades:
4//! - Montar canvas WebGL2 + listeners de mouse/resize/RAF loop.
5//! - Reposicionar los 4 tips DOM (botones cardinales) sobre las posiciones
6//!   proyectadas del aro de la chacana cada frame.
7//! - Inclinar el título "GioSer" central inyectando CSS vars de tilt.
8//! - Manejar click sobre cada tip → animar drawer expandiéndose desde la
9//!   posición del botón hasta fullscreen, cargar el .md asociado vía
10//!   `pluma-reader-web` y renderearlo themed por elemento.
11//! - Cerrar drawer con close button, Escape o backdrop click.
12
13use std::cell::RefCell;
14use std::rc::Rc;
15
16use gioser_canvas_web::{tips, Renderer};
17use pluma_reader_web::Reader;
18use wasm_bindgen::prelude::*;
19use wasm_bindgen::JsCast;
20use web_sys::{
21    Document, Event, HtmlCanvasElement, HtmlElement, KeyboardEvent, MouseEvent, Window,
22};
23
24/// Factor radial sobre `arm_extent` donde se anclan los botones DOM.
25/// Queda entre la punta de la chacana y el aro grueso.
26const BUTTON_RADIUS_FACTOR: f32 = 1.32;
27
28#[wasm_bindgen(start)]
29pub fn boot() -> Result<(), JsValue> {
30    install_panic_hook();
31    let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
32    let document = window
33        .document()
34        .ok_or_else(|| JsValue::from_str("no document"))?;
35
36    let canvas: HtmlCanvasElement = document
37        .get_element_by_id("gioser-canvas")
38        .ok_or_else(|| JsValue::from_str("no canvas#gioser-canvas"))?
39        .dyn_into()?;
40
41    fit_canvas(&canvas, &window);
42    let renderer = Rc::new(RefCell::new(Renderer::new(&canvas)?));
43    renderer
44        .borrow_mut()
45        .resize(canvas.width(), canvas.height());
46
47    install_resize(&window, &canvas, &renderer)?;
48    install_mouse(&document, &canvas, &renderer)?;
49    install_drawer_handlers(&document)?;
50    install_raf(&window, &document, &canvas, &renderer);
51
52    Ok(())
53}
54
55fn install_resize(
56    window: &Window,
57    canvas: &HtmlCanvasElement,
58    renderer: &Rc<RefCell<Renderer>>,
59) -> Result<(), JsValue> {
60    let canvas = canvas.clone();
61    let win2 = window.clone();
62    let r = renderer.clone();
63    let cb = Closure::<dyn FnMut()>::new(move || {
64        fit_canvas(&canvas, &win2);
65        r.borrow_mut().resize(canvas.width(), canvas.height());
66    });
67    window.add_event_listener_with_callback("resize", cb.as_ref().unchecked_ref())?;
68    cb.forget();
69    Ok(())
70}
71
72fn install_mouse(
73    document: &Document,
74    canvas: &HtmlCanvasElement,
75    renderer: &Rc<RefCell<Renderer>>,
76) -> Result<(), JsValue> {
77    let canvas = canvas.clone();
78    let r = renderer.clone();
79    let cb = Closure::<dyn FnMut(MouseEvent)>::new(move |e: MouseEvent| {
80        let w = canvas.client_width().max(1) as f32;
81        let h = canvas.client_height().max(1) as f32;
82        let x = e.client_x() as f32 - w * 0.5;
83        let y = h * 0.5 - e.client_y() as f32;
84        r.borrow_mut().set_mouse_px(x, y);
85    });
86    document.add_event_listener_with_callback("mousemove", cb.as_ref().unchecked_ref())?;
87    cb.forget();
88    Ok(())
89}
90
91fn install_drawer_handlers(document: &Document) -> Result<(), JsValue> {
92    let doc = document.clone();
93
94    // Clicks en los 4 tips → open drawer.
95    let tips = document.query_selector_all(".tip[data-md]")?;
96    for i in 0..tips.length() {
97        let Some(node) = tips.item(i) else { continue };
98        let Ok(el) = node.dyn_into::<HtmlElement>() else { continue };
99        let id = el.id();
100        let element = id.strip_prefix("tip-").unwrap_or("").to_string();
101        let md_url = el.get_attribute("data-md").unwrap_or_default();
102        let d = doc.clone();
103        let el_for_rect = el.clone();
104        let cb = Closure::<dyn FnMut(Event)>::new(move |e: Event| {
105            e.prevent_default();
106            open_drawer(&d, &element, &el_for_rect, &md_url);
107        });
108        el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
109        cb.forget();
110    }
111
112    // Cualquier elemento marcado con `data-close-drawer` cierra.
113    let closes = document.query_selector_all("[data-close-drawer]")?;
114    for i in 0..closes.length() {
115        let Some(node) = closes.item(i) else { continue };
116        let Ok(el) = node.dyn_into::<HtmlElement>() else { continue };
117        let d = doc.clone();
118        let cb = Closure::<dyn FnMut(Event)>::new(move |e: Event| {
119            e.stop_propagation();
120            close_drawers(&d);
121        });
122        el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
123        cb.forget();
124    }
125
126    // Escape cierra.
127    let d = doc.clone();
128    let kcb = Closure::<dyn FnMut(KeyboardEvent)>::new(move |e: KeyboardEvent| {
129        if e.key() == "Escape" {
130            close_drawers(&d);
131        }
132    });
133    document.add_event_listener_with_callback("keydown", kcb.as_ref().unchecked_ref())?;
134    kcb.forget();
135
136    Ok(())
137}
138
139fn open_drawer(doc: &Document, element: &str, button: &HtmlElement, md_url: &str) {
140    let rect = button.get_bounding_client_rect();
141    let cx = rect.left() + rect.width() / 2.0;
142    let cy = rect.top() + rect.height() / 2.0;
143    let drawer_id = format!("drawer-{}", element);
144    let Some(drawer_el) = doc.get_element_by_id(&drawer_id) else {
145        return;
146    };
147    let drawer: HtmlElement = drawer_el.unchecked_into();
148    let _ = drawer
149        .style()
150        .set_property("--origin-x", &format!("{:.1}px", cx));
151    let _ = drawer
152        .style()
153        .set_property("--origin-y", &format!("{:.1}px", cy));
154    let _ = drawer.class_list().add_1("open");
155    drawer.set_attribute("aria-hidden", "false").ok();
156
157    if let Some(body) = doc.body() {
158        let _ = body.class_list().add_1("drawer-active");
159        let _ = body
160            .class_list()
161            .add_1(&format!("drawer-active-{}", element));
162    }
163
164    // Carga del .md en background.
165    let content_id = format!("drawer-{}-content", element);
166    if let Some(content_el) = doc.get_element_by_id(&content_id) {
167        let content: HtmlElement = content_el.unchecked_into();
168        let reader = Reader::new(content);
169        let element_owned = element.to_string();
170        let md_url_owned = md_url.to_string();
171        wasm_bindgen_futures::spawn_local(async move {
172            if let Err(e) = reader.open_url(&md_url_owned, &element_owned).await {
173                web_sys::console::warn_1(&e);
174            }
175        });
176    }
177}
178
179fn close_drawers(doc: &Document) {
180    let Ok(drawers) = doc.query_selector_all(".drawer.open") else {
181        return;
182    };
183    for i in 0..drawers.length() {
184        let Some(node) = drawers.item(i) else { continue };
185        let Ok(el) = node.dyn_into::<HtmlElement>() else {
186            continue;
187        };
188        let _ = el.class_list().remove_1("open");
189        let _ = el.set_attribute("aria-hidden", "true");
190    }
191    if let Some(body) = doc.body() {
192        let _ = body.class_list().remove_1("drawer-active");
193        for e in ["aire", "fuego", "tierra", "agua"] {
194            let _ = body
195                .class_list()
196                .remove_1(&format!("drawer-active-{}", e));
197        }
198    }
199}
200
201fn install_raf(
202    window: &Window,
203    document: &Document,
204    canvas: &HtmlCanvasElement,
205    renderer: &Rc<RefCell<Renderer>>,
206) {
207    let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut(f64)>>));
208    let g = f.clone();
209    let renderer = renderer.clone();
210    let canvas = canvas.clone();
211    let document = document.clone();
212    let window2 = window.clone();
213    *g.borrow_mut() = Some(Closure::<dyn FnMut(f64)>::new(move |time_ms: f64| {
214        let r = renderer.borrow_mut();
215        // r es Mut, ojo: el render necesita mut, lo hacemos antes de paint.
216        drop(r);
217        renderer.borrow_mut().render(time_ms);
218        let r = renderer.borrow();
219        position_tips(&document, &canvas, &r);
220        update_tilt_css(&document, &r);
221        drop(r);
222        if let Some(cb) = f.borrow().as_ref() {
223            let _ = window2.request_animation_frame(cb.as_ref().unchecked_ref());
224        }
225    }));
226    let _ = window.request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref());
227}
228
229fn fit_canvas(canvas: &HtmlCanvasElement, window: &Window) {
230    let dpr = window.device_pixel_ratio() as f32;
231    let w = window
232        .inner_width()
233        .ok()
234        .and_then(|v| v.as_f64())
235        .unwrap_or(1280.0) as f32;
236    let h = window
237        .inner_height()
238        .ok()
239        .and_then(|v| v.as_f64())
240        .unwrap_or(720.0) as f32;
241    canvas.set_width((w * dpr) as u32);
242    canvas.set_height((h * dpr) as u32);
243    let el: &HtmlElement = canvas.unchecked_ref();
244    let style = el.style();
245    let _ = style.set_property("width", &format!("{}px", w));
246    let _ = style.set_property("height", &format!("{}px", h));
247}
248
249fn position_tips(document: &Document, canvas: &HtmlCanvasElement, renderer: &Renderer) {
250    let clips = renderer.cardinal_positions_ndc(BUTTON_RADIUS_FACTOR);
251    let cw = canvas.client_width().max(1) as f32;
252    let ch = canvas.client_height().max(1) as f32;
253    for (i, (id, _color, _label)) in tips::ORDER.iter().enumerate() {
254        let (nx, ny) = clips[i];
255        let px = (nx + 1.0) * 0.5 * cw;
256        let py = (1.0 - (ny + 1.0) * 0.5) * ch;
257        let sel = format!("tip-{}", id);
258        if let Some(el) = document.get_element_by_id(&sel) {
259            if let Ok(el) = el.dyn_into::<HtmlElement>() {
260                let _ = el.style().set_property(
261                    "transform",
262                    &format!("translate({:.2}px, {:.2}px) translate(-50%, -50%)", px, py),
263                );
264            }
265        }
266    }
267}
268
269fn update_tilt_css(document: &Document, renderer: &Renderer) {
270    let (pitch, yaw) = renderer.tilt_degrees();
271    if let Some(brand) = document.get_element_by_id("brand") {
272        if let Ok(brand) = brand.dyn_into::<HtmlElement>() {
273            // CSS rotateX usa el mismo signo que nuestra pitch (mouse up tilts top toward viewer).
274            let _ = brand
275                .style()
276                .set_property("--tilt-x", &format!("{:.2}deg", pitch));
277            let _ = brand
278                .style()
279                .set_property("--tilt-y", &format!("{:.2}deg", yaw));
280        }
281    }
282}
283
284fn install_panic_hook() {
285    static SET: std::sync::Once = std::sync::Once::new();
286    SET.call_once(|| {
287        std::panic::set_hook(Box::new(|info| {
288            let msg = format!("{}", info);
289            web_sys::console::error_1(&JsValue::from_str(&msg));
290        }));
291    });
292}