Files
brahman/crates/modules/cosmobiologia/cosmobiologia-render/src/palette.rs
T
sergio 86fb6ae20b feat(cosmobiologia-render): compose_wheel rico con palette + dial 3D + spread + coord labels
El render agnóstico ya no es un esqueleto — porta al WASM la mayoría
de los detalles visuales que tenía solo el canvas gpui nativo:

- palette.rs: Palette dark/light replicando AstroPalette del theme
  nativo, pero en Rgba (no Hsla de gpui). Métodos planet/aspect/sign
  para resolver color por id simbólico, + house_ring con hue-shift.
- CompositionOpts extendido: palette, dial_3d, draw_ascensional_cross,
  show_coord_labels, show_minor_aspects. Defaults razonables.
- compose_wheel ahora dibuja: background panel, dial 3D bevel (4
  strokes concéntricos con alpha decreciente), subdivisiones cada 10°
  con sign boundaries reforzados, signos con color elemental, casas
  topocéntricas + geocéntricas en sus rings canónicos, cuerpos con
  spread anti-solapamiento + clusters + disco coloreado por planeta,
  coord labels "DD°MM'" en natal, aspectos con width inversa al
  orbe + filtrado opcional de minors, cruz ascensional dashed +
  pills ASC/MC/DESC/IC.
- cosmobiologia-web: nuevo render_model_to_svg_themed(dark: bool)
  para que el cliente JS elija palette según preferencia del UA.

