【硬核】为了绘制这张占星图,作为前端的我又回想起了被初高中数学支配的恐惧

本文仅立足于技术和娱乐层面,崇尚科学,不鼓励、不传播封建迷信,本文所谈及的玄学知识仅供参考娱乐

最近对于西方占星有点感兴趣,所以找了些书籍看了下,知识量还是有点大的,很难一下子记住,在使用一些占星软件的时候,看着软件上展示的星盘,我忽然想到,如果我能用技术手段绘制出一张星盘的话,那么一来对于相关知识我肯定就能做到印象更加深刻,二来也能补充到很多细节知识,于是说干就干

本来我以为只是 canvas 画图罢了,应该很快搞定的,然而直到真正着手开始做的时候才发现还是有点复杂的,相关的占星知识查询过程就不说了,光是技术实现,工期就比一开始预估的多了好几倍

最让人郁闷的是,实现过程涉及到了多个初高中数学知识,这些对于初高中学生来说可能很一般,但对于早就大学毕业的我来说难度还是很大的,想上网搜搜都不知道关键词是啥,为此我还专门找了几页纸画图,这才终于完成

本文涉及到的技术或数学细节有 canvas 锯齿优化、绘制圆/图片/线段/圆环、勾股定理、三角函数(正弦、余弦、正切、反正弦)、碰撞检测

还是老规矩,本文的完整可运行代码已经上传至Github,搞了个在线 Demo(移动端或者 chrome开移动端模拟进行查看),鉴于本文知识比较硬核,看不看得下去都希望诸位点个赞再走呗 []( ̄▽ ̄)*

最终效果图:

23.jpg

星座

圆环

首先明确一点是,在 js中的坐标轴,是以参照物左上角为原点,向右 x轴增大,向下 y轴增大

4.jpg

图中最外层黄色圆环所在的区域就是十二星座区域

2.jpg

圆环就是两个半径不同的同心圆围成的区域,canvas 中可以使用 context.arc(x,y,r,sAngle,eAngle,counterclockwise) 进行绘制,调用两次,一次绘制外圆(设半径为 r1)一次绘制内圆(设半径为 r2

圆环是有背景色的,我这里采取的方式还是绘制一个同心圆,这个圆的半径是上面两个同心圆半径相加除以2,即 (r1 + r2) / 2,即刚好在到达圆环的中间位置,然后再将这个圆的 lineWidth 属性设为 r1 - r2,即圆环的宽度,strokeStyle的颜色设为圆环的颜色,那么调用 arc 之后,就绘制出了一个边宽为 r1 - r2,线条颜色为圆环颜色的同心圆,从视觉上看,就像是给上面两个同心圆围成的圆环设置了一个背景色一样,而实际上这个背景色只是第三个同心圆的线条颜色

/**
 * 绘制圆环
 * @param x0 圆环圆心x坐标
 * @param y0 圆环圆心y坐标
 * @param r1 圆环外圆直径
 * @param r2 圆环内圆直径
 * @param strokeColor 圆环颜色
 */
function drawRing(
  ctx: CanvasRenderingContext2D,
  x0: number,
  y0: number,
  r1: number,
  r2: number,
  strokeColor = '#f8f8c2'
) {
  ctx.save()
  ctx.beginPath()
  ctx.lineWidth = r1 - r2
  ctx.strokeStyle = strokeColor
  ctx.beginPath()
  ctx.arc(x0, y0, (r1 + r2) / 2, 0, Math.PI * 2)
  ctx.stroke()
  ctx.restore()
}
复制代码

绘制完圆后,如果你电脑分辨率还行,你可能会发现画出来的这个圆看着比较糊,不是那么顺滑,这是比较常见的 canvas锯齿问题,通过一个方法可以进行优化

const canvasImprove = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, size = clientW) => {
  canvas.style.width = size + "px"
  canvas.style.height = size + "px"
  canvas.height = size * window.devicePixelRatio
  canvas.width = size * window.devicePixelRatio
  ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
}
复制代码

效果如下(黑线是示意辅助线):

5.jpg

星座图标

首先需要将圆环使用起点为内圆终点为外圆的十二条线段等分成十二份,就得计算这十二条圆环内线段每条线段的起始坐标和终点坐标

因为明确地知道圆环是 360°,等分成十二份,那么每一份必然是 30°,所以十二个线段在圆环内的角度是明确的,这里需要注意的是,在 js的坐标系中,x轴重合,然后顺时针旋转,旋转多少就是多少度

