一点文化注脚
月亮大概是人类最早用来计量时间的天体,也是历史最久的审美对象之一。潮汐跟着它走,农历绕着它转,中秋和元宵这类节日也建立在它的圆缺之上。中国古诗里「千里共婵娟」把一轮圆月直接等同于团聚与思念,这种把情感寄托给月亮的习惯延续到现在,不少人会特意把某一天、某个人的月亮存成手机壁纸。
星座的来路类似。古人把黄道附近明亮的恒星连成十二组,对应一年里的时段,用来标记季节和历法,后来这套体系被占星学接手,变成了出生日期对应一个星座的说法。无论是否相信占星,黄道十二宫都是一套很古老的、把时间空间化的办法。
月相是怎么算出来的
月相本质上是一个几何问题:太阳、地球、月亮三者的相对位置,决定了我们从地球上能看见月亮被照亮的多大一部分。月亮本身不发光,我们看到的亮面是它受太阳光照射、同时又正对地球的那一半。
衡量月相最常用的量是「朔望月」,也就是相邻两次新月之间的平均间隔,长度约为 29.530588853 天。只要知道某一次新月的精确时刻作为基准,之后再过了多少天,就能推出当前处于一个朔望周期的哪个位置:
// 朔望月平均长度(天)
const SYNODIC = 29.530588853;
// 一个已知的新月历元:2000-01-06 18:14 UTC(J2000 附近)
const REF_NEW_MOON = Date.UTC(2000, 0, 6, 18, 14) / 86400000; // 单位:天
function moonPhase(date) {
const jd = date.getTime() / 86400000 + 2440587.5; // 儒略日
let phase = ((jd - REF_NEW_MOON) % SYNODIC) / SYNODIC;
if (phase < 0) phase += 1; // 0 = 朔, 0.5 = 望
const illum = (1 - Math.cos(2 * Math.PI * phase)) / 2; // 照明比例 0~1
return { phase, illum };
}
phase 取值 0 到 1,0 表示新月(朔),0.5 表示满月(望),illum 是月亮被照亮的比例。这个算法只用了平均朔望月,足够做一张壁纸,但和真实天象会有偏差,因为月球轨道是椭圆的,还受太阳引力摄动,周期并不严格恒定。
要算得更准,就得用月球的轨道根数加上周期项修正(Meeus《天文算法》里给了一组精度足够的近似公式),或者直接用 ELP 月球历这类数值星历。方向是一致的:先求太阳和月亮的地心黄经,二者之差就是「距角」,距角决定照明比例和月相名称;再结合观测地的经纬度与时间,就能进一步推出月出、月落的时刻。
星座与星空图的计算
「星座」这个词在代码里其实指两件事,最好别混在一起。
第一件是占星学里的黄道十二宫,纯粹由出生日期决定。回归黄道以春分点(约 3 月 21 日)为白羊座 0°,往后每 30° 一个星座,所以只要知道月日,做一段区间判断就行:
// 黄道十二宫(回归黄道),以春分点(约 3/21)为白羊座起点
const ZODIAC = [
{ name: '白羊座', start: [3, 21], end: [4, 19] },
{ name: '金牛座', start: [4, 20], end: [5, 20] },
{ name: '双子座', start: [5, 21], end: [6, 21] },
{ name: '巨蟹座', start: [6, 22], end: [7, 22] },
{ name: '狮子座', start: [7, 23], end: [8, 22] },
{ name: '处女座', start: [8, 23], end: [9, 22] },
{ name: '天秤座', start: [9, 23], end: [10, 23] },
{ name: '天蝎座', start: [10, 24], end: [11, 22] },
{ name: '射手座', start: [11, 23], end: [12, 21] },
{ name: '摩羯座', start: [12, 22], end: [1, 19] },
{ name: '水瓶座', start: [1, 20], end: [2, 18] },
{ name: '双鱼座', start: [2, 19], end: [3, 20] },
];
// 摩羯座跨年,需单独处理 12/22 – 1/19 这一段区间
它和真实星空已经错位了:由于岁差,今天的春分点其实落在双鱼座,而不在白羊座。但对「我是什么星座」这种日常问题,日期查表已经足够。
第二件是真正画一张星空图。这需要把天体从赤道坐标系(赤经 RA、赤纬 Dec)变换到观测者当地的地平坐标系(高度角、方位角)。恒星的 RA/Dec 在人的一生里基本不变,直接取自星表(比如 Hipparcos);太阳、月亮、行星则要先用星历算出它们当下的 RA/Dec。
变换的关键是本地恒星时 LST:
// lat: 观测者纬度, lst: 本地恒星时, 单位均为弧度
function equatorialToHorizontal(ra, dec, lat, lst) {
const H = lst - ra; // 时角
const alt = Math.asin(
Math.sin(dec) * Math.sin(lat) +
Math.cos(dec) * Math.cos(lat) * Math.cos(H)
);
const az = Math.atan2(
-Math.cos(dec) * Math.sin(H),
Math.sin(dec) * Math.cos(lat) - Math.cos(dec) * Math.sin(lat) * Math.cos(H)
);
return { alt, az };
}
LST 由儒略日推出格林尼治平恒星时 GMST,再加上观测地经度得到。给定某个夜晚、某座城市,把所有亮星按这套公式摆到屏幕上,就是那天那地抬头能看到的真实星空。如果要进一步画本命盘,还要用黄赤交角(约 23.44°)和恒星时算出上升点 ASC 与中天 MC,再划分宫位,那是另一套更细的活。
把计算变成一张壁纸
前面这些公式,落到一个普通用户手里,最直观的产物就是一张壁纸。
如果你只是想留住某个人的月亮,情侣壁纸 把两个人的生日月亮并排放在一起,再配一句想说的话,导出的图直接就能当锁屏。它底层跑的就是上一节的月相算法,只不过把「今天」换成了两个具体的生日。
如果你想留住某一夜的星空,星座壁纸(星图) 会按你给的日期、时间和地点,用恒星时与坐标变换还原那片天空,把真实亮星的位置画出来。无论是出生那晚,还是某个纪念日的夜空,都能变成一张可以每天看着的图。
计算本身并不复杂,难的是把天文历法和普通人记住的某个时刻连起来。这两张壁纸做的,大概就是这件事。