Lovund 大概在北纬 66 度,离北极圈很近了,近到夏夜天几乎不会黑,冬天的下午大半时间又被黑夜吞掉。我想让网站的暗色模式呼应这一点,做一片真实的天空,而不是随便撒一把哪儿都能用的白点。等我真去研究了这个纬度的天空到底长什么样之后,"随便撒一把白点"这种做法在我看来就不够格上线了。

先是点,后来才是天空
夜空背景的第一个版本正是我刚才说不想要的那种东西,一堆像素星星精灵,大致均匀地撒在页面边缘,只有暗色主题才有:
// first version: just scatter dots, no real positions
const stars = Array.from({ length: 40 }, () => ({
x: Math.random() * width,
y: Math.random() * height
}));还行吧,有点氛围感,但过目就忘。让我改主意的是一个发现:在这个纬度,尤其是冬天,大多数晴朗的夜晚其实能清楚看到北斗七星和仙后座,它们一直绕着北极星转,因为在北纬 66 度它们根本不会落到地平线以下,是拱极星。这是关于这个网站写作地点的一个真实、具体、可以验证的事实,而一堆随手撒的星点白白浪费了这一点。所以随机散布只留给填充用的星星,那两个有名字的星座则换成了真实坐标。
把真实的星星放在正确的位置
我找来了这两个星座里亮星的实际公开赤经赤纬数值,跟任何一个天文软件用的坐标一样,然后按拱极星图通常的做法来投影:每颗星绕极点的角度由它的赤经(角度)决定,离极点的距离则由 90 减去赤纬得到。
Approximate RA/Dec used (hours/minutes, degrees/minutes):
Polaris 2h31m +89 15 Dubhe 11h03m +61 45 Merak 11h01m +56 23
Phecda 11h53m +53 41 Megrez 12h15m +57 02 Alioth 12h54m +55 57
Mizar 13h23m +54 55 Alkaid 13h47m +49 19北斗七星的七颗和仙后座的五颗星,按这个投影算出来之后,几乎正好分居北极星两侧,跟现实吻合,它们的平均赤经相差大约 170 度。这也意味着业余天文里那个经典技巧,顺着 Merak 到 Dubhe 的连线延伸就能找到北极星,自然而然就成立了。我一颗星的位置都没动过手脚去凑这条指向线,只是老老实实按真实坐标投影出来就是这样。
const DIPPER: Star[] = [
{ x: 1479.6, y: 391.3, r: 1.8, fill: MUTED, opacity: 0.6, twinkle: '-2.1s' }, // Dubhe (pointer star)
{ x: 1475.3, y: 380.3, r: 1.7, fill: MUTED, opacity: 0.6 }, // Merak (pointer star)
{ x: 1490.8, y: 370.6, r: 1.6, fill: SEA, opacity: 0.55 }, // Phecda
{ x: 1498.6, y: 377.5, r: 1.5, fill: SEA, opacity: 0.5 }, // Megrez
{ x: 1511.2, y: 375.9, r: 1.6, fill: MUTED, opacity: 0.55 }, // Alioth
{ x: 1521.1, y: 375.8, r: 1.5, fill: SEA, opacity: 0.5 }, // Mizar
{ x: 1533.4, y: 366.9, r: 1.7, fill: MUTED, opacity: 0.55 } // Alkaid
];那个有名字的星群之外的所有星星,都还是松散的填充星,大小和间距故意做得不规则,而不是排成网格,这样那两个真实的星座才能从周围的散点里真正跳出来,而不是混成一片。
星空缓缓转动
因为北极星离真正的天极足够近,拱极星在一整夜里是能看出绕着它转的,所以我让整个星群也跟着转,每 40 分钟转满一圈,变换的原点就钉在北极星自己的坐标上。在正常的浏览时间尺度下,比如有人花几分钟读一篇博客文章,这看起来完全是静止的。但要是把标签页开着放一小时在后台,就能看出它确实在转,跟真实夜空挂在峡湾上方、只要在外面待够久就能注意到的样子一样。我只让真实的那一群星转起来,填充用的散星没跟着动,因为它们本来就没有值得保留的真实位置,而且大多数离北极星太远,绕着这个支点做缓慢旋转根本看不出是在动。
一个月亮,以及知道什么时候该停手
场景里还有一个淡淡的月亮,白天是透明的,是嵌在页头地平线夜空里的一个柔和的浅色形状。这个场景目前就这些了,没有极光,没有流星,也还没有飘过的云层(云在计划清单上,跟"以后再扩充鸟群"放在同一个"也许,以后再说"的堆里)。我总是忍不住想再加点什么,又总是把自己劝住,因为这个纬度真实的夜空本身已经把有意思的部分做好了,我在这里的工作更像是别挡它的道,而不是再去给它添装饰。

不是可选项的克制
每一次闪烁,还有整个 40 分钟一圈的转动,都遵循 prefers-reduced-motion,只要用户设置了这个,画面就会完全静止,跟网站其他地方的环境鸟群和鱼群一样。而且这整层效果本来就只在暗色模式下存在,日间模式拿到的是透明的天空,连月亮都没有,因为大白天顶着一片看得见的星座图,不管怎么弄都只会显得像个错误。有时候克制说白了就是照着天空实际的样子来,而不是找借口多画点东西。
为什么偏偏是北纬 66 度改变了答案
大多数写在哪儿都行的软件,默认给人的感觉就是没有地方感,我在别处见过的大多数星空装饰也顺着这个思路来,均匀撒开的点,放到地球上任何一座城市头顶都说得过去。对大多数东西来说这是个合理的默认选择,但它丢掉了这个场景里唯一真正跟网站写作地点挂钩的那部分。在大约北纬 60 度左右,差不多是奥斯陆的纬度,北斗七星还是会像南边大多数星座一样有升有落,一年或一夜里的某些时段会落到地平线以下。往北到 66 度再往上,离天极足够近的星星就干脆不再落下了,不管什么时辰它们都在那儿绕着北极星转。这不是什么小小的美学差异,而是这个场景能有一个绕固定点缓慢无尽旋转的真正原因,而不是像太阳一样升起、划过天空、再落下的某个星座。那 40 分钟一圈的转动不是套在这个想法外面的装饰,它本身就是这个想法,直接从这个纬度的一个具体事实翻译成了一段 CSS 变换。
闪烁也在悄悄干别的活
真实星群和填充散星里都有一小撮星星,包括北极星在内,各自带着独立的透明度闪烁,周期缓慢且各不相同,每一颗的延迟都单独错开,这样它们就不会齐刷刷一起闪。这是个很小的细节,很容易被当成纯粹的氛围点缀打发过去,但它其实跟这个网站其他地方那些随机化的动画延迟干的是同一件事:打破那种整张图用一套 CSS 动画整齐划一套在每个元素上的死板感。要是每颗星都齐步闪烁,看起来就会像个加载动画。要是只有几颗,错开着,各自独立地闪,读起来就更接近真实的星光了。