diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/aquarius.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/aquarius.svg
new file mode 100644
index 0000000..2d3dd55
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/aquarius.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/aries.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/aries.svg
new file mode 100644
index 0000000..2843d95
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/aries.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/cancer.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/cancer.svg
new file mode 100644
index 0000000..a1a20a7
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/cancer.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/capricorn.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/capricorn.svg
new file mode 100644
index 0000000..6cabad7
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/capricorn.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/gemini.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/gemini.svg
new file mode 100644
index 0000000..eef33e8
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/gemini.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/leo.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/leo.svg
new file mode 100644
index 0000000..72c2ca9
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/leo.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/libra.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/libra.svg
new file mode 100644
index 0000000..7e020da
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/libra.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/pisces.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/pisces.svg
new file mode 100644
index 0000000..2e5fc4e
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/pisces.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/sagittarius.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/sagittarius.svg
new file mode 100644
index 0000000..5274e7b
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/sagittarius.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/scorpio.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/scorpio.svg
new file mode 100644
index 0000000..e4c9f3e
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/scorpio.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/taurus.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/taurus.svg
new file mode 100644
index 0000000..9c491ef
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/taurus.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/virgo.svg b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/virgo.svg
new file mode 100644
index 0000000..7bc16d3
--- /dev/null
+++ b/src/apps/gameboard/static/apps/gameboard/icons/zodiac-signs/virgo.svg
@@ -0,0 +1 @@
+
diff --git a/src/apps/gameboard/static/apps/gameboard/natus-wheel.js b/src/apps/gameboard/static/apps/gameboard/natus-wheel.js
index 8c923a2..a291b64 100644
--- a/src/apps/gameboard/static/apps/gameboard/natus-wheel.js
+++ b/src/apps/gameboard/static/apps/gameboard/natus-wheel.js
@@ -27,18 +27,18 @@ const NatusWheel = (() => {
// ── Constants ──────────────────────────────────────────────────────────────
const SIGNS = [
- { name: 'Aries', symbol: '♈', element: 'Fire' },
- { name: 'Taurus', symbol: '♉', element: 'Stone' },
- { name: 'Gemini', symbol: '♊', element: 'Air' },
- { name: 'Cancer', symbol: '♋', element: 'Water' },
- { name: 'Leo', symbol: '♌', element: 'Fire' },
- { name: 'Virgo', symbol: '♍', element: 'Stone' },
- { name: 'Libra', symbol: '♎', element: 'Air' },
- { name: 'Scorpio', symbol: '♏', element: 'Water' },
- { name: 'Sagittarius', symbol: '♐', element: 'Fire' },
- { name: 'Capricorn', symbol: '♑', element: 'Stone' },
- { name: 'Aquarius', symbol: '♒', element: 'Air' },
- { name: 'Pisces', symbol: '♓', element: 'Water' },
+ { name: 'Aries', symbol: '♈', element: 'Fire', fa: 'aries' },
+ { name: 'Taurus', symbol: '♉', element: 'Stone', fa: 'taurus' },
+ { name: 'Gemini', symbol: '♊', element: 'Air', fa: 'gemini' },
+ { name: 'Cancer', symbol: '♋', element: 'Water', fa: 'cancer' },
+ { name: 'Leo', symbol: '♌', element: 'Fire', fa: 'leo' },
+ { name: 'Virgo', symbol: '♍', element: 'Stone', fa: 'virgo' },
+ { name: 'Libra', symbol: '♎', element: 'Air', fa: 'libra' },
+ { name: 'Scorpio', symbol: '♏', element: 'Water', fa: 'scorpio' },
+ { name: 'Sagittarius', symbol: '♐', element: 'Fire', fa: 'sagittarius' },
+ { name: 'Capricorn', symbol: '♑', element: 'Stone', fa: 'capricorn' },
+ { name: 'Aquarius', symbol: '♒', element: 'Air', fa: 'aquarius' },
+ { name: 'Pisces', symbol: '♓', element: 'Water', fa: 'pisces' },
];
const PLANET_SYMBOLS = {
@@ -46,6 +46,12 @@ const NatusWheel = (() => {
Jupiter: '♃', Saturn: '♄', Uranus: '♅', Neptune: '♆', Pluto: '♇',
};
+ // Alchemical element symbol → CSS modifier class suffix (matches rootvars palette)
+ const PLANET_ELEMENTS = {
+ Sun: 'au', Moon: 'ag', Mercury: 'hg', Venus: 'cu', Mars: 'fe',
+ Jupiter: 'sn', Saturn: 'pb', Uranus: 'u', Neptune: 'np', Pluto: 'pu',
+ };
+
// Aspect stroke colors remain in JS — they are data-driven, not stylistic.
const ASPECT_COLORS = {
Conjunction: 'var(--priYl, #f0e060)',
@@ -66,6 +72,9 @@ const NatusWheel = (() => {
let _svg = null;
let _cx, _cy, _r; // centre + outer radius
+ // Cached SVG path strings keyed by sign name, populated by preload().
+ const _signPaths = {};
+
// Ring radii (fractions of _r, set in _layout)
let R = {};
@@ -170,16 +179,29 @@ const NatusWheel = (() => {
}))
.attr('class', `nw-sign--${sign.element.toLowerCase()}`);
- // Symbol at midpoint
+ // Icon at midpoint
const midA = (sa + ea) / 2;
- sigGroup.append('text')
- .attr('x', _cx + R.labelR * Math.cos(midA))
- .attr('y', _cy + R.labelR * Math.sin(midA))
- .attr('text-anchor', 'middle')
- .attr('dominant-baseline', 'middle')
- .attr('font-size', `${_r * 0.095}px`)
- .attr('class', 'nw-sign-label')
- .text(sign.symbol);
+ const lx = _cx + R.labelR * Math.cos(midA);
+ const ly = _cy + R.labelR * Math.sin(midA);
+ const cr = _r * 0.065; // slightly larger than planet circles so icons breathe
+ // scale the 640×640 icon viewBox down to 85% of the circle diameter
+ const sf = (cr * 2 * 0.85) / 640;
+
+ // Colored circle behind icon
+ sigGroup.append('circle')
+ .attr('cx', lx)
+ .attr('cy', ly)
+ .attr('r', cr)
+ .attr('class', `nw-sign-icon-bg--${sign.element.toLowerCase()}`);
+
+ // Inline SVG path — translate origin to label centre, scale, re-centre icon
+ if (_signPaths[sign.name]) {
+ sigGroup.append('path')
+ .attr('d', _signPaths[sign.name])
+ .attr('transform',
+ `translate(${lx},${ly}) scale(${sf}) translate(-320,-320)`)
+ .attr('class', `nw-sign-icon nw-sign-icon--${sign.element.toLowerCase()}`);
+ }
});
}
@@ -240,13 +262,15 @@ const NatusWheel = (() => {
Object.entries(data.planets).forEach(([name, pdata], idx) => {
const finalA = _toAngle(pdata.degree, asc);
+ const el = PLANET_ELEMENTS[name] || '';
// Circle behind symbol
+ const circleBase = pdata.retrograde ? 'nw-planet-circle--rx' : 'nw-planet-circle';
const circle = planetGroup.append('circle')
.attr('cx', _cx + R.planetR * Math.cos(ascAngle))
.attr('cy', _cy + R.planetR * Math.sin(ascAngle))
.attr('r', _r * 0.05)
- .attr('class', pdata.retrograde ? 'nw-planet-circle--rx' : 'nw-planet-circle');
+ .attr('class', el ? `${circleBase} nw-planet--${el}` : circleBase);
// Symbol
const label = planetGroup.append('text')
@@ -256,7 +280,7 @@ const NatusWheel = (() => {
.attr('dominant-baseline', 'middle')
.attr('dy', '0.1em')
.attr('font-size', `${_r * 0.09}px`)
- .attr('class', 'nw-planet-label')
+ .attr('class', el ? `nw-planet-label nw-planet-label--${el}` : 'nw-planet-label')
.text(PLANET_SYMBOLS[name] || name[0]);
// Retrograde indicator
@@ -358,6 +382,37 @@ const NatusWheel = (() => {
// ── Public API ────────────────────────────────────────────────────────────
+ /**
+ * Preload zodiac sign SVGs from `basePath` (default: same dir as this script).
+ * Returns a Promise that resolves when all 12 are cached in _signPaths.
+ * Safe to call multiple times; re-fetches every call so swapped files are picked up.
+ *
+ * To swap a sign icon: replace the corresponding .svg file in zodiac-signs/
+ * and call NatusWheel.preload() before the next draw().
+ */
+ async function preload(basePath) {
+ const base = basePath ||
+ (() => {
+ const scripts = document.querySelectorAll('script[src]');
+ for (const s of scripts) {
+ if (s.src.includes('natus-wheel')) {
+ return s.src.replace(/natus-wheel\.js.*$/, 'icons/zodiac-signs/');
+ }
+ }
+ return '/static/apps/gameboard/icons/zodiac-signs/';
+ })();
+
+ await Promise.all(SIGNS.map(async sign => {
+ const url = base + sign.name.toLowerCase() + '.svg';
+ const resp = await window.fetch(url);
+ if (!resp.ok) { console.warn(`NatusWheel: failed to load ${url}`); return; }
+ const text = await resp.text();
+ const doc = new DOMParser().parseFromString(text, 'image/svg+xml');
+ const path = doc.querySelector('path');
+ if (path) _signPaths[sign.name] = path.getAttribute('d');
+ }));
+ }
+
function draw(svgEl, data) {
_svg = d3.select(svgEl);
_svg.selectAll('*').remove();
@@ -393,5 +448,5 @@ const NatusWheel = (() => {
if (_svg) _svg.selectAll('*').remove();
}
- return { draw, redraw, clear };
+ return { preload, draw, redraw, clear };
})();
diff --git a/src/static_src/scss/_natus.scss b/src/static_src/scss/_natus.scss
index 6317037..278b7f0 100644
--- a/src/static_src/scss/_natus.scss
+++ b/src/static_src/scss/_natus.scss
@@ -359,35 +359,76 @@ html.natus-open .natus-modal-wrap {
.nw-outer-ring {
fill: none;
- stroke: rgba(var(--quaUser), 0.6);
- stroke-width: 1px;
+ stroke: rgba(var(--terUser), 1);
+ stroke-width: 1.5px;
}
.nw-inner-disc {
fill: rgba(var(--quaUser), 0.6);
+ stroke: rgba(var(--terUser), 1);
+ stroke-width: 0.75px;
}
// Axes (ASC / DSC / MC / IC)
.nw-axis-line { stroke: rgba(var(--secUser), 1); stroke-width: 1.5px; }
.nw-axis-label { fill: rgba(var(--secUser), 1); }
-// Sign ring — vars are RGB tuples ("R, G, B"), must be wrapped in rgba()
-.nw-sign--fire { fill: rgba(var(--priRd, 192, 64, 64), 0.75); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; }
-.nw-sign--stone { fill: rgba(var(--priFs, 122, 96, 64), 0.75); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; }
-.nw-sign--air { fill: rgba(var(--priCy, 64, 144, 176), 0.75); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; }
-.nw-sign--water { fill: rgba(var(--priId, 80, 80, 160), 0.75); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; }
-.nw-sign-label { fill: rgba(var(--secUser), 1); }
+// Sign ring — uniform --priMe bg at half opacity
+.nw-sign--fire,
+.nw-sign--stone,
+.nw-sign--air,
+.nw-sign--water {
+ fill: rgba(var(--priMe), 0.25);
+ stroke: rgba(var(--terUser), 1);
+ stroke-width: 0.75px;
+}
-// House ring
-.nw-house-cusp { stroke: rgba(var(--quaUser), 0.8); stroke-width: 0.8px; }
-.nw-house-num { fill: rgba(var(--quiUser), 1); }
-.nw-house-fill--even { fill: rgba(var(--quaUser), 0.45); }
-.nw-house-fill--odd { fill: rgba(var(--quiUser), 0.35); }
+// Icon bg circles — element fill + matching border
+.nw-sign-icon-bg--fire { fill: rgba(var(--terRd), 1); stroke: rgba(var(--priOr), 1); stroke-width: 1px; }
+.nw-sign-icon-bg--stone { fill: rgba(var(--priYl), 1); stroke: rgba(var(--priLm), 1); stroke-width: 1px; }
+.nw-sign-icon-bg--air { fill: rgba(var(--terGn), 1); stroke: rgba(var(--priTk), 1); stroke-width: 1px; }
+.nw-sign-icon-bg--water { fill: rgba(var(--priCy), 1); stroke: rgba(var(--priBl), 1); stroke-width: 1px; }
-// Planets
-.nw-planet-circle { fill: rgba(var(--quiUser), 1); }
-.nw-planet-circle--rx { fill: rgba(var(--quiUser), 1); }
-.nw-planet-label { fill: rgba(var(--quaUser), 1); stroke: rgba(var(--quaUser), 0.6); stroke-width: 0.4px; paint-order: stroke fill; }
+// Inline SVG path icons — per-element colors
+.nw-sign-icon--fire { fill: rgba(var(--priOr), 1); }
+.nw-sign-icon--stone { fill: rgba(var(--terLm), 1); }
+.nw-sign-icon--air { fill: rgba(var(--priTk), 1); }
+.nw-sign-icon--water { fill: rgba(var(--terBl), 1); }
+
+// House ring — uniform --priFs bg
+.nw-house-cusp { stroke: rgba(var(--terUser), 1); stroke-width: 1.2px; }
+.nw-house-num { fill: rgba(var(--ninUser), 1); }
+.nw-house-fill--even { fill: rgba(var(--priFs), 0.25); stroke: rgba(var(--terUser), 1); stroke-width: 0.75px; }
+.nw-house-fill--odd { fill: rgba(var(--priFs), 0.25); stroke: rgba(var(--terUser), 1); stroke-width: 0.75px; }
+
+// Planets — base geometry
+.nw-planet-circle,
+.nw-planet-circle--rx { stroke-width: 1px; }
+.nw-planet-label { stroke-width: 0.4px; paint-order: stroke fill; }
+
+// Per-planet circle: fill = ternary, border = senary
+.nw-planet--au { fill: rgba(var(--terAu), 1); stroke: rgba(var(--sixAu), 1); } // Sun
+.nw-planet--ag { fill: rgba(var(--terAg), 1); stroke: rgba(var(--sixAg), 1); } // Moon
+.nw-planet--hg { fill: rgba(var(--terHg), 1); stroke: rgba(var(--sixHg), 1); } // Mercury
+.nw-planet--cu { fill: rgba(var(--terCu), 1); stroke: rgba(var(--sixCu), 1); } // Venus
+.nw-planet--fe { fill: rgba(var(--terFe), 1); stroke: rgba(var(--sixFe), 1); } // Mars
+.nw-planet--sn { fill: rgba(var(--terSn), 1); stroke: rgba(var(--sixSn), 1); } // Jupiter
+.nw-planet--pb { fill: rgba(var(--terPb), 1); stroke: rgba(var(--sixPb), 1); } // Saturn
+.nw-planet--u { fill: rgba(var(--terU), 1); stroke: rgba(var(--sixU), 1); } // Uranus
+.nw-planet--np { fill: rgba(var(--terNp), 1); stroke: rgba(var(--sixNp), 1); } // Neptune
+.nw-planet--pu { fill: rgba(var(--terPu), 1); stroke: rgba(var(--sixPu), 1); } // Pluto
+
+// Per-planet label: fill + stroke halo = senary
+.nw-planet-label--au { fill: rgba(var(--sixAu), 1); stroke: rgba(var(--sixAu), 0.6); }
+.nw-planet-label--ag { fill: rgba(var(--sixAg), 1); stroke: rgba(var(--sixAg), 0.6); }
+.nw-planet-label--hg { fill: rgba(var(--sixHg), 1); stroke: rgba(var(--sixHg), 0.6); }
+.nw-planet-label--cu { fill: rgba(var(--sixCu), 1); stroke: rgba(var(--sixCu), 0.6); }
+.nw-planet-label--fe { fill: rgba(var(--sixFe), 1); stroke: rgba(var(--sixFe), 0.6); }
+.nw-planet-label--sn { fill: rgba(var(--sixSn), 1); stroke: rgba(var(--sixSn), 0.6); }
+.nw-planet-label--pb { fill: rgba(var(--sixPb), 1); stroke: rgba(var(--sixPb), 0.6); }
+.nw-planet-label--u { fill: rgba(var(--sixU), 1); stroke: rgba(var(--sixU), 0.6); }
+.nw-planet-label--np { fill: rgba(var(--sixNp), 1); stroke: rgba(var(--sixNp), 0.6); }
+.nw-planet-label--pu { fill: rgba(var(--sixPu), 1); stroke: rgba(var(--sixPu), 0.6); }
.nw-rx { fill: rgba(var(--terUser), 1); }
// Aspects
diff --git a/src/templates/apps/gameboard/_partials/_natus_overlay.html b/src/templates/apps/gameboard/_partials/_natus_overlay.html
index 64b0449..a8b2b1f 100644
--- a/src/templates/apps/gameboard/_partials/_natus_overlay.html
+++ b/src/templates/apps/gameboard/_partials/_natus_overlay.html
@@ -130,6 +130,10 @@
const PLACE_DELAY = 400; // ms — Nominatim polite rate
const CHART_DELAY = 300; // ms — chart preview debounce
+ // Preload zodiac SVG icons eagerly — they'll be cached before any draw() call.
+ // To swap an icon, replace the .svg file in zodiac-signs/ and hard-refresh.
+ NatusWheel.preload();
+
// ── localStorage persistence ──────────────────────────────────────────────
// Key scoped to room so multiple rooms don't clobber each other.