放在星盘这个圆上,可以看成 x轴水平穿过圆心,有一条以圆心为端点、与 x轴完全重合的射线顺指针旋转,射线与 x轴的夹角即为 js坐标系中的角度,这与星盘在玄学知识上,以圆的最左端点为起始点逆时针旋转的逻辑不匹配,所以需要我们自行换算一下

6.jpg

但是光有角度不行,想用 canvas 画出一条线段来,必须得知道这条线段的明确起终坐标才行,问题转化为:已知圆的半径和圆心角,求圆上一点的坐标

7.jpg

如图,我们已经知道了 ∠BOA的度数和圆的半径 AO的长度,求 A点坐标,根据三角函数公式,可知:cos∠BOA = BO/AOsin∠BOA = AB/AO,即可求出BOAB的值,即点 Axy坐标

sincos分别对应 js 中的 Math.sinMath.cos,不过它们的参数都是弧度而非角度,所以需要一个角度转弧度的公式:

/**
 * 角度换算成弧度
 * @param degrees 角度
 */
const toRadians = (degrees: number) => degrees * Math.PI / 180
复制代码

即可求得所有十二条线段的起终坐标

下一步需要将十二星座的图标绘制到这十二条线段等分的十二个区域内,将图片绘制到 canvas画布上可以使用 drawImage(img, x, y, width, height),五个参数中,其中 xy是需要计算得到的,计算方法和上面计算十二条等分线段的起终坐标类似

最终效果如下:

9.jpg

十二宫

下图中除了黄色圆环外的内容,就认为是十二宫的内容

10.jpg

首先可以看到又来了一个浅蓝色背景的圆环,还是跟上面绘制黄色圆环的套路一样,画三个同心圆即可

然后有十二条起点在浅蓝色圆环内圆上、终点在黄色圆环内圆上的蓝色线段,这十二条蓝色线段划分出的十二个圆环区域称之为 ,与十二星座不同的是,十二宫每个宫占据的角度并不一定一样,依据不同的分宫制角度都是可能有差别的,也就是说,相邻两个蓝色线段的角度可能是不一样的

不过这也无所谓,因为只要知道角度,就可以按照上面十二星座的套路,通过三角函数计算得到每条线段的起、终坐标

同样的,十二宫每宫内的数字绘制,和十二星座图标的绘制也差不多,重点都是找到坐标

行星落点

在黄色圆环和蓝色圆环中间的区域,就是行星落入的区域,行星包括太阳、月亮、水星、金星、火星、木星、土星、天王星、海王星、冥王星十个主行星,南、北交点、黑月莉莉丝等特殊点,以及凯龙星、婚神星、谷神星等小行星,一般主流占星软件大概会给出20个左右的行星落点数据

11.jpg

行星的落入角度是由后端经过一系列计算给到前端的,所以行星角度是已知的,黄色圆环(设半径为 r3)和蓝色圆环(设半径为 r4)之间的区域比较大,但为了美观更主要是方便绘制,可以假定所有行星都落在黄色圆环的同心圆上,这个圆的半径是 (r3 + r4) / 2,即一个位于圆环中环线的圆,有了角度有了圆半径,那么就变成了跟上面十二星座圆环内绘制星座图标的问题,解法类似即可

但由于行星数量可能多达 20个左右,且角度是不可知的,那么就会出现一个问题,即多个行星之间角度太过靠近导致图标相互叠加,例如:

12.jpg

所以在计算得到行星的具体坐标后,还不能直接绘制,还得先要进行一下碰撞检测

13.jpg

碰撞检测的方法很多,在平面上,两个矩形之间只要确保不在 x轴或 y轴相交即可,具体化为只要满足以下两个条件任意一个,即可认为是没有碰撞(方法有很多,不局限于此):

  1. 两个矩形的左边之间的距离,大于各自长度(和水平面同方向的叫做长)的一半相加,即 AB > w1/2 + w3/2
  2. 两个矩形的上边之间的距离,大于各自宽度(与水平面相对垂直方向的叫做宽)的一半相加,即 CD > h1/2 + h2/2
// rect1 和 rect2 是否发生碰撞
const isRectCollision = (
  rect1: { x: number, y: number, width: number, height: number },
  rect2: { x: number, y: number, width: number, height: number }
) {
  return !(Math.abs(rect2.x - rect1.x) > (rect1.width / 2 + rect2.width / 2)
    || Math.abs(rect2.y - rect1.y) > (rect1.height / 2 + rect2.height / 2))
}
复制代码

