From 9acdf68d6707c47fe1dcd5900acd6b969ab80c13 Mon Sep 17 00:00:00 2001 From: sergio Date: Mon, 18 May 2026 16:10:01 +0000 Subject: [PATCH] =?UTF-8?q?feat(tahuantinsuyu):=20orden=20visual=20?= =?UTF-8?q?=E2=80=94=20zoom=20uniforme,=20c=C3=ADrculo=20de=20aspectos,=20?= =?UTF-8?q?profundidad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tercera tanda de UX a partir de feedback: - Zoom uniforme sobre glyphs DOM: font_size y disk_size de signos, números de casa, planetas natales/overlay/outer y labels ASC/MC/DESC/IC se multiplican por view_scale. Antes solo escalaba la geometría del canvas (anillos, líneas), los símbolos quedaban fijos — sensación de "todo se mueve menos los iconos". - Doble anillo de planetas + círculo de aspectos: nuevo `bodies_inner` en `Radii`, junto con `bodies` define el "cinturón" donde viven los glyphs natales. `aspects` movido de 0.24*r a 0.49*r (de cerca-del-centro a pegado al cinturón) — las líneas de aspecto ahora conectan cuerpos cerca de su anillo en lugar de cruzar toda la rueda. Los tres anillos (bodies, bodies_inner, aspects) se pintan con stroke_circle_3d para que sean visibles. - Doble línea de casas más fuerte: houses_outer + houses_inner ambos con stroke_circle_3d y `house_cusp` α=0.85. Antes solo houses_inner tenía un stroke plano y débil. - Líneas de aspecto por orbe + filtro de menores: `aspect_width(kind, orb, mono)` modula grosor inverso al orbe. Aspectos mayores arrancan en techo 2.1 px (orbe 0°) hasta 0.7 px (orbe 8°); menores entre 0.5 y 1.2 px sobre orbe 0-3°. Los aspectos menores se omiten directamente si orbe > 3°. - Vignette en lugar de starfield: `paint_depth_field` reemplaza `paint_starfield`. Pinta ~28 anillos concéntricos del centro al borde con alpha cuadrática creciente (curve t²) — el centro permanece claro y el borde se oscurece. Da profundidad sin ruido de puntos. Solo en dark themes. Co-Authored-By: Claude Opus 4.7 --- .../tahuantinsuyu-canvas/src/lib.rs | 221 +++++++++++------- 1 file changed, 137 insertions(+), 84 deletions(-) diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs index d3466b9..6739525 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs @@ -667,12 +667,17 @@ impl AstrologyCanvas { const WHEEL_SIZE: f32 = 580.0; const WHEEL_MARGIN: f32 = 28.0; -/// Pinta un starfield sutil sobre el background del panel del canvas. -/// Posiciones generadas con xorshift32 + seed const → idénticas entre -/// frames (no parpadea). Las estrellas viven solo cuando el theme es -/// dark — sobre fondos claros (impresora / solarized) un punteado de -/// puntos quedaría como ruido visual y NO suma al sentido "papel". -fn paint_starfield(bounds: Bounds, window: &mut Window, theme: &Theme) { +/// Pinta un gradiente radial de profundidad sobre el background del +/// canvas — efecto vignette. Se aproxima al gradient radial (no +/// soportado nativamente por gpui en `.bg()`) pintando ~28 anillos +/// concéntricos del centro hacia afuera, con alpha creciente hacia el +/// borde. El centro queda claro y los extremos se oscurecen, dando +/// sensación de "el wheel emerge desde la profundidad". +/// +/// Solo activo en themes dark — sobre papel (light / print) el panel +/// queda plano: una viñeta sobre fondo claro tiñe el papel y rompe +/// la metáfora "impresión". +fn paint_depth_field(bounds: Bounds, window: &mut Window, theme: &Theme) { if !theme.is_dark { return; } @@ -683,39 +688,26 @@ fn paint_starfield(bounds: Bounds, window: &mut Window, theme: &Theme) { if bw <= 0.0 || bh <= 0.0 { return; } - - // Densidad: ~1 estrella por 4800 px² → unas 130 estrellas en - // 800×800, escala con el panel. - let count = ((bw * bh) / 4800.0).clamp(40.0, 320.0) as u32; - - let mut state: u32 = 0x1f3a_5b7d; - let mut next = || -> u32 { - // xorshift32 — barato y determinístico. - state ^= state << 13; - state ^= state >> 17; - state ^= state << 5; - state - }; - - let star_color = hsla(220.0 / 360.0, 0.20, 0.92, 1.0); - for _ in 0..count { - let rx = (next() as f32) / (u32::MAX as f32); - let ry = (next() as f32) / (u32::MAX as f32); - let ra = (next() as f32) / (u32::MAX as f32); - let rs = (next() as f32) / (u32::MAX as f32); - let x = ox + rx * bw; - let y = oy + ry * bh; - // Distribución de tamaños: la mayoría 0.6-1.0px ("polvo"), un - // 15% un poco más grandes (1.4-2.2px) que actúan como - // "estrellas brillantes". - let r = if rs > 0.85 { - 1.4 + rs * 0.8 - } else { - 0.6 + rs * 0.4 - }; - // Alpha entre 0.10 y 0.55 — sutil, nunca compite con la rueda. - let a = 0.10 + ra * 0.45; - fill_circle(window, x, y, r, with_alpha(star_color, a)); + let cx = ox + bw / 2.0; + let cy = oy + bh / 2.0; + // El gradient se extiende hasta la diagonal del rectángulo para + // que las esquinas estén dentro del último anillo (sin "halo" + // visible donde se corta). + let r_max = ((bw * bw + bh * bh).sqrt()) / 2.0 * 1.05; + let steps = 28; + // Color: casi-negro con tinte ligero del panel (el panel es dark). + let deep = hsla(230.0 / 360.0, 0.30, 0.04, 1.0); + // Stroke de cada anillo: el ancho cubre 1/steps del radio para + // que no queden gaps entre anillos. + let stroke_w = (r_max / steps as f32) * 1.15; + for i in 0..steps { + let t = i as f32 / (steps - 1) as f32; + let r = r_max * t; + // Curva ease-in: alpha crece de 0 (centro) a ~0.55 (borde), + // con la mayor parte del cambio en la mitad exterior. t² da + // ese "fondo profundo en el perímetro sin opacar el centro". + let alpha = 0.55 * (t * t); + stroke_circle(window, cx, cy, r, stroke_w, with_alpha(deep, alpha)); } } @@ -767,15 +759,15 @@ impl Render for AstrologyCanvas { CanvasMode::Thumbnails { items, .. } => render_thumbnails(&theme, items), }; - // Starfield: capa absoluta detrás del body, ocupa todo el - // canvas. Pinta ~140 puntos pequeños semi-transparentes en - // posiciones deterministas (PRNG con seed const) — sin - // parpadeo entre frames. Sutil; aporta el "universo" sin - // competir con la rueda. - let theme_for_stars = theme.clone(); - let starfield = canvas( + // Depth field: capa absoluta detrás del body, ocupa todo el + // canvas. Vignette radial — el centro queda claro y los + // bordes se oscurecen, dando profundidad sin "ruido" de + // puntos. Solo en themes dark (en papel rompería la + // metáfora). + let theme_for_depth = theme.clone(); + let depth_field = canvas( |_b, _w, _cx| (), - move |bounds, _, window, _| paint_starfield(bounds, window, &theme_for_stars), + move |bounds, _, window, _| paint_depth_field(bounds, window, &theme_for_depth), ) .absolute() .size_full(); @@ -796,7 +788,7 @@ impl Render for AstrologyCanvas { .bg(theme.bg_panel.clone()) .relative() .overflow_hidden() - .child(starfield) + .child(depth_field) .child( div() .size_full() @@ -1001,6 +993,12 @@ fn render_wheel( .mt(px(view_pan_y)) .child(canvas_element); + // Factor de escala para los glyphs DOM. Los radii ya están + // escalados (vienen de wheel_size = WHEEL_SIZE * view_scale), pero + // los tamaños de fuente y disco están hardcoded — los multiplico + // por view_scale para que el zoom afecte uniformemente todo el + // contenido visual del wheel, no solo la geometría del canvas. + let s = view_scale; // Sign glyphs. if visible.get(&LayerKind::SignDial).copied().unwrap_or(true) { let sign_ring_mid = (radii.sign_outer + radii.sign_inner) / 2.0; @@ -1012,8 +1010,8 @@ fn render_wheel( wheel = wheel.child(centered_glyph( cx_center + x, cy_center + y, - 20.0, - 18.0, + 20.0 * s, + 18.0 * s, sign_unicode(&g.symbol).into(), color, )); @@ -1033,8 +1031,8 @@ fn render_wheel( wheel = wheel.child(centered_glyph( cx_center + x, cy_center + y, - 16.0, - 10.0, + 16.0 * s, + 11.0 * s, format!("{}", h).into(), palette.house_cusp, )); @@ -1055,8 +1053,8 @@ fn render_wheel( let is_natal = layer.module_id == "natal"; let ring = radii.body_ring(&layer.module_id); let alpha = if is_natal { 1.0 } else { 0.88 }; - let font_size = if is_natal { 18.0 } else { 14.0 }; - let disk_size = if is_natal { 26.0 } else { 22.0 }; + let font_size = (if is_natal { 18.0 } else { 14.0 }) * s; + let disk_size = (if is_natal { 26.0 } else { 22.0 }) * s; for g in &layer.glyphs { let (x, y) = polar_to_screen(g.deg, asc, rot_offset, ring); let color = with_alpha(planet_color(palette, &g.symbol), alpha); @@ -1101,8 +1099,8 @@ fn render_wheel( wheel = wheel.child(planet_glyph( cx_center + x, cy_center + y, - 20.0, - 13.0, + 20.0 * s, + 13.0 * s, glyph_text.into(), color, halo_bg, @@ -1224,8 +1222,8 @@ fn render_wheel( let label_r = r_outer * 1.08; for (deg, label) in angle_labels { let (x, y) = polar_to_screen(deg, asc, rot_offset, label_r); - let pill_w = if label.len() > 2 { 38.0 } else { 30.0 }; - let pill_h = 18.0; + let pill_w = (if label.len() > 2 { 38.0 } else { 30.0 }) * s; + let pill_h = 18.0 * s; wheel = wheel.child( div() .absolute() @@ -1236,11 +1234,11 @@ fn render_wheel( .flex() .items_center() .justify_center() - .rounded(px(9.0)) + .rounded(px(9.0 * s)) .bg(halo_bg) .border_1() .border_color(with_alpha(palette.angle_highlight, 0.85)) - .text_size(px(11.0)) + .text_size(px(11.0 * s)) .text_color(palette.angle_highlight) .child(SharedString::from(label)), ); @@ -1506,13 +1504,23 @@ struct Radii { houses_inner: f32, /// Anillo de midpoints — entre bodies natales y houses_inner. midpoints: f32, + /// Anillo principal de cuerpos natales — donde se posan los + /// glyphs. Junto con `bodies_inner` forman el "cinturón" de los + /// planetas (doble línea visual). bodies: f32, + /// Borde interior del cinturón de planetas. Marca dónde "termina" + /// la zona de cuerpos y empieza la zona de aspectos. + bodies_inner: f32, /// Anillo interno con cuerpos progresados (overlay opcional). progression: f32, /// Anillo más interno con cuerpos dirigidos por Solar Arc. solar_arc: f32, /// Anillo de carta compuesta (midpoint Davison) con un partner. composite: f32, + /// Círculo donde anclan las líneas de aspecto entre cuerpos + /// natales. Justo dentro del cinturón de planetas, no en el + /// centro — así las líneas conectan cuerpos cercanos al ring + /// donde se ven, no atraviesan toda la rueda. aspects: f32, } @@ -1526,10 +1534,14 @@ impl Radii { houses_inner: r * 0.66, midpoints: r * 0.62, bodies: r * 0.58, - progression: r * 0.48, - solar_arc: r * 0.40, - composite: r * 0.32, - aspects: r * 0.24, + bodies_inner: r * 0.53, + // aspects pegado al cinturón de cuerpos pero adentro: + // las líneas entran al "círculo de aspectos" justo bajo + // los glyphs en lugar de cruzar el centro. + aspects: r * 0.49, + progression: r * 0.43, + solar_arc: r * 0.36, + composite: r * 0.28, } } @@ -1615,16 +1627,13 @@ fn paint_wheel( } } - // 2. Casas — cusps radiales + énfasis Asc/IC/Desc/MC. + // 2. Casas — doble anillo (inner + outer) + cusps radiales + + // énfasis Asc/IC/Desc/MC. La doble línea vuelve a la zona de + // casas una "corona" claramente identificable contra el resto. if show(LayerKind::Houses) { - stroke_circle( - window, - cx, - cy, - radii.houses_inner, - 0.8, - with_alpha(palette.house_cusp, 0.6), - ); + let house_color = with_alpha(palette.house_cusp, 0.85); + stroke_circle_3d(window, cx, cy, radii.houses_outer, 1.1, house_color, theme); + stroke_circle_3d(window, cx, cy, radii.houses_inner, 1.1, house_color, theme); for layer in layers { if matches!(layer.kind, LayerKind::Houses) { @@ -1681,6 +1690,22 @@ fn paint_wheel( } } + // 2.5. Cinturón de planetas + círculo de aspectos. El cinturón + // (bodies + bodies_inner) marca la franja donde viven los + // glyphs natales. El círculo de aspectos queda apenas más + // adentro — las líneas de aspecto se anclan ahí, no en el + // centro, así "conectan" cuerpos cercanos a su anillo en lugar + // de cruzar toda la rueda. + if show(LayerKind::Bodies) { + let belt_color = with_alpha(palette.dial_ring, 0.55); + stroke_circle_3d(window, cx, cy, radii.bodies, 1.0, belt_color, theme); + stroke_circle_3d(window, cx, cy, radii.bodies_inner, 0.9, belt_color, theme); + } + if show(LayerKind::Aspects) { + let aspect_ring_color = with_alpha(palette.dial_ring, 0.45); + stroke_circle_3d(window, cx, cy, radii.aspects, 0.9, aspect_ring_color, theme); + } + // 3. Aspectos. Cada module_id usa su par de radios — natal-natal // ambos en `aspects`, cross con transit en `bodies → transits`, // cross con progression en `bodies → progression`. @@ -1692,6 +1717,13 @@ fn paint_wheel( let (r_from, r_to) = radii.aspect_endpoints(&layer.module_id); let is_cross = r_from != r_to; for seg in segs { + // Filtro minors con orbe ancho: los aspectos + // menores (quincunx, semi-square, quintile…) + // solo se trazan si están MUY apretados + // (orbe ≤ 3°). Sobre 3° ensucian sin aportar. + if !is_major_aspect(&seg.kind) && seg.orb_deg.abs() > 3.0 { + continue; + } let base = aspect_color(palette, &seg.kind); let base = with_alpha(base, base.a * seg.opacity); // Hover focus: si hay un planeta hovereado y @@ -1707,17 +1739,11 @@ fn paint_wheel( } else { None }; - // En BW las "fuertes" (conjunction/opposition/ - // square) van un poco más gruesas para sumar - // diferenciación al dash. - let width = if mono { - match seg.kind.as_str() { - "conjunction" | "opposition" | "square" => 1.3, - _ => 1.0, - } - } else { - 1.0 - }; + // Width inverso al orbe: orbes cerrados se ven + // gruesos (aspecto "fuerte"), orbes amplios + // finos. Mayores van un escalón más gruesos + // que menores en su mismo orbe. + let width = aspect_width(&seg.kind, seg.orb_deg, mono); if is_cross { paint_cross_aspect_line( window, @@ -2055,6 +2081,33 @@ fn paint_segment( } } +/// `true` para los 5 aspectos Ptoloméicos (conjunction, sextile, +/// square, trine, opposition). Cualquier otro `kind` se considera +/// menor — quincunx, semi-square, quintile, sesquiquadrate, etc. +fn is_major_aspect(kind: &str) -> bool { + matches!( + kind, + "conjunction" | "sextile" | "square" | "trine" | "opposition" + ) +} + +/// Grosor de línea de aspecto inverso al orbe. La idea: a orbe 0° +/// (aspecto exacto) la línea va gruesa porque "pesa" más; a orbe +/// amplio se afina. Los mayores arrancan en un techo más alto que +/// los menores. En BW se le suma un poquito a todos porque las +/// líneas competen con sus dash patterns. +fn aspect_width(kind: &str, orb_deg: f32, mono: bool) -> f32 { + let orb = orb_deg.abs(); + let major = is_major_aspect(kind); + // Orbe de referencia para normalizar: ~8° para mayores, ~3° para + // menores. Más allá la línea ya está afinada al mínimo. + let max_orb = if major { 8.0 } else { 3.0 }; + let t = (1.0 - (orb / max_orb)).clamp(0.0, 1.0); + let (min_w, max_w) = if major { (0.7, 2.1) } else { (0.5, 1.2) }; + let w = min_w + (max_w - min_w) * t; + if mono { w + 0.2 } else { w } +} + /// Dash pattern por aspecto, para modo monocromático. En modo color /// el caller pasa `None` y las líneas van sólidas. Patterns elegidos /// para que cada kind sea distinguible a ojo: