caelus-wheel 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/astromap.d.ts +32 -0
- package/dist/src/astromap.js +64 -0
- package/dist/src/ephemerisgraph.d.ts +27 -0
- package/dist/src/ephemerisgraph.js +64 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/sphere.d.ts +39 -0
- package/dist/src/sphere.js +89 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AstroMap: astrocartography angle lines on an equirectangular world map,
|
|
3
|
+
* SSR-safe SVG with no runtime dependencies. MC and IC are vertical meridians;
|
|
4
|
+
* ASC and DSC are the curved rising/setting tracks, split where they cross the
|
|
5
|
+
* date line. The basemap is a graticule only (no bundled geodata, to stay
|
|
6
|
+
* zero-dependency); pass your own coastline paths as children to layer a map
|
|
7
|
+
* under the lines.
|
|
8
|
+
*
|
|
9
|
+
* Feed it the output of the caelus engine's `astrocartography(...)`.
|
|
10
|
+
*/
|
|
11
|
+
import { type WheelTheme } from "./index.js";
|
|
12
|
+
/** One body's four angle lines (matches the engine's AngleLines). */
|
|
13
|
+
export interface MapLines {
|
|
14
|
+
mc: number;
|
|
15
|
+
ic: number;
|
|
16
|
+
asc: [number, number][];
|
|
17
|
+
dsc: [number, number][];
|
|
18
|
+
}
|
|
19
|
+
export type AngleKind = "mc" | "ic" | "asc" | "dsc";
|
|
20
|
+
export interface AstroMapProps {
|
|
21
|
+
lines: Record<string, MapLines>;
|
|
22
|
+
width?: number;
|
|
23
|
+
height?: number;
|
|
24
|
+
/** Which angle lines to draw (default all four). */
|
|
25
|
+
show?: AngleKind[];
|
|
26
|
+
/** Per-body line colour; falls back to a default palette. */
|
|
27
|
+
colors?: Record<string, string>;
|
|
28
|
+
graticule?: boolean;
|
|
29
|
+
theme?: Partial<WheelTheme>;
|
|
30
|
+
children?: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
export declare function AstroMap({ lines, width, height, show, colors, graticule, theme, children, }: AstroMapProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* AstroMap: astrocartography angle lines on an equirectangular world map,
|
|
4
|
+
* SSR-safe SVG with no runtime dependencies. MC and IC are vertical meridians;
|
|
5
|
+
* ASC and DSC are the curved rising/setting tracks, split where they cross the
|
|
6
|
+
* date line. The basemap is a graticule only (no bundled geodata, to stay
|
|
7
|
+
* zero-dependency); pass your own coastline paths as children to layer a map
|
|
8
|
+
* under the lines.
|
|
9
|
+
*
|
|
10
|
+
* Feed it the output of the caelus engine's `astrocartography(...)`.
|
|
11
|
+
*/
|
|
12
|
+
import { DARK_THEME } from "./index.js";
|
|
13
|
+
const DEFAULT_COLORS = {
|
|
14
|
+
sun: "#e0b020", moon: "#c8c8d0", mercury: "#9a93c4", venus: "#4fb09a",
|
|
15
|
+
mars: "#c0564f", jupiter: "#c08a4f", saturn: "#8d8a99",
|
|
16
|
+
uranus: "#4f8fc0", neptune: "#5a6fc0", pluto: "#a05a8a", chiron: "#7a9a5a",
|
|
17
|
+
};
|
|
18
|
+
const PALETTE = ["#8a7fd4", "#c0564f", "#4f8fc0", "#4fb09a", "#c08a4f", "#a05a8a"];
|
|
19
|
+
const ALL = ["mc", "ic", "asc", "dsc"];
|
|
20
|
+
export function AstroMap({ lines, width = 720, height = 360, show = ALL, colors, graticule = true, theme, children, }) {
|
|
21
|
+
const th = { ...DARK_THEME, ...theme };
|
|
22
|
+
const px = (lon) => ((lon + 180) / 360) * width;
|
|
23
|
+
const py = (lat) => ((90 - lat) / 180) * height;
|
|
24
|
+
// ASC/DSC polyline, broken at the date-line wrap.
|
|
25
|
+
const track = (pts) => {
|
|
26
|
+
const segs = [];
|
|
27
|
+
let cur = [];
|
|
28
|
+
for (let i = 0; i < pts.length; i++) {
|
|
29
|
+
const [lon, lat] = pts[i];
|
|
30
|
+
if (i > 0 && Math.abs(lon - pts[i - 1][0]) > 180) {
|
|
31
|
+
if (cur.length > 1)
|
|
32
|
+
segs.push("M" + cur.join(" L"));
|
|
33
|
+
cur = [];
|
|
34
|
+
}
|
|
35
|
+
cur.push(`${px(lon).toFixed(1)} ${py(lat).toFixed(1)}`);
|
|
36
|
+
}
|
|
37
|
+
if (cur.length > 1)
|
|
38
|
+
segs.push("M" + cur.join(" L"));
|
|
39
|
+
return segs;
|
|
40
|
+
};
|
|
41
|
+
const colorOf = (body, i) => colors?.[body] ?? DEFAULT_COLORS[body] ?? PALETTE[i % PALETTE.length];
|
|
42
|
+
// MC labels crowd into an unreadable run where meridians sit close in
|
|
43
|
+
// longitude. Pack them into staggered rows: each label drops to the first row
|
|
44
|
+
// whose previous label clears its x, so every "<body> MC" tag stays legible.
|
|
45
|
+
const mcLabels = show.includes("mc")
|
|
46
|
+
? Object.keys(lines)
|
|
47
|
+
.map((body, i) => ({ body, x: px(lines[body].mc), col: colorOf(body, i) }))
|
|
48
|
+
.sort((a, b) => a.x - b.x)
|
|
49
|
+
: [];
|
|
50
|
+
const rowEnds = [];
|
|
51
|
+
const placedLabels = mcLabels.map((l) => {
|
|
52
|
+
const w = (l.body.length + 3) * 6.4; // chars * ~px, including " MC"
|
|
53
|
+
let row = 0;
|
|
54
|
+
while (row < rowEnds.length && rowEnds[row] > l.x - 4)
|
|
55
|
+
row++;
|
|
56
|
+
rowEnds[row] = l.x + w;
|
|
57
|
+
return { ...l, y: 11 + row * 12, w };
|
|
58
|
+
});
|
|
59
|
+
return (_jsxs("svg", { viewBox: `0 0 ${width} ${height}`, width: width, height: height, xmlns: "http://www.w3.org/2000/svg", role: "img", "aria-label": "Astrocartography map", children: [_jsx("rect", { x: 0, y: 0, width: width, height: height, fill: th.background }), children, graticule && (_jsxs("g", { stroke: th.ring, strokeWidth: 0.5, fill: "none", opacity: 0.5, children: [[-120, -60, 0, 60, 120].map((lon) => (_jsx("line", { x1: px(lon), y1: 0, x2: px(lon), y2: height }, `m${lon}`))), [-60, -30, 0, 30, 60].map((lat) => (_jsx("line", { x1: 0, y1: py(lat), x2: width, y2: py(lat), strokeWidth: lat === 0 ? 1 : 0.5, opacity: lat === 0 ? 0.9 : 0.5 }, `p${lat}`)))] })), Object.keys(lines).map((body, i) => {
|
|
60
|
+
const L = lines[body];
|
|
61
|
+
const col = colorOf(body, i);
|
|
62
|
+
return (_jsxs("g", { stroke: col, fill: col, strokeWidth: 1.5, children: [show.includes("mc") && (_jsx("line", { x1: px(L.mc), y1: 0, x2: px(L.mc), y2: height })), show.includes("ic") && (_jsx("line", { x1: px(L.ic), y1: 0, x2: px(L.ic), y2: height, strokeDasharray: "4 3", opacity: 0.8 })), show.includes("asc") && track(L.asc).map((d, k) => (_jsx("path", { d: d, fill: "none" }, `a${k}`))), show.includes("dsc") && track(L.dsc).map((d, k) => (_jsx("path", { d: d, fill: "none", strokeDasharray: "4 3", opacity: 0.8 }, `d${k}`)))] }, body));
|
|
63
|
+
}), _jsx("g", { fontFamily: th.fontFamily, fontSize: 11, children: placedLabels.map((l) => (_jsxs("g", { children: [_jsx("rect", { x: l.x, y: l.y - 9, width: l.w, height: 11, rx: 2, fill: th.background, opacity: 0.65 }), _jsxs("text", { x: l.x + 2, y: l.y, fill: l.col, stroke: "none", children: [l.body, " MC"] })] }, l.body))) })] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EphemerisGraph: a graphic ephemeris -- one value per body over time as line
|
|
3
|
+
* graphs, SSR-safe SVG with no runtime dependencies. Longitude wraps 360 -> 0,
|
|
4
|
+
* so the lines are split at the wrap; declination and speed draw straight.
|
|
5
|
+
* Feed it the output of the caelus engine's `ephemeris(...)`.
|
|
6
|
+
*/
|
|
7
|
+
import { type WheelTheme } from "./index.js";
|
|
8
|
+
export interface SeriesPoint {
|
|
9
|
+
jd: number;
|
|
10
|
+
value: number;
|
|
11
|
+
}
|
|
12
|
+
export interface EphemerisGraphProps {
|
|
13
|
+
series: Record<string, SeriesPoint[]>;
|
|
14
|
+
width?: number;
|
|
15
|
+
height?: number;
|
|
16
|
+
/** Value-axis bounds. Defaults to [0, wrap] when wrap is set, else the data
|
|
17
|
+
* range with a little padding. */
|
|
18
|
+
yMin?: number;
|
|
19
|
+
yMax?: number;
|
|
20
|
+
/** Split a line when the value jumps more than wrap/2 (use 360 for longitude). */
|
|
21
|
+
wrap?: number;
|
|
22
|
+
/** Value-axis gridline spacing. */
|
|
23
|
+
gridStep?: number;
|
|
24
|
+
colors?: Record<string, string>;
|
|
25
|
+
theme?: Partial<WheelTheme>;
|
|
26
|
+
}
|
|
27
|
+
export declare function EphemerisGraph({ series, width, height, yMin, yMax, wrap, gridStep, colors, theme, }: EphemerisGraphProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* EphemerisGraph: a graphic ephemeris -- one value per body over time as line
|
|
4
|
+
* graphs, SSR-safe SVG with no runtime dependencies. Longitude wraps 360 -> 0,
|
|
5
|
+
* so the lines are split at the wrap; declination and speed draw straight.
|
|
6
|
+
* Feed it the output of the caelus engine's `ephemeris(...)`.
|
|
7
|
+
*/
|
|
8
|
+
import { DARK_THEME } from "./index.js";
|
|
9
|
+
const DEFAULT_COLORS = {
|
|
10
|
+
sun: "#e0b020", moon: "#c8c8d0", mercury: "#9a93c4", venus: "#4fb09a",
|
|
11
|
+
mars: "#c0564f", jupiter: "#c08a4f", saturn: "#8d8a99",
|
|
12
|
+
uranus: "#4f8fc0", neptune: "#5a6fc0", pluto: "#a05a8a", chiron: "#7a9a5a",
|
|
13
|
+
};
|
|
14
|
+
const PALETTE = ["#8a7fd4", "#c0564f", "#4f8fc0", "#4fb09a", "#c08a4f", "#a05a8a"];
|
|
15
|
+
export function EphemerisGraph({ series, width = 720, height = 360, yMin, yMax, wrap, gridStep, colors, theme, }) {
|
|
16
|
+
const th = { ...DARK_THEME, ...theme };
|
|
17
|
+
const bodies = Object.keys(series).filter((b) => series[b]?.length);
|
|
18
|
+
const all = bodies.flatMap((b) => series[b]);
|
|
19
|
+
const jds = all.map((p) => p.jd);
|
|
20
|
+
const j0 = Math.min(...jds), j1 = Math.max(...jds);
|
|
21
|
+
let lo = yMin, hi = yMax;
|
|
22
|
+
if (lo === undefined || hi === undefined) {
|
|
23
|
+
if (wrap !== undefined) {
|
|
24
|
+
lo = 0;
|
|
25
|
+
hi = wrap;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const vs = all.map((p) => p.value);
|
|
29
|
+
const min = Math.min(...vs), max = Math.max(...vs), pad = (max - min) * 0.05 || 1;
|
|
30
|
+
lo = min - pad;
|
|
31
|
+
hi = max + pad;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const span = hi - lo || 1;
|
|
35
|
+
const m = { l: 44, r: 96, t: 12, b: 24 };
|
|
36
|
+
const pw = width - m.l - m.r, ph = height - m.t - m.b;
|
|
37
|
+
const px = (jd) => m.l + ((jd - j0) / (j1 - j0 || 1)) * pw;
|
|
38
|
+
const py = (v) => m.t + (1 - (v - lo) / span) * ph;
|
|
39
|
+
const step = gridStep ?? (wrap ? wrap / 6 : span / 6);
|
|
40
|
+
const gridVals = [];
|
|
41
|
+
for (let v = Math.ceil(lo / step) * step; v <= hi + 1e-9; v += step)
|
|
42
|
+
gridVals.push(v);
|
|
43
|
+
const colorOf = (b, i) => colors?.[b] ?? DEFAULT_COLORS[b] ?? PALETTE[i % PALETTE.length];
|
|
44
|
+
// A body's polyline, split where the value wraps.
|
|
45
|
+
const line = (pts) => {
|
|
46
|
+
const segs = [];
|
|
47
|
+
let cur = [];
|
|
48
|
+
for (let i = 0; i < pts.length; i++) {
|
|
49
|
+
if (wrap !== undefined && i > 0 && Math.abs(pts[i].value - pts[i - 1].value) > wrap / 2) {
|
|
50
|
+
if (cur.length > 1)
|
|
51
|
+
segs.push("M" + cur.join(" L"));
|
|
52
|
+
cur = [];
|
|
53
|
+
}
|
|
54
|
+
cur.push(`${px(pts[i].jd).toFixed(1)} ${py(pts[i].value).toFixed(1)}`);
|
|
55
|
+
}
|
|
56
|
+
if (cur.length > 1)
|
|
57
|
+
segs.push("M" + cur.join(" L"));
|
|
58
|
+
return segs;
|
|
59
|
+
};
|
|
60
|
+
return (_jsxs("svg", { viewBox: `0 0 ${width} ${height}`, width: width, height: height, xmlns: "http://www.w3.org/2000/svg", role: "img", "aria-label": "Graphic ephemeris", children: [_jsx("rect", { x: 0, y: 0, width: width, height: height, fill: th.background }), _jsx("g", { fontFamily: th.fontFamily, fontSize: 10, fill: th.labelText, children: gridVals.map((v) => (_jsxs("g", { children: [_jsx("line", { x1: m.l, y1: py(v), x2: m.l + pw, y2: py(v), stroke: th.ring, strokeWidth: 0.5, opacity: 0.5 }), _jsx("text", { x: m.l - 4, y: py(v), textAnchor: "end", dominantBaseline: "central", children: Math.round(v) })] }, v))) }), _jsx("rect", { x: m.l, y: m.t, width: pw, height: ph, fill: "none", stroke: th.ring, strokeWidth: 1 }), bodies.map((b, i) => {
|
|
61
|
+
const col = colorOf(b, i);
|
|
62
|
+
return (_jsxs("g", { stroke: col, fill: col, strokeWidth: 1.5, children: [line(series[b]).map((d, k) => _jsx("path", { d: d, fill: "none" }, k)), _jsx("line", { x1: m.l + pw + 8, y1: m.t + 12 + i * 16, x2: m.l + pw + 22, y2: m.t + 12 + i * 16 }), _jsx("text", { x: m.l + pw + 26, y: m.t + 12 + i * 16, fontSize: 11, stroke: "none", fontFamily: th.fontFamily, dominantBaseline: "central", children: b })] }, b));
|
|
63
|
+
})] }));
|
|
64
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -66,3 +66,6 @@ export interface ChartWheelProps {
|
|
|
66
66
|
*/
|
|
67
67
|
export declare function spreadAngles(lons: number[], minSep: number): number[];
|
|
68
68
|
export declare function ChartWheel({ chart, size, showAspects, aspectTypes, bodies, theme, glyphs, }: ChartWheelProps): ReactElement;
|
|
69
|
+
export * from "./sphere.js";
|
|
70
|
+
export * from "./astromap.js";
|
|
71
|
+
export * from "./ephemerisgraph.js";
|
package/dist/src/index.js
CHANGED
|
@@ -190,3 +190,6 @@ export function ChartWheel({ chart, size = 520, showAspects = true, aspectTypes
|
|
|
190
190
|
});
|
|
191
191
|
return (_jsx("svg", { width: size, height: size, viewBox: `${-pad} ${-pad} ${size + 2 * pad} ${size + 2 * pad}`, role: "img", "aria-label": "astrological chart wheel", style: { background: T.background, display: "block" }, children: el }));
|
|
192
192
|
}
|
|
193
|
+
export * from "./sphere.js";
|
|
194
|
+
export * from "./astromap.js";
|
|
195
|
+
export * from "./ephemerisgraph.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChartSphere: the chart as a tilted celestial sphere instead of a flat wheel,
|
|
3
|
+
* SSR-safe SVG with no runtime dependencies. Planets sit at their true ecliptic
|
|
4
|
+
* latitude (a flat wheel collapses that to zero), so you see the Moon ride above
|
|
5
|
+
* the ecliptic and Pluto swing well off it. The ecliptic and equator are drawn
|
|
6
|
+
* as great-circle rings, solid on the near hemisphere and faded on the far one;
|
|
7
|
+
* a short stem drops each planet to the ecliptic plane to show its latitude.
|
|
8
|
+
*
|
|
9
|
+
* Inspired by Astrolog's chart spheres. The 3D aspect angle that pairs with this
|
|
10
|
+
* view is `angularSeparation3d` in the caelus engine.
|
|
11
|
+
*/
|
|
12
|
+
import { type WheelTheme } from "./index.js";
|
|
13
|
+
export interface SpherePosition {
|
|
14
|
+
lon: number;
|
|
15
|
+
lat: number;
|
|
16
|
+
retrograde?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface SphereChart {
|
|
19
|
+
/** Bodies with ecliptic longitude AND latitude (the caelus chart's bodies
|
|
20
|
+
* satisfy this as-is). */
|
|
21
|
+
bodies: Record<string, SpherePosition>;
|
|
22
|
+
}
|
|
23
|
+
export interface ChartSphereProps {
|
|
24
|
+
chart: SphereChart;
|
|
25
|
+
/** Square size in px. */
|
|
26
|
+
size?: number;
|
|
27
|
+
/** Degrees the ecliptic pole tilts away from the viewer (0 = pole-on wheel,
|
|
28
|
+
* 90 = edge-on). */
|
|
29
|
+
tilt?: number;
|
|
30
|
+
/** Degrees the sphere is spun about its pole (which longitude faces front). */
|
|
31
|
+
turn?: number;
|
|
32
|
+
/** Obliquity for the equator ring, degrees. */
|
|
33
|
+
obliquity?: number;
|
|
34
|
+
showEquator?: boolean;
|
|
35
|
+
bodies?: string[];
|
|
36
|
+
theme?: Partial<WheelTheme>;
|
|
37
|
+
glyphs?: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
export declare function ChartSphere({ chart, size, tilt, turn, obliquity, showEquator, bodies, theme, glyphs, }: ChartSphereProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ChartSphere: the chart as a tilted celestial sphere instead of a flat wheel,
|
|
4
|
+
* SSR-safe SVG with no runtime dependencies. Planets sit at their true ecliptic
|
|
5
|
+
* latitude (a flat wheel collapses that to zero), so you see the Moon ride above
|
|
6
|
+
* the ecliptic and Pluto swing well off it. The ecliptic and equator are drawn
|
|
7
|
+
* as great-circle rings, solid on the near hemisphere and faded on the far one;
|
|
8
|
+
* a short stem drops each planet to the ecliptic plane to show its latitude.
|
|
9
|
+
*
|
|
10
|
+
* Inspired by Astrolog's chart spheres. The 3D aspect angle that pairs with this
|
|
11
|
+
* view is `angularSeparation3d` in the caelus engine.
|
|
12
|
+
*/
|
|
13
|
+
import { GLYPHS, DARK_THEME } from "./index.js";
|
|
14
|
+
const D2R = Math.PI / 180;
|
|
15
|
+
const DEFAULT_OBLIQUITY = 23.4368; // mean obliquity, deg; for the equator ring
|
|
16
|
+
const SIGN_GLYPHS = ["♈", "♉", "♊", "♋", "♌", "♍",
|
|
17
|
+
"♎", "♏", "♐", "♑", "♒", "♓"];
|
|
18
|
+
const eclVec = (lonDeg, latDeg) => {
|
|
19
|
+
const l = lonDeg * D2R, b = latDeg * D2R, cb = Math.cos(b);
|
|
20
|
+
return [cb * Math.cos(l), cb * Math.sin(l), Math.sin(b)];
|
|
21
|
+
};
|
|
22
|
+
const rotX = (v, deg) => {
|
|
23
|
+
const c = Math.cos(deg * D2R), s = Math.sin(deg * D2R);
|
|
24
|
+
return [v[0], v[1] * c - v[2] * s, v[1] * s + v[2] * c];
|
|
25
|
+
};
|
|
26
|
+
const rotZ = (v, deg) => {
|
|
27
|
+
const c = Math.cos(deg * D2R), s = Math.sin(deg * D2R);
|
|
28
|
+
return [v[0] * c - v[1] * s, v[0] * s + v[1] * c, v[2]];
|
|
29
|
+
};
|
|
30
|
+
function project(world, ctx) {
|
|
31
|
+
const v = rotX(rotZ(world, ctx.turn), ctx.tilt);
|
|
32
|
+
return { x: ctx.c + v[0] * ctx.R, y: ctx.c - v[1] * ctx.R, front: v[2] >= 0, depth: v[2] };
|
|
33
|
+
}
|
|
34
|
+
/** A great circle sampled and split into near (front) and far (back) polyline
|
|
35
|
+
* paths, so the near arc can be drawn solid over the far one. */
|
|
36
|
+
function ringPaths(points, ctx) {
|
|
37
|
+
const segs = { front: [], back: [] };
|
|
38
|
+
let run = [];
|
|
39
|
+
let runFront = true;
|
|
40
|
+
const flush = () => {
|
|
41
|
+
if (run.length < 2) {
|
|
42
|
+
run = [];
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const d = "M" + run.map((p) => `${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(" L");
|
|
46
|
+
segs[runFront ? "front" : "back"].push(d);
|
|
47
|
+
run = [];
|
|
48
|
+
};
|
|
49
|
+
for (let i = 0; i <= points.length; i++) {
|
|
50
|
+
const p = project(points[i % points.length], ctx);
|
|
51
|
+
if (run.length && p.front !== runFront) {
|
|
52
|
+
flush();
|
|
53
|
+
}
|
|
54
|
+
if (!run.length)
|
|
55
|
+
runFront = p.front;
|
|
56
|
+
run.push(p);
|
|
57
|
+
}
|
|
58
|
+
flush();
|
|
59
|
+
return { front: segs.front.join(" "), back: segs.back.join(" ") };
|
|
60
|
+
}
|
|
61
|
+
function circleSamples(make, n = 120) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (let i = 0; i < n; i++)
|
|
64
|
+
out.push(make((i / n) * 360));
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
export function ChartSphere({ chart, size = 520, tilt = 64, turn = -90, obliquity = DEFAULT_OBLIQUITY, showEquator = true, bodies, theme, glyphs, }) {
|
|
68
|
+
const th = { ...DARK_THEME, ...theme };
|
|
69
|
+
const gl = { ...GLYPHS, ...glyphs };
|
|
70
|
+
const c = size / 2;
|
|
71
|
+
const R = size * 0.42;
|
|
72
|
+
const ctx = { turn, tilt, R, c };
|
|
73
|
+
const ecliptic = ringPaths(circleSamples((d) => eclVec(d, 0)), ctx);
|
|
74
|
+
const equator = ringPaths(circleSamples((d) => rotX(eclVec(d, 0), obliquity)), ctx);
|
|
75
|
+
const names = (bodies ?? Object.keys(chart.bodies)).filter((b) => chart.bodies[b]);
|
|
76
|
+
const drawn = names.map((name) => {
|
|
77
|
+
const { lon, lat, retrograde } = chart.bodies[name];
|
|
78
|
+
const p = project(eclVec(lon, lat), ctx);
|
|
79
|
+
const base = project(eclVec(lon, 0), ctx); // foot on the ecliptic plane
|
|
80
|
+
return { name, p, base, retrograde };
|
|
81
|
+
}).sort((a, b) => a.p.depth - b.p.depth); // far first (painter's order)
|
|
82
|
+
return (_jsxs("svg", { viewBox: `0 0 ${size} ${size}`, width: size, height: size, xmlns: "http://www.w3.org/2000/svg", role: "img", "aria-label": "Astrological chart sphere", children: [_jsx("circle", { cx: c, cy: c, r: R, fill: th.background, stroke: th.ring, strokeWidth: 1 }), showEquator && (_jsxs("g", { fill: "none", stroke: th.axis, strokeWidth: 1, children: [_jsx("path", { d: equator.back, opacity: 0.25, strokeDasharray: "3 3" }), _jsx("path", { d: equator.front, opacity: 0.7 })] })), _jsxs("g", { fill: "none", stroke: th.signText, strokeWidth: 1.25, children: [_jsx("path", { d: ecliptic.back, opacity: 0.3, strokeDasharray: "3 3" }), _jsx("path", { d: ecliptic.front, opacity: 0.9 })] }), _jsx("g", { fontFamily: th.fontFamily, fontSize: size * 0.03, fill: th.signText, textAnchor: "middle", dominantBaseline: "central", children: SIGN_GLYPHS.map((g, i) => {
|
|
83
|
+
const m = project(eclVec(i * 30, 0), ctx);
|
|
84
|
+
return (_jsx("text", { x: m.x, y: m.y, opacity: m.front ? 0.85 : 0.3, children: g }, i));
|
|
85
|
+
}) }), _jsx("g", { fontFamily: th.fontFamily, children: drawn.map(({ name, p, base, retrograde }) => {
|
|
86
|
+
const op = p.front ? 1 : 0.4;
|
|
87
|
+
return (_jsxs("g", { opacity: op, children: [_jsx("line", { x1: base.x, y1: base.y, x2: p.x, y2: p.y, stroke: th.labelText, strokeWidth: 1, opacity: 0.6 }), _jsx("circle", { cx: base.x, cy: base.y, r: 1.5, fill: th.labelText }), _jsx("text", { x: p.x, y: p.y, fontSize: size * 0.045, fill: th.planetText, textAnchor: "middle", dominantBaseline: "central", children: gl[name] ?? name.slice(0, 2) }), retrograde && (_jsx("text", { x: p.x + size * 0.03, y: p.y - size * 0.03, fontSize: size * 0.022, fill: th.labelText, textAnchor: "middle", children: "R" }))] }, name));
|
|
88
|
+
}) })] }));
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caelus-wheel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "React SVG chart wheel for caelus: zodiac, houses, planets with collision avoidance, aspect lines. SSR-safe, zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|