由于已经知道了行星图标的坐标和尺寸(我们自己指定的),所以可以根据上述公式计算得到两个行星图标矩形是否发生了碰撞,如果发生了碰撞,那么应该进行位置处理,让它们不要叠加到一起

如何进行位置处理呢?随便朝某个方向位移一个距离只要保证不碰撞当然可以,但这样的话未免可能使得行星图标在星盘上的落点跟初始值不一样导致角度不对了,本着严谨的态度这不太合适

行星图标所在矩形的中心点与圆心划出一条连接线,图标只要在这个连线上移动,即可保证在圆内的角度不变。如下图,连线OP即为所需连接线,保证矩形的中心点在此连接线上,则无论矩形如何移动,其在圆内角度都是固定的

14.jpg

为了使得行星图标在圆内不过于分散且方便计算,我们只要找到一个最小移动距离保证行星图标之间没有碰撞就行了,那么来找这个最小移动距离

根据上面的碰撞检测理论可知,只要保证两个矩形在 x轴或 y轴上不相交就行了,考虑到实际场景,我们可以认为两个矩形如果只有边相交也属于没有碰撞,这种情况就是我们想要的两个矩形不碰撞的最小距离

这就会出现两个解,分别是在 x轴上刚好相交和在 y轴上刚好相交,至于到底在哪个边上相交所需移动距离最短,是可以进行计算比较的,例如对于下图中的红色矩形来说,紫色矩形是与其在 y轴上刚好相交且角度相同的矩形,绿色矩形是与其在 x轴上刚好相交且角度相同的矩形

15.jpg

所以紫色矩形和绿色矩形,相对于红色矩形需要在圆心-中心点直线上的位移距离是多少呢?

16.jpg

设点 A是红色矩形的中心点(x1, y1),点 B是紫色三角形的中心点(x2, y2),则从 A绘制水平线,从 B绘制垂直线,交于 C点,ABC三点围成一个直角三角形,其中点 A的坐标是行星图标落点坐标,∠CAB 是行星在星盘上的角度,AC的长度就是红色矩形长度一半 + 紫色矩形长度一半,都是已知的,所以 Bx坐标为 x = x1 + AC,再根据三角函数:tan∠CAB = BC / AC,得到Bx坐标为 y = y1 + tan∠CAB * AC,同理可得蓝色矩形的坐标

另外,这里可能会随着角度的不同,计算的变量也不同,例如,上述是对位于 [0°, 90°] 范围内的矩形的计算描述,而如果这个角度范围变成了 [90, 180],可能就要变一下了,不过也都是三角函数可以解决的,就是比较繁琐

还有个问题,对于红色矩形来说,在连接线上并不只有一个矩形会与其在 y轴上相交,而是有两个,分别在其两侧,考虑到行星图标挤在一起的可能不止有两个,而是多个,所以我们可以把这两个位置都计算得到,预留足够多的坑位

17.jpg

相位

星盘可以看做是圆,落在星盘上的行星可以看做是圆内的点,每个点与圆心相连可得到一条直线,每两条直线之间的角度即为两点之间的圆心角,如果圆心角的角度恰好等于一些特殊的角度,例如 0°(合相)60°(六分相)90°(四分相)120°(三分相)180°(二分相),则认为行星之间产生了相位

由于行星的落点随机性较大,所以两个行星之间的相位度数可能很难精确地刚好是一些特殊角度,所以产生了容许度的概念,只要两个行星的相位角度跟特殊角度的差别在一个范围内,就认为这也算是一个相位,例如我们认为 63°或者 58°也都是 六分相

如果两个行星产生了相位,则会在星盘上用不同颜色线连接这两个行星

18.jpg

经过前面的行星落点计算,可能并不是所有的行星都在一个圆上,但为了方便计算,我们可以根据前面的知识将行星的坐标再次进行位移,让它们的连接线围成一个圆,这样一来问题就变成了:已知圆的半径r,以及圆上两点坐标 (x1, y1)(x2, y2),求圆心角度数

如下图,O是圆的圆心,设点 A(x1, y1)、点 B(x2, y2)是圆上已知坐标的两点,连接 AOBOAB,并作 AB的垂直平分线 L,则L必定经过圆心 O,即 LCO重合,我们所需求得角度即 ∠AOB