Tests del módulo math siguen verdes (10/10). Smoke test del server:
/api/sky.svg ahora emite 22 circles, 77 lines, 52 texts con paleta
real (vs ~6 circles, 24 lines, 36 texts del esqueleto previo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 01:41:36 +00:00

222 lines
7.7 KiB
Rust

//! Paleta astrológica agnóstica (`Rgba`, no `Hsla` de gpui). Replica
//! los slots de `cosmobiologia-theme::AstroPalette` con `dark()` y
//! `light()`, sin arrastrar dependencia de gpui. El canvas nativo
//! traduce desde su `AstroPalette` Hsla; el cliente WASM usa esta
//! palette directamente.
use crate::draw::Rgba;
/// Color en HSL `[0..1]^4`, helper local para construir la palette
/// con la misma convención que el theme nativo (que era Hsla de gpui).
fn hsla(h_deg: f32, s: f32, l: f32, a: f32) -> Rgba {
// Conversión HSL → RGB (algoritmo estándar). H en grados.
let h = h_deg / 360.0;
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let h6 = (h * 6.0).rem_euclid(6.0);
let x = c * (1.0 - (h6 % 2.0 - 1.0).abs());
let (r1, g1, b1) = match h6 as i32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
let m = l - c / 2.0;
Rgba {
r: (r1 + m).clamp(0.0, 1.0),
g: (g1 + m).clamp(0.0, 1.0),
b: (b1 + m).clamp(0.0, 1.0),
a,
}
}
/// Paleta astrológica completa. Mismos slots que el theme nativo —
/// permite que el cliente WASM y el canvas gpui generen las mismas
/// decisiones de color en su superficie.
#[derive(Debug, Clone, Copy)]
pub struct Palette {
pub is_dark: bool,
// Elementos
pub fire: Rgba,
pub earth: Rgba,
pub air: Rgba,
pub water: Rgba,
// Planetas
pub sun: Rgba,
pub moon: Rgba,
pub mercury: Rgba,
pub venus: Rgba,
pub mars: Rgba,
pub jupiter: Rgba,
pub saturn: Rgba,
pub uranus: Rgba,
pub neptune: Rgba,
pub pluto: Rgba,
pub chiron: Rgba,
pub north_node: Rgba,
pub south_node: Rgba,
pub lilith: Rgba,
// Aspectos
pub conjunction: Rgba,
pub sextile: Rgba,
pub square: Rgba,
pub trine: Rgba,
pub opposition: Rgba,
pub minor_aspect: Rgba,
// Estructura
pub dial_ring: Rgba,
pub house_cusp: Rgba,
pub angle_highlight: Rgba,
// Estructura del lienzo (background panel / texto)
pub bg_panel: Rgba,
pub fg_text: Rgba,
pub fg_muted: Rgba,
}
impl Palette {
/// Paleta dark — equivalente a `AstroPalette::dark()` del theme
/// nativo.
pub fn dark() -> Self {
Self {
is_dark: true,
fire: hsla(11.0, 0.78, 0.58, 1.0),
earth: hsla(95.0, 0.40, 0.48, 1.0),
air: hsla(48.0, 0.72, 0.66, 1.0),
water: hsla(210.0, 0.68, 0.58, 1.0),
sun: hsla(45.0, 0.92, 0.62, 1.0),
moon: hsla(220.0, 0.25, 0.85, 1.0),
mercury: hsla(140.0, 0.40, 0.62, 1.0),
venus: hsla(330.0, 0.55, 0.70, 1.0),
mars: hsla(8.0, 0.78, 0.55, 1.0),
jupiter: hsla(38.0, 0.72, 0.62, 1.0),
saturn: hsla(28.0, 0.20, 0.50, 1.0),
uranus: hsla(195.0, 0.65, 0.62, 1.0),
neptune: hsla(225.0, 0.55, 0.66, 1.0),
pluto: hsla(280.0, 0.40, 0.45, 1.0),
chiron: hsla(75.0, 0.30, 0.55, 1.0),
north_node: hsla(35.0, 0.35, 0.70, 1.0),
south_node: hsla(35.0, 0.20, 0.45, 1.0),
lilith: hsla(310.0, 0.45, 0.40, 1.0),
conjunction: hsla(50.0, 0.65, 0.70, 0.85),
sextile: hsla(195.0, 0.60, 0.62, 0.75),
square: hsla(8.0, 0.75, 0.58, 0.85),
trine: hsla(140.0, 0.55, 0.55, 0.80),
opposition: hsla(280.0, 0.55, 0.62, 0.85),
minor_aspect: hsla(220.0, 0.20, 0.55, 0.55),
dial_ring: hsla(40.0, 0.18, 0.78, 0.85),
house_cusp: hsla(40.0, 0.12, 0.55, 0.60),
angle_highlight: hsla(50.0, 0.95, 0.65, 1.0),
bg_panel: hsla(245.0, 0.28, 0.10, 1.0),
fg_text: hsla(210.0, 0.35, 0.88, 1.0),
fg_muted: hsla(215.0, 0.22, 0.58, 1.0),
}
}
/// Paleta light — análoga a `AstroPalette::light()`.
pub fn light() -> Self {
Self {
is_dark: false,
fire: hsla(11.0, 0.65, 0.42, 1.0),
earth: hsla(95.0, 0.45, 0.30, 1.0),
air: hsla(48.0, 0.55, 0.42, 1.0),
water: hsla(210.0, 0.60, 0.38, 1.0),
sun: hsla(38.0, 0.85, 0.45, 1.0),
moon: hsla(220.0, 0.22, 0.45, 1.0),
mercury: hsla(140.0, 0.45, 0.36, 1.0),
venus: hsla(330.0, 0.55, 0.45, 1.0),
mars: hsla(8.0, 0.75, 0.40, 1.0),
jupiter: hsla(38.0, 0.72, 0.42, 1.0),
saturn: hsla(28.0, 0.25, 0.30, 1.0),
uranus: hsla(195.0, 0.65, 0.40, 1.0),
neptune: hsla(225.0, 0.55, 0.42, 1.0),
pluto: hsla(280.0, 0.45, 0.30, 1.0),
chiron: hsla(75.0, 0.32, 0.35, 1.0),
north_node: hsla(35.0, 0.45, 0.45, 1.0),
south_node: hsla(35.0, 0.20, 0.30, 1.0),
lilith: hsla(310.0, 0.50, 0.30, 1.0),
conjunction: hsla(45.0, 0.70, 0.38, 0.95),
sextile: hsla(195.0, 0.65, 0.36, 0.90),
square: hsla(8.0, 0.80, 0.38, 0.95),
trine: hsla(140.0, 0.60, 0.32, 0.92),
opposition: hsla(280.0, 0.60, 0.40, 0.95),
minor_aspect: hsla(220.0, 0.30, 0.38, 0.75),
dial_ring: hsla(40.0, 0.20, 0.28, 0.95),
house_cusp: hsla(40.0, 0.15, 0.32, 0.80),
angle_highlight: hsla(38.0, 0.90, 0.38, 1.0),
bg_panel: hsla(40.0, 0.25, 0.97, 1.0),
fg_text: hsla(30.0, 0.15, 0.18, 1.0),
fg_muted: hsla(30.0, 0.12, 0.40, 1.0),
}
}
/// Color del planeta por su id simbólico (`"sun"`, `"moon"`, …).
pub fn planet(&self, sym: &str) -> Rgba {
match sym {
"sun" => self.sun,
"moon" => self.moon,
"mercury" => self.mercury,
"venus" => self.venus,
"mars" => self.mars,
"jupiter" => self.jupiter,
"saturn" => self.saturn,
"uranus" => self.uranus,
"neptune" => self.neptune,
"pluto" => self.pluto,
"chiron" => self.chiron,
"north_node" => self.north_node,
"south_node" => self.south_node,
"lilith" => self.lilith,
_ => self.fg_muted,
}
}
/// Color del aspecto por su kind.
pub fn aspect(&self, kind: &str) -> Rgba {
match kind {
"conjunction" => self.conjunction,
"sextile" => self.sextile,
"square" => self.square,
"trine" => self.trine,
"opposition" => self.opposition,
_ => self.minor_aspect,
}
}
/// Color del signo zodiacal por su elemento (fire/earth/air/water).
pub fn sign(&self, sym: &str) -> Rgba {
match sym {
"aries" | "leo" | "sagittarius" => self.fire,
"taurus" | "virgo" | "capricorn" => self.earth,
"gemini" | "libra" | "aquarius" => self.air,
"cancer" | "scorpio" | "pisces" => self.water,
_ => self.fg_muted,
}
}
/// Color del anillo de casas (sistema ascensional Polich-Page).
/// Hue-shift de 140° respecto a `house_cusp` para diferenciar
/// del dial zodiacal — replica el shift que hace el canvas
/// nativo via `house_ring_color`.
pub fn house_ring(&self) -> Rgba {
// Aproximación rápida en HSL: rotamos el hue por 140°
// manteniendo aspecto similar.
let base = self.house_cusp;
// Conversión RGB → HSL → shift → RGB. Para no agregar
// dependencias, lo aproximamos con un mix simple hacia el
// verde/teal de la palette base.
let target = if self.is_dark {
hsla(170.0, 0.30, 0.55, base.a)
} else {
hsla(170.0, 0.40, 0.32, base.a)
};
target
}
}
impl Default for Palette {
fn default() -> Self {
Self::dark()
}
}