feat(tahuantinsuyu): rueda 3D, hover-highlight, universo, themes papel
Segunda tanda de UX a partir de feedback de uso real: - Zoom/pan reasignados: wheel = zoom puro (sin modifier). LMB drag fuera del anillo de signos = pan; sobre el anillo = jog-dial (rectificación). MMB sigue como pan secundario. Tecla `0` resetea zoom + pan. - Planetas legibles: el "dot rellenado" se reduce a 3 px (solo marca el grado exacto). Encima va `planet_glyph` con disco-halo del bg_panel y border del color del planeta — el glyph unicode astronómico (☉☽☿♀♂♃♄♅♆♇) ahora se lee contra cualquier fondo. - Aspectos hover-highlight: al hovear un planeta, sus líneas se mantienen al 100 % y el resto cae a 18 %. Resuelve el "¿quién contra quién?" sin desordenar la rueda. - Ascensionales: cruz completa ASC-DESC + MC-IC (4 radios) con α=0.55. Labels ASC/MC/DESC/IC como pills con bg-halo y border `angle_highlight`, font 11 — antes eran texto chico que se fundía con el dial. - Universo: el wheel pierde su bg de cuadrado (que cortaba contra el panel). El root del canvas pinta un starfield sutil ~130 puntos deterministas (xorshift32 con seed fija, sin parpadeo entre frames). Solo activo en themes dark — sobre fondos claros generaría ruido. - Estilo 3D anillos: `stroke_circle_3d` (highlight +luma + base + shadow -luma) reemplaza al stroke plano en sign_outer, sign_inner y el outer ring. Más `paint_dial_bevel` con 10 strokes finos en bell curve entre sign_inner y sign_outer — simula gradient radial que gpui canvas no soporta nativo. - Theme `Print Color`: papel crema, paleta astro con luminancia 0.26-0.34 y saturación alta, sin glow ni gradients. - Theme `Print B&W`: monocromático sobre blanco puro. Aspectos diferenciados por dash pattern en lugar de color: conjunction/opposition sólidos, square dash medio, trine dash largo, sextile dotted, minors dotted finísimo. `paint_segment` con `dash: Option<(on,off)>` para implementar dashes (gpui canvas no tiene stroke dash nativo). Todos los tests siguen verdes (6 shell + 5 yahweh-theme + 2 tahuantinsuyu-theme). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ use gpui::{
|
|||||||
Bounds, Context, EventEmitter, FocusHandle, Focusable, Hsla, IntoElement,
|
Bounds, Context, EventEmitter, FocusHandle, Focusable, Hsla, IntoElement,
|
||||||
KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
|
KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
|
||||||
PathBuilder, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, SharedString, Styled,
|
PathBuilder, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, SharedString, Styled,
|
||||||
Window, canvas, div, hsla, linear_color_stop, linear_gradient, point, prelude::*, px,
|
Window, canvas, div, hsla, point, prelude::*, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tahuantinsuyu_engine::{Geometry, Layer, LayerKind, OUTER_RING_MODULES, RenderModel};
|
use tahuantinsuyu_engine::{Geometry, Layer, LayerKind, OUTER_RING_MODULES, RenderModel};
|
||||||
@@ -324,6 +324,7 @@ impl AstrologyCanvas {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
fn pan_by(&mut self, dx: f32, dy: f32, cx: &mut Context<'_, Self>) {
|
fn pan_by(&mut self, dx: f32, dy: f32, cx: &mut Context<'_, Self>) {
|
||||||
if dx == 0.0 && dy == 0.0 {
|
if dx == 0.0 && dy == 0.0 {
|
||||||
return;
|
return;
|
||||||
@@ -335,11 +336,14 @@ impl AstrologyCanvas {
|
|||||||
|
|
||||||
// ----- Internos: handlers de jog-dial -----
|
// ----- Internos: handlers de jog-dial -----
|
||||||
|
|
||||||
fn on_jog_down(
|
/// Despacha el LMB down entre jog-dial (sobre el anillo de signos)
|
||||||
|
/// y pan (cualquier otra parte del canvas). El jog-dial es el
|
||||||
|
/// control de rectificación de hora; el pan es navegación libre.
|
||||||
|
fn on_primary_down(
|
||||||
&mut self,
|
&mut self,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
_cx: &mut Context<'_, Self>,
|
cx: &mut Context<'_, Self>,
|
||||||
) {
|
) {
|
||||||
let (cx_px, cy_px) = bounds_center(bounds);
|
let (cx_px, cy_px) = bounds_center(bounds);
|
||||||
let mx: f32 = position.x.into();
|
let mx: f32 = position.x.into();
|
||||||
@@ -347,20 +351,18 @@ impl AstrologyCanvas {
|
|||||||
let dx = mx - cx_px;
|
let dx = mx - cx_px;
|
||||||
let dy = my - cy_px;
|
let dy = my - cy_px;
|
||||||
let dist = (dx * dx + dy * dy).sqrt();
|
let dist = (dx * dx + dy * dy).sqrt();
|
||||||
// r_outer se deriva del width actual del canvas (que ya
|
|
||||||
// incorpora view_scale), no del WHEEL_SIZE constante. Sin esto,
|
|
||||||
// el jog-dial dejaría de funcionar al hacer zoom.
|
|
||||||
let r_outer = effective_r_outer(bounds);
|
let r_outer = effective_r_outer(bounds);
|
||||||
let radii = Radii::from_outer(r_outer);
|
let radii = Radii::from_outer(r_outer);
|
||||||
// Aro de captura un poco más generoso que el anillo del dial.
|
let on_dial = dist >= radii.sign_inner * 0.95 && dist <= radii.sign_outer * 1.10;
|
||||||
if dist < radii.sign_inner * 0.95 || dist > radii.sign_outer * 1.10 {
|
if on_dial {
|
||||||
return;
|
|
||||||
}
|
|
||||||
let angle = dy.atan2(dx).to_degrees();
|
let angle = dy.atan2(dx).to_degrees();
|
||||||
self.state.drag_jog = Some(JogDragState {
|
self.state.drag_jog = Some(JogDragState {
|
||||||
last_screen_angle_deg: angle,
|
last_screen_angle_deg: angle,
|
||||||
accumulated_delta_deg: 0.0,
|
accumulated_delta_deg: 0.0,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
self.on_pan_down(position, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_jog_move(
|
fn on_jog_move(
|
||||||
@@ -600,22 +602,14 @@ impl AstrologyCanvas {
|
|||||||
_w: &mut Window,
|
_w: &mut Window,
|
||||||
cx: &mut Context<'_, Self>,
|
cx: &mut Context<'_, Self>,
|
||||||
) {
|
) {
|
||||||
let (dx_px, dy_px) = match event.delta {
|
let (_dx_px, dy_px) = match event.delta {
|
||||||
ScrollDelta::Pixels(p) => (f32::from(p.x), f32::from(p.y)),
|
ScrollDelta::Pixels(p) => (f32::from(p.x), f32::from(p.y)),
|
||||||
ScrollDelta::Lines(p) => (p.x * 16.0, p.y * 16.0),
|
ScrollDelta::Lines(p) => (p.x * 16.0, p.y * 16.0),
|
||||||
};
|
};
|
||||||
// Ctrl + wheel = zoom. wheel solo = pan (contenido sigue al
|
// Wheel = zoom puro, sin modifier. Pan se hace con drag (LMB
|
||||||
// dedo). El criterio de "modifier" usa el control flag estándar
|
// fuera del anillo, o MMB). 100px de scroll ≈ ±20% zoom.
|
||||||
// de gpui (en macOS sería cmd; aceptamos ambos como zoom).
|
|
||||||
let zoom_mod = event.modifiers.control || event.modifiers.platform;
|
|
||||||
if zoom_mod {
|
|
||||||
// Sensibilidad: 100px de scroll ≈ ±20% zoom. exp es suave y
|
|
||||||
// simétrico contra dy negativo (zoom out).
|
|
||||||
let factor = (dy_px * 0.002).exp();
|
let factor = (dy_px * 0.002).exp();
|
||||||
self.zoom_by(factor, cx);
|
self.zoom_by(factor, cx);
|
||||||
} else {
|
|
||||||
self.pan_by(dx_px, dy_px, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_jog_up(&mut self, cx: &mut Context<'_, Self>) {
|
fn on_jog_up(&mut self, cx: &mut Context<'_, Self>) {
|
||||||
@@ -673,6 +667,58 @@ impl AstrologyCanvas {
|
|||||||
const WHEEL_SIZE: f32 = 580.0;
|
const WHEEL_SIZE: f32 = 580.0;
|
||||||
const WHEEL_MARGIN: f32 = 28.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<Pixels>, window: &mut Window, theme: &Theme) {
|
||||||
|
if !theme.is_dark {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ox: f32 = bounds.origin.x.into();
|
||||||
|
let oy: f32 = bounds.origin.y.into();
|
||||||
|
let bw: f32 = bounds.size.width.into();
|
||||||
|
let bh: f32 = bounds.size.height.into();
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn bounds_center(bounds: Bounds<Pixels>) -> (f32, f32) {
|
fn bounds_center(bounds: Bounds<Pixels>) -> (f32, f32) {
|
||||||
let ox: f32 = bounds.origin.x.into();
|
let ox: f32 = bounds.origin.x.into();
|
||||||
let oy: f32 = bounds.origin.y.into();
|
let oy: f32 = bounds.origin.y.into();
|
||||||
@@ -721,6 +767,19 @@ impl Render for AstrologyCanvas {
|
|||||||
CanvasMode::Thumbnails { items, .. } => render_thumbnails(&theme, items),
|
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(
|
||||||
|
|_b, _w, _cx| (),
|
||||||
|
move |bounds, _, window, _| paint_starfield(bounds, window, &theme_for_stars),
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.size_full();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("astrology-canvas-root")
|
.id("astrology-canvas-root")
|
||||||
.track_focus(&focus)
|
.track_focus(&focus)
|
||||||
@@ -735,12 +794,18 @@ impl Render for AstrologyCanvas {
|
|||||||
.on_scroll_wheel(cx.listener(Self::on_scroll))
|
.on_scroll_wheel(cx.listener(Self::on_scroll))
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(theme.bg_panel.clone())
|
.bg(theme.bg_panel.clone())
|
||||||
|
.relative()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(starfield)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.overflow_hidden()
|
.child(body),
|
||||||
.child(body)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,6 +908,12 @@ fn render_wheel(
|
|||||||
let mc_for_paint = render.midheaven_deg;
|
let mc_for_paint = render.midheaven_deg;
|
||||||
let visibility_for_paint = visible.clone();
|
let visibility_for_paint = visible.clone();
|
||||||
let entity_for_canvas = entity.clone();
|
let entity_for_canvas = entity.clone();
|
||||||
|
// Hover focus para el highlight de aspectos — solo cuando el hover
|
||||||
|
// es un Body (sobre un planeta), no sobre cusps ni aspectos.
|
||||||
|
let hover_focus_paint: Option<String> = match hover {
|
||||||
|
Some(HoverInfo::Body { symbol, .. }) => Some(symbol.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
let canvas_element = canvas(
|
let canvas_element = canvas(
|
||||||
move |_b: Bounds<Pixels>, _w, _cx| (),
|
move |_b: Bounds<Pixels>, _w, _cx| (),
|
||||||
move |bounds: Bounds<Pixels>, _, window, _| {
|
move |bounds: Bounds<Pixels>, _, window, _| {
|
||||||
@@ -858,12 +929,13 @@ fn render_wheel(
|
|||||||
rot_offset,
|
rot_offset,
|
||||||
radii,
|
radii,
|
||||||
&visibility_for_paint,
|
&visibility_for_paint,
|
||||||
|
hover_focus_paint.as_deref(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handlers de mouse — se registran cada frame contra el
|
// Handlers de mouse — se registran cada frame contra el
|
||||||
// window; GPUI los reemplaza al re-renderear. Jog-dial (LMB
|
// window; GPUI los reemplaza al re-renderear. LMB despacha
|
||||||
// sobre el anillo de signos) y pan (MMB en cualquier parte
|
// entre jog-dial (sobre el anillo) y pan (afuera). MMB es
|
||||||
// del canvas) coexisten porque consumen botones distintos.
|
// pan secundario para usuarios con scroll-mouse.
|
||||||
let entity_d = entity_for_canvas.clone();
|
let entity_d = entity_for_canvas.clone();
|
||||||
window.on_mouse_event(move |ev: &MouseDownEvent, _, _w, cx| {
|
window.on_mouse_event(move |ev: &MouseDownEvent, _, _w, cx| {
|
||||||
if !bounds.contains(&ev.position) {
|
if !bounds.contains(&ev.position) {
|
||||||
@@ -871,8 +943,9 @@ fn render_wheel(
|
|||||||
}
|
}
|
||||||
match ev.button {
|
match ev.button {
|
||||||
MouseButton::Left => {
|
MouseButton::Left => {
|
||||||
entity_d
|
entity_d.update(cx, |this, cx| {
|
||||||
.update(cx, |this, cx| this.on_jog_down(ev.position, bounds, cx));
|
this.on_primary_down(ev.position, bounds, cx)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
MouseButton::Middle => {
|
MouseButton::Middle => {
|
||||||
entity_d.update(cx, |this, cx| this.on_pan_down(ev.position, cx));
|
entity_d.update(cx, |this, cx| this.on_pan_down(ev.position, cx));
|
||||||
@@ -915,33 +988,17 @@ fn render_wheel(
|
|||||||
.w(px(wheel_size))
|
.w(px(wheel_size))
|
||||||
.h(px(wheel_size));
|
.h(px(wheel_size));
|
||||||
|
|
||||||
// Gradient sutil diagonal en el fondo del wheel — toque "místico
|
// El wheel ya no tiene bg propio — antes era un cuadrado con
|
||||||
// velado". En dark la alpha es muy baja (el fondo del panel ya es
|
// gradient que cortaba contra el fondo del panel; ahora el panel
|
||||||
// oscuro, no hace falta tinte fuerte). En light el panel es claro,
|
// (con su starfield encima en `render`) fluye continuo a través
|
||||||
// así que necesitamos alphas mayores para que el gradient se vea
|
// del área del wheel, dando el efecto de "rueda flotando en el
|
||||||
// como un fondo "papel teñido" y no se borre contra blanco.
|
// universo" en lugar de "rueda sobre placa cuadrada".
|
||||||
let (a0, a1) = if theme.is_dark {
|
|
||||||
(0.06, 0.03)
|
|
||||||
} else {
|
|
||||||
(0.18, 0.10)
|
|
||||||
};
|
|
||||||
let wheel_bg = linear_gradient(
|
|
||||||
155.0,
|
|
||||||
linear_color_stop(with_alpha(palette.dial_ring, a0), 0.0),
|
|
||||||
linear_color_stop(with_alpha(palette.angle_highlight, a1), 1.0),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut wheel = div()
|
let mut wheel = div()
|
||||||
.relative()
|
.relative()
|
||||||
.w(px(wheel_size))
|
.w(px(wheel_size))
|
||||||
.h(px(wheel_size))
|
.h(px(wheel_size))
|
||||||
// El parent del canvas centra con flex; aplicamos el pan como
|
|
||||||
// margin shift desde ese centrado natural. Positivo = a la
|
|
||||||
// derecha / abajo; negativo desplaza al lado opuesto.
|
|
||||||
.ml(px(view_pan_x))
|
.ml(px(view_pan_x))
|
||||||
.mt(px(view_pan_y))
|
.mt(px(view_pan_y))
|
||||||
.bg(wheel_bg)
|
|
||||||
.rounded(px(12.0))
|
|
||||||
.child(canvas_element);
|
.child(canvas_element);
|
||||||
|
|
||||||
// Sign glyphs.
|
// Sign glyphs.
|
||||||
@@ -987,17 +1044,19 @@ fn render_wheel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Planet glyphs: natal (en `bodies`) + overlays en sus rings
|
// Planet glyphs: natal en `bodies` + overlays (progression,
|
||||||
// (progression, solar_arc) con alpha + tamaño más chico para
|
// solar_arc) en sus rings, ambos con disco-halo para legibilidad
|
||||||
// diferenciarse visualmente del natal.
|
// contra cualquier fondo. El natal lleva un tamaño un poco mayor
|
||||||
|
// que los overlays para que se lea como "el cuerpo principal".
|
||||||
|
let halo_bg = glyph_halo(theme);
|
||||||
if visible.get(&LayerKind::Bodies).copied().unwrap_or(true) {
|
if visible.get(&LayerKind::Bodies).copied().unwrap_or(true) {
|
||||||
for layer in &render.layers {
|
for layer in &render.layers {
|
||||||
if matches!(layer.kind, LayerKind::Bodies) {
|
if matches!(layer.kind, LayerKind::Bodies) {
|
||||||
let is_natal = layer.module_id == "natal";
|
let is_natal = layer.module_id == "natal";
|
||||||
let ring = radii.body_ring(&layer.module_id);
|
let ring = radii.body_ring(&layer.module_id);
|
||||||
let alpha = if is_natal { 1.0 } else { 0.85 };
|
let alpha = if is_natal { 1.0 } else { 0.88 };
|
||||||
let font_size = if is_natal { 18.0 } else { 14.0 };
|
let font_size = if is_natal { 18.0 } else { 14.0 };
|
||||||
let box_size = if is_natal { 24.0 } else { 20.0 };
|
let disk_size = if is_natal { 26.0 } else { 22.0 };
|
||||||
for g in &layer.glyphs {
|
for g in &layer.glyphs {
|
||||||
let (x, y) = polar_to_screen(g.deg, asc, rot_offset, ring);
|
let (x, y) = polar_to_screen(g.deg, asc, rot_offset, ring);
|
||||||
let color = with_alpha(planet_color(palette, &g.symbol), alpha);
|
let color = with_alpha(planet_color(palette, &g.symbol), alpha);
|
||||||
@@ -1008,21 +1067,24 @@ fn render_wheel(
|
|||||||
if let Some(marker) = &g.dignity_marker {
|
if let Some(marker) = &g.dignity_marker {
|
||||||
glyph_text.push_str(marker);
|
glyph_text.push_str(marker);
|
||||||
}
|
}
|
||||||
wheel = wheel.child(centered_glyph(
|
wheel = wheel.child(planet_glyph(
|
||||||
cx_center + x,
|
cx_center + x,
|
||||||
cy_center + y,
|
cy_center + y,
|
||||||
box_size,
|
disk_size,
|
||||||
font_size,
|
font_size,
|
||||||
glyph_text.into(),
|
glyph_text.into(),
|
||||||
color,
|
color,
|
||||||
|
halo_bg,
|
||||||
|
with_alpha(color, 0.85),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Planet glyphs en el outer ring — transit o synastry, los dos
|
// Planet glyphs en el outer ring — transit o synastry (slot
|
||||||
// comparten ese slot (mutuamente excluyentes a nivel de Shell).
|
// compartido, mutuamente excluyentes a nivel de Shell). Disco un
|
||||||
|
// poco más chico que el natal — el outer es "secundario".
|
||||||
if visible.get(&LayerKind::Outer).copied().unwrap_or(true) {
|
if visible.get(&LayerKind::Outer).copied().unwrap_or(true) {
|
||||||
for layer in &render.layers {
|
for layer in &render.layers {
|
||||||
if matches!(layer.kind, LayerKind::Outer)
|
if matches!(layer.kind, LayerKind::Outer)
|
||||||
@@ -1030,19 +1092,21 @@ fn render_wheel(
|
|||||||
{
|
{
|
||||||
for g in &layer.glyphs {
|
for g in &layer.glyphs {
|
||||||
let (x, y) = polar_to_screen(g.deg, asc, rot_offset, radii.transits);
|
let (x, y) = polar_to_screen(g.deg, asc, rot_offset, radii.transits);
|
||||||
let color = with_alpha(planet_color(palette, &g.symbol), 0.9);
|
let color = with_alpha(planet_color(palette, &g.symbol), 0.92);
|
||||||
let glyph_text = if g.retrograde {
|
let glyph_text = if g.retrograde {
|
||||||
format!("{}ᴿ", planet_unicode(&g.symbol))
|
format!("{}ᴿ", planet_unicode(&g.symbol))
|
||||||
} else {
|
} else {
|
||||||
planet_unicode(&g.symbol).into()
|
planet_unicode(&g.symbol).into()
|
||||||
};
|
};
|
||||||
wheel = wheel.child(centered_glyph(
|
wheel = wheel.child(planet_glyph(
|
||||||
cx_center + x,
|
cx_center + x,
|
||||||
cy_center + y,
|
cy_center + y,
|
||||||
20.0,
|
20.0,
|
||||||
14.0,
|
13.0,
|
||||||
glyph_text.into(),
|
glyph_text.into(),
|
||||||
color,
|
color,
|
||||||
|
halo_bg,
|
||||||
|
with_alpha(color, 0.75),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1147,27 +1211,39 @@ fn render_wheel(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels ASC/MC/DESC/IC en el perímetro. Texto pequeño en el
|
// Labels ASC/MC/DESC/IC como pills en el perímetro — bg del halo
|
||||||
// margen exterior (radius * 1.05) para que no se monte con los
|
// + border y texto en `angle_highlight`. Más legibles que el
|
||||||
// glifos de los signos. Color angle_highlight para que el ojo los
|
// centered_glyph plano del fase anterior, en especial sobre
|
||||||
// reconozca como los cuatro ángulos cardinales.
|
// fondos claros donde el ámbar/oro de angle_highlight se diluye.
|
||||||
let angle_labels = [
|
let angle_labels = [
|
||||||
(asc, "ASC"),
|
(asc, "ASC"),
|
||||||
(render.midheaven_deg, "MC"),
|
(render.midheaven_deg, "MC"),
|
||||||
(render.descendant_deg, "DESC"),
|
(render.descendant_deg, "DESC"),
|
||||||
(render.imum_coeli_deg, "IC"),
|
(render.imum_coeli_deg, "IC"),
|
||||||
];
|
];
|
||||||
let label_r = r_outer * 1.06;
|
let label_r = r_outer * 1.08;
|
||||||
for (deg, label) in angle_labels {
|
for (deg, label) in angle_labels {
|
||||||
let (x, y) = polar_to_screen(deg, asc, rot_offset, label_r);
|
let (x, y) = polar_to_screen(deg, asc, rot_offset, label_r);
|
||||||
wheel = wheel.child(centered_glyph(
|
let pill_w = if label.len() > 2 { 38.0 } else { 30.0 };
|
||||||
cx_center + x,
|
let pill_h = 18.0;
|
||||||
cy_center + y,
|
wheel = wheel.child(
|
||||||
32.0,
|
div()
|
||||||
10.0,
|
.absolute()
|
||||||
label.into(),
|
.left(px(cx_center + x - pill_w / 2.0))
|
||||||
palette.angle_highlight,
|
.top(px(cy_center + y - pill_h / 2.0))
|
||||||
));
|
.w(px(pill_w))
|
||||||
|
.h(px(pill_h))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.rounded(px(9.0))
|
||||||
|
.bg(halo_bg)
|
||||||
|
.border_1()
|
||||||
|
.border_color(with_alpha(palette.angle_highlight, 0.85))
|
||||||
|
.text_size(px(11.0))
|
||||||
|
.text_color(palette.angle_highlight)
|
||||||
|
.child(SharedString::from(label)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Header + footer + indicador de tiempo ---
|
// --- Header + footer + indicador de tiempo ---
|
||||||
@@ -1487,6 +1563,10 @@ impl Radii {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
// `hover_focus`: symbol del planeta hovereado en este frame (si lo
|
||||||
|
// hay). Las líneas de aspecto que NO tocan a ese planeta se opacan
|
||||||
|
// para que el usuario lea claramente "qué afecta a qué". Si `None`,
|
||||||
|
// todas las líneas se pintan a alpha plena.
|
||||||
fn paint_wheel(
|
fn paint_wheel(
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
@@ -1498,18 +1578,23 @@ fn paint_wheel(
|
|||||||
rot_offset_deg: f32,
|
rot_offset_deg: f32,
|
||||||
radii: Radii,
|
radii: Radii,
|
||||||
visibility: &HashMap<LayerKind, bool>,
|
visibility: &HashMap<LayerKind, bool>,
|
||||||
|
hover_focus: Option<&str>,
|
||||||
) {
|
) {
|
||||||
let (cx, cy) = bounds_center(bounds);
|
let (cx, cy) = bounds_center(bounds);
|
||||||
let _ = theme;
|
|
||||||
let show = |k: LayerKind| visibility.get(&k).copied().unwrap_or(true);
|
let show = |k: LayerKind| visibility.get(&k).copied().unwrap_or(true);
|
||||||
|
|
||||||
// 1. Sectores zodiacales (parte del SignDial layer).
|
// 1. Sectores zodiacales (parte del SignDial layer).
|
||||||
if show(LayerKind::SignDial) {
|
if show(LayerKind::SignDial) {
|
||||||
paint_sign_sectors(window, cx, cy, &radii, palette, ascendant_deg, rot_offset_deg);
|
paint_sign_sectors(window, cx, cy, &radii, palette, ascendant_deg, rot_offset_deg);
|
||||||
|
|
||||||
// Anillos del dial.
|
// Anillos del dial con efecto 3D: highlight interior + base +
|
||||||
stroke_circle(window, cx, cy, radii.sign_outer, 1.5, palette.dial_ring);
|
// shadow exterior. El highlight es 1 px hacia el centro con
|
||||||
stroke_circle(window, cx, cy, radii.sign_inner, 1.0, palette.dial_ring);
|
// luminancia +0.18; la shadow 1 px hacia afuera con -0.18.
|
||||||
|
// El bevel central — varios strokes finos con alpha en bell
|
||||||
|
// curve entre sign_inner y sign_outer — da volumen al dial.
|
||||||
|
stroke_circle_3d(window, cx, cy, radii.sign_outer, 1.5, palette.dial_ring, theme);
|
||||||
|
stroke_circle_3d(window, cx, cy, radii.sign_inner, 1.0, palette.dial_ring, theme);
|
||||||
|
paint_dial_bevel(window, cx, cy, &radii, palette, theme);
|
||||||
|
|
||||||
// Cusps zodiacales cada 30°.
|
// Cusps zodiacales cada 30°.
|
||||||
for i in 0..12 {
|
for i in 0..12 {
|
||||||
@@ -1569,45 +1654,70 @@ fn paint_wheel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asc + MC extendidos hasta el centro con opacidad sutil.
|
// Cruz completa Asc-Desc + MC-IC, alpha bastante visible para
|
||||||
paint_radial_line(
|
// que orienten la lectura sin competir con cuerpos/aspectos.
|
||||||
window,
|
// 4 radios desde el centro: ASC, DESC (=asc+180), MC, IC
|
||||||
cx,
|
// (=mc+180). `paint_radial_line` con r_inner=0 pinta un radio
|
||||||
cy,
|
// del centro al borde — la cruz es la unión de los 4.
|
||||||
|
let axis_color = with_alpha(palette.angle_highlight, 0.55);
|
||||||
|
for axis_deg in [
|
||||||
ascendant_deg,
|
ascendant_deg,
|
||||||
ascendant_deg,
|
ascendant_deg + 180.0,
|
||||||
rot_offset_deg,
|
|
||||||
0.0,
|
|
||||||
radii.houses_outer,
|
|
||||||
with_alpha(palette.angle_highlight, 0.35),
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
paint_radial_line(
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
midheaven_deg,
|
midheaven_deg,
|
||||||
|
midheaven_deg + 180.0,
|
||||||
|
] {
|
||||||
|
paint_radial_line(
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
axis_deg,
|
||||||
ascendant_deg,
|
ascendant_deg,
|
||||||
rot_offset_deg,
|
rot_offset_deg,
|
||||||
0.0,
|
0.0,
|
||||||
radii.houses_outer,
|
radii.houses_outer,
|
||||||
with_alpha(palette.angle_highlight, 0.35),
|
axis_color,
|
||||||
1.0,
|
1.4,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Aspectos. Cada module_id usa su par de radios — natal-natal
|
// 3. Aspectos. Cada module_id usa su par de radios — natal-natal
|
||||||
// ambos en `aspects`, cross con transit en `bodies → transits`,
|
// ambos en `aspects`, cross con transit en `bodies → transits`,
|
||||||
// cross con progression en `bodies → progression`.
|
// cross con progression en `bodies → progression`.
|
||||||
if show(LayerKind::Aspects) {
|
if show(LayerKind::Aspects) {
|
||||||
|
let mono = palette.is_monochrome();
|
||||||
for layer in layers {
|
for layer in layers {
|
||||||
if matches!(layer.kind, LayerKind::Aspects) {
|
if matches!(layer.kind, LayerKind::Aspects) {
|
||||||
if let Geometry::Lines(segs) = &layer.geometry {
|
if let Geometry::Lines(segs) = &layer.geometry {
|
||||||
let (r_from, r_to) = radii.aspect_endpoints(&layer.module_id);
|
let (r_from, r_to) = radii.aspect_endpoints(&layer.module_id);
|
||||||
let is_cross = r_from != r_to;
|
let is_cross = r_from != r_to;
|
||||||
for seg in segs {
|
for seg in segs {
|
||||||
let color = aspect_color(palette, &seg.kind);
|
let base = aspect_color(palette, &seg.kind);
|
||||||
let color = with_alpha(color, color.a * seg.opacity);
|
let base = with_alpha(base, base.a * seg.opacity);
|
||||||
|
// Hover focus: si hay un planeta hovereado y
|
||||||
|
// este segmento NO lo toca, lo atenuamos al
|
||||||
|
// 18%; si lo toca o no hay hover, va pleno.
|
||||||
|
let touches_hover = hover_focus
|
||||||
|
.map(|sym| seg.from_body == sym || seg.to_body == sym)
|
||||||
|
.unwrap_or(true);
|
||||||
|
let factor = if touches_hover { 1.0 } else { 0.18 };
|
||||||
|
let color = with_alpha(base, base.a * factor);
|
||||||
|
let dash = if mono {
|
||||||
|
dash_pattern_for_kind(&seg.kind)
|
||||||
|
} 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
|
||||||
|
};
|
||||||
if is_cross {
|
if is_cross {
|
||||||
paint_cross_aspect_line(
|
paint_cross_aspect_line(
|
||||||
window,
|
window,
|
||||||
@@ -1620,6 +1730,7 @@ fn paint_wheel(
|
|||||||
r_from,
|
r_from,
|
||||||
r_to,
|
r_to,
|
||||||
color,
|
color,
|
||||||
|
dash,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
paint_aspect_line(
|
paint_aspect_line(
|
||||||
@@ -1632,6 +1743,8 @@ fn paint_wheel(
|
|||||||
rot_offset_deg,
|
rot_offset_deg,
|
||||||
r_from,
|
r_from,
|
||||||
color,
|
color,
|
||||||
|
dash,
|
||||||
|
width,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1640,11 +1753,13 @@ fn paint_wheel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Dots de cuerpos: natal en `bodies`, overlays en sus rings
|
// 4. Marcadores de posición exacta. Antes el dot era "el planeta";
|
||||||
// específicos (progression, solar_arc). Las luminarias natales
|
// ahora el glyph (con halo, en DOM) lo es. El círculo acá queda
|
||||||
// (Sol/Luna) llevan glow halo — invita la mística sin saturar.
|
// como marker de precisión angular — chico, alpha alta, sobre el
|
||||||
|
// anillo correspondiente. Glow se mantiene para Sol/Luna como
|
||||||
|
// toque místico, pero también reducido.
|
||||||
if show(LayerKind::Bodies) {
|
if show(LayerKind::Bodies) {
|
||||||
let dot_r = (radii.sign_outer * 0.018).max(2.0);
|
let dot_r = (radii.sign_outer * 0.009).max(1.5);
|
||||||
for layer in layers {
|
for layer in layers {
|
||||||
if matches!(layer.kind, LayerKind::Bodies) {
|
if matches!(layer.kind, LayerKind::Bodies) {
|
||||||
let ring = radii.body_ring(&layer.module_id);
|
let ring = radii.body_ring(&layer.module_id);
|
||||||
@@ -1654,7 +1769,7 @@ fn paint_wheel(
|
|||||||
let color = with_alpha(planet_color(palette, &g.symbol), alpha);
|
let color = with_alpha(planet_color(palette, &g.symbol), alpha);
|
||||||
let (x, y) = polar_to_screen(g.deg, ascendant_deg, rot_offset_deg, ring);
|
let (x, y) = polar_to_screen(g.deg, ascendant_deg, rot_offset_deg, ring);
|
||||||
if is_natal && (g.symbol == "sun" || g.symbol == "moon") {
|
if is_natal && (g.symbol == "sun" || g.symbol == "moon") {
|
||||||
paint_glow(window, cx + x, cy + y, dot_r, color);
|
paint_glow(window, cx + x, cy + y, dot_r * 1.8, color);
|
||||||
}
|
}
|
||||||
fill_circle(window, cx + x, cy + y, dot_r, color);
|
fill_circle(window, cx + x, cy + y, dot_r, color);
|
||||||
}
|
}
|
||||||
@@ -1699,24 +1814,27 @@ fn paint_wheel(
|
|||||||
&& OUTER_RING_MODULES.contains(&l.module_id.as_str())
|
&& OUTER_RING_MODULES.contains(&l.module_id.as_str())
|
||||||
});
|
});
|
||||||
if outer_active && show(LayerKind::Outer) {
|
if outer_active && show(LayerKind::Outer) {
|
||||||
stroke_circle(
|
let band = radii.sign_outer * 0.035;
|
||||||
|
stroke_circle_3d(
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
cy,
|
cy,
|
||||||
radii.transits + radii.sign_outer * 0.035,
|
radii.transits + band,
|
||||||
0.6,
|
0.7,
|
||||||
with_alpha(palette.dial_ring, 0.4),
|
with_alpha(palette.dial_ring, 0.55),
|
||||||
|
theme,
|
||||||
);
|
);
|
||||||
stroke_circle(
|
stroke_circle_3d(
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
cy,
|
cy,
|
||||||
radii.transits - radii.sign_outer * 0.035,
|
radii.transits - band,
|
||||||
0.6,
|
0.7,
|
||||||
with_alpha(palette.dial_ring, 0.4),
|
with_alpha(palette.dial_ring, 0.55),
|
||||||
|
theme,
|
||||||
);
|
);
|
||||||
|
|
||||||
let dot_r = (radii.sign_outer * 0.017).max(2.0);
|
let dot_r = (radii.sign_outer * 0.008).max(1.5);
|
||||||
for layer in layers {
|
for layer in layers {
|
||||||
if matches!(layer.kind, LayerKind::Outer)
|
if matches!(layer.kind, LayerKind::Outer)
|
||||||
&& (OUTER_RING_MODULES.contains(&layer.module_id.as_str()))
|
&& (OUTER_RING_MODULES.contains(&layer.module_id.as_str()))
|
||||||
@@ -1853,15 +1971,12 @@ fn paint_aspect_line(
|
|||||||
rot_offset_deg: f32,
|
rot_offset_deg: f32,
|
||||||
r: f32,
|
r: f32,
|
||||||
color: Hsla,
|
color: Hsla,
|
||||||
|
dash: Option<(f32, f32)>,
|
||||||
|
width: f32,
|
||||||
) {
|
) {
|
||||||
let (xa, ya) = polar_to_screen(a_deg, ascendant_deg, rot_offset_deg, r);
|
let (xa, ya) = polar_to_screen(a_deg, ascendant_deg, rot_offset_deg, r);
|
||||||
let (xb, yb) = polar_to_screen(b_deg, ascendant_deg, rot_offset_deg, r);
|
let (xb, yb) = polar_to_screen(b_deg, ascendant_deg, rot_offset_deg, r);
|
||||||
let mut builder = PathBuilder::stroke(px(1.0));
|
paint_segment(window, cx + xa, cy + ya, cx + xb, cy + yb, color, dash, width);
|
||||||
builder.move_to(point(px(cx + xa), px(cy + ya)));
|
|
||||||
builder.line_to(point(px(cx + xb), px(cy + yb)));
|
|
||||||
if let Ok(path) = builder.build() {
|
|
||||||
window.paint_path(path, color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Línea de aspecto natal ↔ tránsito: extremos en radios distintos.
|
/// Línea de aspecto natal ↔ tránsito: extremos en radios distintos.
|
||||||
@@ -1880,14 +1995,82 @@ fn paint_cross_aspect_line(
|
|||||||
r_from: f32,
|
r_from: f32,
|
||||||
r_to: f32,
|
r_to: f32,
|
||||||
color: Hsla,
|
color: Hsla,
|
||||||
|
dash: Option<(f32, f32)>,
|
||||||
) {
|
) {
|
||||||
let (xa, ya) = polar_to_screen(natal_deg, ascendant_deg, rot_offset_deg, r_from);
|
let (xa, ya) = polar_to_screen(natal_deg, ascendant_deg, rot_offset_deg, r_from);
|
||||||
let (xb, yb) = polar_to_screen(transit_deg, ascendant_deg, rot_offset_deg, r_to);
|
let (xb, yb) = polar_to_screen(transit_deg, ascendant_deg, rot_offset_deg, r_to);
|
||||||
let mut builder = PathBuilder::stroke(px(0.7));
|
paint_segment(window, cx + xa, cy + ya, cx + xb, cy + yb, color, dash, 0.7);
|
||||||
builder.move_to(point(px(cx + xa), px(cy + ya)));
|
}
|
||||||
builder.line_to(point(px(cx + xb), px(cy + yb)));
|
|
||||||
if let Ok(path) = builder.build() {
|
/// Pinta un segmento entre dos puntos. Si `dash` es `Some((on, off))`,
|
||||||
window.paint_path(path, color);
|
/// itera el vector pintando trechos de `on` px con gaps de `off` px.
|
||||||
|
/// Si `None`, una sola línea continua. Usado por todos los aspect
|
||||||
|
/// painters — el dash pattern es la forma de distinguir kinds en
|
||||||
|
/// el theme BW (donde el color no sirve).
|
||||||
|
fn paint_segment(
|
||||||
|
window: &mut Window,
|
||||||
|
x0: f32,
|
||||||
|
y0: f32,
|
||||||
|
x1: f32,
|
||||||
|
y1: f32,
|
||||||
|
color: Hsla,
|
||||||
|
dash: Option<(f32, f32)>,
|
||||||
|
width: f32,
|
||||||
|
) {
|
||||||
|
let Some((on, off)) = dash else {
|
||||||
|
let mut b = PathBuilder::stroke(px(width));
|
||||||
|
b.move_to(point(px(x0), px(y0)));
|
||||||
|
b.line_to(point(px(x1), px(y1)));
|
||||||
|
if let Ok(p) = b.build() {
|
||||||
|
window.paint_path(p, color);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let dx = x1 - x0;
|
||||||
|
let dy = y1 - y0;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len < 0.1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ux = dx / len;
|
||||||
|
let uy = dy / len;
|
||||||
|
let step = on + off;
|
||||||
|
if step < 0.1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut t = 0.0;
|
||||||
|
while t < len {
|
||||||
|
let t_end = (t + on).min(len);
|
||||||
|
let sx = x0 + ux * t;
|
||||||
|
let sy = y0 + uy * t;
|
||||||
|
let ex = x0 + ux * t_end;
|
||||||
|
let ey = y0 + uy * t_end;
|
||||||
|
let mut b = PathBuilder::stroke(px(width));
|
||||||
|
b.move_to(point(px(sx), px(sy)));
|
||||||
|
b.line_to(point(px(ex), px(ey)));
|
||||||
|
if let Ok(p) = b.build() {
|
||||||
|
window.paint_path(p, color);
|
||||||
|
}
|
||||||
|
t += step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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:
|
||||||
|
/// - conjunction/opposition: sólido (más peso visual, son los
|
||||||
|
/// aspectos "fuertes")
|
||||||
|
/// - square: dash medio (4 on / 3 off)
|
||||||
|
/// - trine: dash largo (8 on / 2 off) — casi sólido pero distinguible
|
||||||
|
/// - sextile: dotted (1.5 on / 3 off)
|
||||||
|
/// - minor: dotted finísimo (1 on / 4 off)
|
||||||
|
fn dash_pattern_for_kind(kind: &str) -> Option<(f32, f32)> {
|
||||||
|
match kind {
|
||||||
|
"conjunction" | "opposition" => None,
|
||||||
|
"square" => Some((4.0, 3.0)),
|
||||||
|
"trine" => Some((8.0, 2.0)),
|
||||||
|
"sextile" => Some((1.5, 3.0)),
|
||||||
|
_ => Some((1.0, 4.0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1948,10 +2131,115 @@ fn centered_glyph(
|
|||||||
.child(text)
|
.child(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Glyph de planeta con disco-halo detrás del char. El disco viene en
|
||||||
|
/// `disk_bg` (semi-opaco para que se vea a través el fondo del wheel)
|
||||||
|
/// y `disk_border` (típicamente el color del planeta). El char por
|
||||||
|
/// dentro va en `text_color` — recomendado el color del planeta sobre
|
||||||
|
/// disco neutro, o color contrastante sobre disco coloreado.
|
||||||
|
fn planet_glyph(
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
disk_size: f32,
|
||||||
|
font_size: f32,
|
||||||
|
text: SharedString,
|
||||||
|
text_color: Hsla,
|
||||||
|
disk_bg: Hsla,
|
||||||
|
disk_border: Hsla,
|
||||||
|
) -> gpui::Div {
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(x - disk_size / 2.0))
|
||||||
|
.top(px(y - disk_size / 2.0))
|
||||||
|
.w(px(disk_size))
|
||||||
|
.h(px(disk_size))
|
||||||
|
.rounded_full()
|
||||||
|
.bg(disk_bg)
|
||||||
|
.border_1()
|
||||||
|
.border_color(disk_border)
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_size(px(font_size))
|
||||||
|
.text_color(text_color)
|
||||||
|
.child(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color HSL semi-opaco para los halos de los glyphs — derivado del
|
||||||
|
/// theme. En dark va casi negro; en light casi blanco. Alpha alta para
|
||||||
|
/// que el char quede legible contra cualquier cosa que haya detrás
|
||||||
|
/// (anillo, líneas de aspecto, starfield).
|
||||||
|
fn glyph_halo(theme: &Theme) -> Hsla {
|
||||||
|
if theme.is_dark {
|
||||||
|
hsla(0.0, 0.0, 0.07, 0.92)
|
||||||
|
} else {
|
||||||
|
hsla(0.0, 0.0, 0.97, 0.92)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn with_alpha(c: Hsla, a: f32) -> Hsla {
|
fn with_alpha(c: Hsla, a: f32) -> Hsla {
|
||||||
hsla(c.h, c.s, c.l, a.clamp(0.0, 1.0))
|
hsla(c.h, c.s, c.l, a.clamp(0.0, 1.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Devuelve `c` con la luminancia modificada por `delta` (clamp 0..1).
|
||||||
|
/// Útil para derivar highlight (+luma) y shadow (-luma) de un color
|
||||||
|
/// base manteniendo hue y saturación — efecto bevel/3D barato.
|
||||||
|
fn adjust_luma(c: Hsla, delta: f32) -> Hsla {
|
||||||
|
hsla(c.h, c.s, (c.l + delta).clamp(0.0, 1.0), c.a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stroke con efecto embossed: 3 trazos concéntricos. El highlight va
|
||||||
|
/// 0.7 px hacia el centro con luminancia subida; el principal en `r`;
|
||||||
|
/// el shadow 0.7 px hacia afuera con luminancia bajada. La dirección
|
||||||
|
/// del bevel depende del theme: en dark el highlight es exterior (luz
|
||||||
|
/// "desde arriba"), en light interior (sombra "desde arriba" hacia
|
||||||
|
/// el centro).
|
||||||
|
fn stroke_circle_3d(
|
||||||
|
window: &mut Window,
|
||||||
|
cx: f32,
|
||||||
|
cy: f32,
|
||||||
|
r: f32,
|
||||||
|
width: f32,
|
||||||
|
color: Hsla,
|
||||||
|
theme: &Theme,
|
||||||
|
) {
|
||||||
|
let (hl_offset, sh_offset) = if theme.is_dark {
|
||||||
|
(-0.7, 0.7)
|
||||||
|
} else {
|
||||||
|
(0.7, -0.7)
|
||||||
|
};
|
||||||
|
let hl = with_alpha(adjust_luma(color, 0.20), color.a * 0.55);
|
||||||
|
let sh = with_alpha(adjust_luma(color, -0.18), color.a * 0.55);
|
||||||
|
stroke_circle(window, cx, cy, r + hl_offset, (width * 0.7).max(0.4), hl);
|
||||||
|
stroke_circle(window, cx, cy, r, width, color);
|
||||||
|
stroke_circle(window, cx, cy, r + sh_offset, (width * 0.7).max(0.4), sh);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bevel central del anillo de signos: ~10 strokes finos entre
|
||||||
|
/// sign_inner y sign_outer, con alpha en bell curve (máximo en el
|
||||||
|
/// medio, decae hacia los bordes). Genera la sensación de volumen
|
||||||
|
/// sin pintar gradient radial (no soportado en gpui canvas).
|
||||||
|
fn paint_dial_bevel(
|
||||||
|
window: &mut Window,
|
||||||
|
cx: f32,
|
||||||
|
cy: f32,
|
||||||
|
radii: &Radii,
|
||||||
|
palette: &AstroPalette,
|
||||||
|
theme: &Theme,
|
||||||
|
) {
|
||||||
|
let steps = 10;
|
||||||
|
let base = if theme.is_dark { 0.07 } else { 0.10 };
|
||||||
|
let color = palette.dial_ring;
|
||||||
|
for i in 0..steps {
|
||||||
|
let t = (i as f32 + 0.5) / steps as f32;
|
||||||
|
let r = radii.sign_inner + (radii.sign_outer - radii.sign_inner) * t;
|
||||||
|
// Bell curve simétrica: |t-0.5|*2 da 0..1 desde el centro, lo
|
||||||
|
// invertimos para que el centro tenga peso máximo.
|
||||||
|
let bell = 1.0 - ((t - 0.5).abs() * 2.0);
|
||||||
|
let a = base * bell;
|
||||||
|
stroke_circle(window, cx, cy, r, 1.0, with_alpha(color, a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sign_unicode(name: &str) -> &'static str {
|
fn sign_unicode(name: &str) -> &'static str {
|
||||||
match name {
|
match name {
|
||||||
"aries" => "♈",
|
"aries" => "♈",
|
||||||
|
|||||||
@@ -215,14 +215,132 @@ impl AstroPalette {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_theme(theme: &yahweh_theme::Theme) -> Self {
|
/// Variante "papel coloreado" — para preview de impresión. Hue de
|
||||||
if theme.is_dark {
|
/// cada slot mantenido; luminancia 0.26-0.34 y saturación alta
|
||||||
Self::dark()
|
/// para que sobreviva el ink-bleed sin perder identidad. Sin glow.
|
||||||
} else {
|
pub fn print_color() -> Self {
|
||||||
Self::light()
|
Self {
|
||||||
|
is_dark: false,
|
||||||
|
|
||||||
|
fire: hsla(11.0 / 360.0, 0.78, 0.34, 1.0),
|
||||||
|
earth: hsla(95.0 / 360.0, 0.55, 0.26, 1.0),
|
||||||
|
air: hsla(48.0 / 360.0, 0.78, 0.34, 1.0),
|
||||||
|
water: hsla(210.0 / 360.0, 0.72, 0.32, 1.0),
|
||||||
|
|
||||||
|
cardinal: hsla(340.0 / 360.0, 0.65, 0.34, 1.0),
|
||||||
|
fixed: hsla(258.0 / 360.0, 0.55, 0.32, 1.0),
|
||||||
|
mutable: hsla(170.0 / 360.0, 0.55, 0.28, 1.0),
|
||||||
|
|
||||||
|
sun: hsla(35.0 / 360.0, 0.95, 0.34, 1.0),
|
||||||
|
moon: hsla(220.0 / 360.0, 0.35, 0.34, 1.0),
|
||||||
|
mercury: hsla(140.0 / 360.0, 0.55, 0.28, 1.0),
|
||||||
|
venus: hsla(330.0 / 360.0, 0.65, 0.36, 1.0),
|
||||||
|
mars: hsla(8.0 / 360.0, 0.85, 0.34, 1.0),
|
||||||
|
jupiter: hsla(38.0 / 360.0, 0.85, 0.34, 1.0),
|
||||||
|
saturn: hsla(28.0 / 360.0, 0.30, 0.26, 1.0),
|
||||||
|
uranus: hsla(195.0 / 360.0, 0.75, 0.34, 1.0),
|
||||||
|
neptune: hsla(225.0 / 360.0, 0.65, 0.34, 1.0),
|
||||||
|
pluto: hsla(280.0 / 360.0, 0.55, 0.28, 1.0),
|
||||||
|
chiron: hsla(75.0 / 360.0, 0.42, 0.30, 1.0),
|
||||||
|
north_node: hsla(35.0 / 360.0, 0.55, 0.36, 1.0),
|
||||||
|
south_node: hsla(35.0 / 360.0, 0.30, 0.28, 1.0),
|
||||||
|
lilith: hsla(310.0 / 360.0, 0.60, 0.26, 1.0),
|
||||||
|
|
||||||
|
conjunction: hsla(45.0 / 360.0, 0.75, 0.32, 1.0),
|
||||||
|
sextile: hsla(195.0 / 360.0, 0.70, 0.32, 1.0),
|
||||||
|
square: hsla(8.0 / 360.0, 0.85, 0.34, 1.0),
|
||||||
|
trine: hsla(140.0 / 360.0, 0.65, 0.28, 1.0),
|
||||||
|
opposition: hsla(280.0 / 360.0, 0.65, 0.36, 1.0),
|
||||||
|
minor_aspect: hsla(220.0 / 360.0, 0.40, 0.40, 0.85),
|
||||||
|
|
||||||
|
dial_ring: hsla(40.0 / 360.0, 0.30, 0.22, 1.0),
|
||||||
|
house_cusp: hsla(40.0 / 360.0, 0.20, 0.28, 0.90),
|
||||||
|
angle_highlight: hsla(15.0 / 360.0, 0.85, 0.36, 1.0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Variante "papel B&N" — preview de impresión monocromática.
|
||||||
|
/// TODO los slots de planeta y aspecto se reducen a niveles de
|
||||||
|
/// gris. El canvas se encarga de diferenciar aspectos por dash
|
||||||
|
/// pattern y planetas por glyph (el unicode astronómico es
|
||||||
|
/// distintivo aunque pierda color).
|
||||||
|
pub fn print_bw() -> Self {
|
||||||
|
// Tres niveles funcionales: muy oscuro (texto, glyphs
|
||||||
|
// principales), medio (cusps, líneas), claro (fondos, minors).
|
||||||
|
let ink_strong = hsla(0.0, 0.0, 0.10, 1.0);
|
||||||
|
let ink_mid = hsla(0.0, 0.0, 0.30, 1.0);
|
||||||
|
let ink_soft = hsla(0.0, 0.0, 0.50, 0.90);
|
||||||
|
let ink_faint = hsla(0.0, 0.0, 0.55, 0.75);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
is_dark: false,
|
||||||
|
|
||||||
|
fire: ink_strong,
|
||||||
|
earth: ink_strong,
|
||||||
|
air: ink_strong,
|
||||||
|
water: ink_strong,
|
||||||
|
|
||||||
|
cardinal: ink_mid,
|
||||||
|
fixed: ink_mid,
|
||||||
|
mutable: ink_mid,
|
||||||
|
|
||||||
|
// Planetas: todos en ink_strong para que los glyphs se
|
||||||
|
// lean fuerte. El usuario distingue por el unicode
|
||||||
|
// astronómico, no por hue.
|
||||||
|
sun: ink_strong,
|
||||||
|
moon: ink_strong,
|
||||||
|
mercury: ink_strong,
|
||||||
|
venus: ink_strong,
|
||||||
|
mars: ink_strong,
|
||||||
|
jupiter: ink_strong,
|
||||||
|
saturn: ink_strong,
|
||||||
|
uranus: ink_strong,
|
||||||
|
neptune: ink_strong,
|
||||||
|
pluto: ink_strong,
|
||||||
|
chiron: ink_mid,
|
||||||
|
north_node: ink_mid,
|
||||||
|
south_node: ink_mid,
|
||||||
|
lilith: ink_mid,
|
||||||
|
|
||||||
|
// Aspectos: el color es uniforme; la diferenciación es por
|
||||||
|
// dash pattern en el painter (square=dashed, trine=solid,
|
||||||
|
// sextile=dotted, etc.). Acá solo damos el "intensity"
|
||||||
|
// base que el painter modula.
|
||||||
|
conjunction: ink_strong,
|
||||||
|
sextile: ink_mid,
|
||||||
|
square: ink_strong,
|
||||||
|
trine: ink_mid,
|
||||||
|
opposition: ink_strong,
|
||||||
|
minor_aspect: ink_faint,
|
||||||
|
|
||||||
|
dial_ring: ink_mid,
|
||||||
|
house_cusp: ink_soft,
|
||||||
|
angle_highlight: ink_strong,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_theme(theme: &yahweh_theme::Theme) -> Self {
|
||||||
|
// Dispatcher por nombre para los themes "papel"; el resto cae
|
||||||
|
// al binary dark/light según `is_dark`. Mantenemos el match
|
||||||
|
// case-insensitive por defensa contra cambios de naming.
|
||||||
|
match theme.name {
|
||||||
|
"Print Color" => Self::print_color(),
|
||||||
|
"Print B&W" => Self::print_bw(),
|
||||||
|
_ if theme.is_dark => Self::dark(),
|
||||||
|
_ => Self::light(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve `true` si la paleta es monocromática — los painters
|
||||||
|
/// la usan para activar dash patterns en lugar de diferenciar
|
||||||
|
/// aspectos por color.
|
||||||
|
pub fn is_monochrome(&self) -> bool {
|
||||||
|
// Heurística simple: si conjunction y square (que en color
|
||||||
|
// siempre tienen hues distintos) tienen el mismo hue,
|
||||||
|
// estamos en BW.
|
||||||
|
(self.conjunction.h - self.square.h).abs() < 1e-3
|
||||||
|
}
|
||||||
|
|
||||||
pub fn element(&self, e: Element) -> Hsla {
|
pub fn element(&self, e: Element) -> Hsla {
|
||||||
match e {
|
match e {
|
||||||
Element::Fire => self.fire,
|
Element::Fire => self.fire,
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ impl Theme {
|
|||||||
Self::flat_dark(),
|
Self::flat_dark(),
|
||||||
Self::solarized_light(),
|
Self::solarized_light(),
|
||||||
Self::high_contrast(),
|
Self::high_contrast(),
|
||||||
|
Self::print_color(),
|
||||||
|
Self::print_bw(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,6 +371,71 @@ impl Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **Print Color** — preview de impresión a color sobre papel.
|
||||||
|
/// Fondo crema cálido (#f7f4ea-ish), texto y ornamentos en
|
||||||
|
/// luminancias bajas para que sobrevivan ink-bleed. Sin gradientes
|
||||||
|
/// (los gradients no imprimen bien) y sin glow.
|
||||||
|
pub fn print_color() -> Self {
|
||||||
|
let bg_app: Background = hsla(42.0 / 360.0, 0.30, 0.94, 1.0).into();
|
||||||
|
let bg_panel: Background = hsla(40.0 / 360.0, 0.25, 0.97, 1.0).into();
|
||||||
|
let bg_panel_alt: Background = hsla(40.0 / 360.0, 0.20, 0.92, 1.0).into();
|
||||||
|
Self {
|
||||||
|
name: "Print Color",
|
||||||
|
is_dark: false,
|
||||||
|
bg_app,
|
||||||
|
bg_panel,
|
||||||
|
bg_panel_alt,
|
||||||
|
bg_row_hover: hsla(40.0 / 360.0, 0.30, 0.86, 0.70),
|
||||||
|
bg_row_active: hsla(35.0 / 360.0, 0.45, 0.78, 0.85),
|
||||||
|
fg_text: hsla(30.0 / 360.0, 0.15, 0.18, 1.0),
|
||||||
|
fg_muted: hsla(30.0 / 360.0, 0.12, 0.40, 1.0),
|
||||||
|
fg_disabled: hsla(30.0 / 360.0, 0.08, 0.62, 1.0),
|
||||||
|
accent: hsla(15.0 / 360.0, 0.70, 0.40, 1.0),
|
||||||
|
accent_strong: hsla(355.0 / 360.0, 0.78, 0.36, 1.0),
|
||||||
|
border: hsla(40.0 / 360.0, 0.22, 0.82, 1.0),
|
||||||
|
border_strong: hsla(30.0 / 360.0, 0.30, 0.55, 1.0),
|
||||||
|
marker_palette: vec![
|
||||||
|
hsla(15.0 / 360.0, 0.70, 0.35, 0.30),
|
||||||
|
hsla(210.0 / 360.0, 0.65, 0.35, 0.30),
|
||||||
|
hsla(140.0 / 360.0, 0.55, 0.30, 0.30),
|
||||||
|
hsla(285.0 / 360.0, 0.55, 0.38, 0.30),
|
||||||
|
hsla(40.0 / 360.0, 0.85, 0.40, 0.30),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Print B&W** — preview de impresión monocromática. Fondo
|
||||||
|
/// blanco puro, todo en escala de grises. Cualquier slot que
|
||||||
|
/// dependa de "color" en widgets astrológicos se diferencia por
|
||||||
|
/// forma o por dash pattern, no por tinte.
|
||||||
|
pub fn print_bw() -> Self {
|
||||||
|
let bg_app: Background = hsla(0.0, 0.0, 1.00, 1.0).into();
|
||||||
|
let bg_panel: Background = hsla(0.0, 0.0, 0.99, 1.0).into();
|
||||||
|
let bg_panel_alt: Background = hsla(0.0, 0.0, 0.95, 1.0).into();
|
||||||
|
Self {
|
||||||
|
name: "Print B&W",
|
||||||
|
is_dark: false,
|
||||||
|
bg_app,
|
||||||
|
bg_panel,
|
||||||
|
bg_panel_alt,
|
||||||
|
bg_row_hover: hsla(0.0, 0.0, 0.88, 0.85),
|
||||||
|
bg_row_active: hsla(0.0, 0.0, 0.78, 0.95),
|
||||||
|
fg_text: hsla(0.0, 0.0, 0.10, 1.0),
|
||||||
|
fg_muted: hsla(0.0, 0.0, 0.40, 1.0),
|
||||||
|
fg_disabled: hsla(0.0, 0.0, 0.65, 1.0),
|
||||||
|
accent: hsla(0.0, 0.0, 0.20, 1.0),
|
||||||
|
accent_strong: hsla(0.0, 0.0, 0.05, 1.0),
|
||||||
|
border: hsla(0.0, 0.0, 0.80, 1.0),
|
||||||
|
border_strong: hsla(0.0, 0.0, 0.40, 1.0),
|
||||||
|
marker_palette: vec![
|
||||||
|
hsla(0.0, 0.0, 0.30, 0.35),
|
||||||
|
hsla(0.0, 0.0, 0.50, 0.35),
|
||||||
|
hsla(0.0, 0.0, 0.20, 0.35),
|
||||||
|
hsla(0.0, 0.0, 0.60, 0.35),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// **High Contrast** — accesibilidad. Negro puro con texto blanco y
|
/// **High Contrast** — accesibilidad. Negro puro con texto blanco y
|
||||||
/// ornamentos amarillo/verde fuertes. Suficientemente diferente para
|
/// ornamentos amarillo/verde fuertes. Suficientemente diferente para
|
||||||
/// notar inmediatamente al usar el switcher.
|
/// notar inmediatamente al usar el switcher.
|
||||||
|
|||||||
Reference in New Issue
Block a user