根据勾股定理,AB^2 = (y2 - y1)^2 + (x2 - x2)^2,即可得到弦长 AB的长度,AC的长度为 AB的一半,AO是圆的半径 r,所以可得 sin∠AOC = AC/AO,可得 sin∠AOC的值,再通过反三角函数得到 ∠AOC的度数,那么可得 ∠AOB = 2 * ∠AOC,也就是最终得到了我们想要计算角度

/**
 * 已知半径为 r 的圆上两点坐标(x1, y1)、(x2, y2),求两点围成的圆心角
 */
export const genCenterAngle = (x1: number, y1: number, x2: number, y2: number, r: number) => {
  // 弦长
  const chord = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
  const halfAngle = Math.asin(chord / 2 / r)
  return toDegrees(halfAngle) * 2
}
复制代码

19.jpg

娱乐知识

再次声明,本文不传播封建迷信,不讨论这个东西是否科学是否真的有效,仅供参考娱乐

学习占星知识的过程中,发现了一个叫占星地图(Astro-Chart-Graphy,简称ACG)的东西,也称为择地占星,简单点说,这是一种可以用来选择吉地的占星方式,用途十分广泛,比如可以用来寻找有利学业、事业发展、寻找爱情和贵人的地方,外出旅游、出国移民,甚至时事政治等也都可以参考ACG

使用方式如下

20.jpg

进入Astro占星网站,在页面上输入你的名字(随便输一个就行不需要真名),tab选择 特殊占星图,占星图类型选择 占星地图-东亚,输入你的出生日期(这个要是真的,因为最终结果是以出生日期作为基准计算得到的),然后点击右侧的蓝色按钮,网站将出现一副东亚地图,地图上会有许多不同颜色的直线穿过(放在地球这个球体上当然就变成了曲线)

每条直线的端点处都会有一个图标,这些是行星图标,用于标识是哪个行星在你出生时相对于地球的位置,其线所经过的地点或者说具体城市,就是这颗行星的能量所能影响的地点或城市,不同的行星具有不同的能量

行星图标可参照如下

21.jpg

行星能量参考如下

行星线 含义
太阳线 代表你在这个地方更能实现自己的人生价值,活出自己,自我实现、名望和荣耀。
月亮线 代表在这个地方比较有安全感,比较舒服,也容易触发跟房子相关的问题。代表家庭、温馨舒适、女性、小孩、房地产。
水星线 代表脑力活动、学习、沟通,代表在这个城市比较能打开你的心智,想要去沟通表达,通常也跟商业相关。生意人也很适合选水星线经过的城市做生意。
金星线 代表你在这个地方比较旺桃花和人缘,也和赚钱、审美方面的事情相关。恋情,人际关系, 音乐艺术和享受。
火星线 很多人都说这是一个不好的地方,容易碰到冲突事件。但我认为这也是让我们很有动力的一个地方。暴力,鲁莽,受伤,意外。
木星线 代表这是一个让你觉得自信、获得许多帮助的地方,也很适合作为修行地。财富、幸运、乐观。
土星线 在这个地方,你容易感受到压力和阻碍,但是特别能增强自己的实力。阻碍、沮丧、自卑、失望、孤独。
天王星线 代表在这个地方,你容易遇到许多认为不寻常的经历。也比较想突破往常的自己。冒险、刺激、新奇。
海王星线 这个地方和身心灵相关,也和我们的梦想相关。也有占星师说,修行选择月海交汇的地方比较好。神秘、精神启发、宗教幻想、逃避现实。
冥王星线 这可能是一个痛苦的地方,你可能会经历一些颠覆自己的事情,比如遇到危机或劫后重生的感觉。感受生死、 超神秘体验, 业力呈现。

例如对于上面的东亚地图,我们可以看到火星这个行星所代表的红色曲线穿过或靠近的中国主要城市包括南京、福州,可能意味着这幅占星地图的主人在这两个城市会留下不太好的印象、糟糕的体验、激烈的冲突,代表太阳的橙色线经过的城市有昆明,可能意味着这幅占星地图的主人在这个城市更能实现自己的人生价值,收获名望和荣耀

小结

当把这个需求实现完成后,回过头来看好像也没啥,小问题一个个冒出来,但一个个解决就是了,然而过程还是比较折磨人的,解数学题就够头大的了,实际上很多数学公式与业务实际代码实现之间的场景兼容、边界判断我还没写出来,不过真正实现完成后,还是比较有成就感的

猜你喜欢

转载自juejin.im/post/7106397555600130056
今日推荐