<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>idelem</title>
    <link>https://matterofti.me/idelem/</link>
    <description></description>
    <pubDate>Sun, 26 Apr 2026 21:09:06 +0000</pubDate>
    <item>
      <title>紫微代数 1</title>
      <link>https://matterofti.me/idelem/zi-wei-dai-shu-1</link>
      <description>&lt;![CDATA[本系列文章将研究紫微斗数的一些数学性质。&#xA;&#xA;首先介绍紫微斗数排盘算法，为示范起见，以阳历 2021 年 3 月 19 日（农历辛丑年二月初七）亥时出生的女性为例。&#xA;&#xA;0 安十二宫&#xA;起寅宫，先顺数生月 (2) 到卯，再逆数生时 (12) 到辰。命宫的位置在辰。注意这一步计数是包括起点宫位的。&#xA;6fdjm9.png&#xA;&#xA;0.5 安身宫&#xA;起寅宫，先顺数生月 (2) 到卯，再顺数生时 (12) 到寅。&#xA;易证：身宫只可能为命、夫、财、迁、官、福之一，因为它和命宫之间隔着双倍的生时。&#xA;&#xA;1 起寅首&#xA;出生年为辛年 (8)，则寅宫宫干为 $(82+1) \mod 10 = 7$，为庚。&#xA;&#xA;|$x$|$(x  2 + 1)\mod 10$|$x$|$(x  2 + 1)\mod 10$|&#xA;|---|---|---|---|&#xA;|1|3|6|3|&#xA;|2|5|7|5|&#xA;|3|7|8|7|&#xA;|4|9|9|9|&#xA;|5|1|10|1|&#xA;&#xA;由上表可见，其实寅宫宫干只有五种可能性。换句话说，天干地支的阴阳必须相配，不能混搭。&#xA;&#xA;2 定纳音五行局&#xA;命宫为壬辰，于是查表，为水二局。&#xA;因为五行局是相邻的阴阳干、阴阳支为一组，每一年的寅宫宫干相同，而命盘中相邻时辰必有相邻的阴阳干支，故每天有六个局，且和前后连续。&#xA;&#xA;3 定紫微星&#xA;这一步比较复杂，而且是第一个用到出生日的步骤。&#xA;&#xA;  得出命造五行局後，推判幾倍的命造五行局數可以大於生日數（例如：十六日生人木三局者則六倍，商數+1,得可大與生日數）；下一步判斷得出來的倍數與生日數之差數（(商數+1）五行局數-生日數)，再判斷此差數為奇數或偶數；若差數為奇數，則以倍數減去差數得到一個新的數字；若差數為偶數，則倍數與差數相加而得一新的數字，下一步起寅宮並順時針數到上一步驟得出的數目，此一落宮點便是紫微星的位置；依照上一步驟如果改為逆時針來數，便是安天府星。&#xA;  生日數 = (商+1)五行局數-差&#xA;寅起步數 = ((-1)^差)差+商+if(餘  0, then 1, else 0)&#xA;&#xA;水二局的 4 倍可大于生日数 (7)，余数 = 1。因余数为奇数，所以先从寅顺行 4 步到巳，再倒退 1 步回辰，紫微星在命宫。注意这一步计数是包括寅宫的。&#xA;&#xA;由于五行局有 2 3 4 5 6 这几种倍数，农历最多 30 天（大月 30，小月 29），故最多有可能需要顺行 15 步。&#xA;&#xA;在排出表格后，可以发现对于局数 $n$ 来说，从初一到初 $n$ 的位置一旦定下，其余按同余关系顺时针排列即可，每一圈都是 $\mathbb{Z}/\{x\in \mathbb{Z}\ |\ x\mod n = 0\}$ 的 coset（陪集）。&#xA;&#xA;水二局&#xA;&#xA;| 地支 | 日数          | 地支 | 日数  | 地支 | 日数        |&#xA;| ---- | ------------- | ---- | ----- | ---- | ----------- |&#xA;| 寅   | 2 3 26 27 | 午   | 10 11 | 戌   | 18 19       |&#xA;| 卯   | 4 5 28 29     | 未   | 12 13 | 亥   | 20 21       |&#xA;| 辰   | 6 7 30        | 申   | 14 15 | 子   | 22 23       |&#xA;| 巳   | 8 9           | 酉   | 16 17 | 丑   | 1 24 25 | &#xA;&#xA;木三局&#xA;&#xA;| 地支 | 日数       | 地支 | 日数     | 地支 | 日数     |&#xA;| ---- | ---------- | ---- | -------- | ---- | -------- |&#xA;| 寅   | 3 5    | 午   | 7 15 17  | 戌   | 19 27 29 |&#xA;| 卯   | 6 8        | 未   | 10 18 20 | 亥   | 22 30    | &#xA;| 辰   | 1 9 11 | 申   | 13 21 23 | 子   | 25 23    |&#xA;| 巳   | 4 12 14    | 酉   | 16 24 26 | 丑   | 2 28 |&#xA;&#xA;金四局&#xA;&#xA;| 地支 | 日数           | 地支 | 日数        | 地支 | 日数     |&#xA;| ---- | -------------- | ---- | ----------- | ---- | -------- |&#xA;| 寅   | 4 7 13     | 午   | 10 20 23 29 | 戌   | 26       |&#xA;| 卯   | 8 11 17        | 未   | 14 28 27    | 亥   | 1 30 |&#xA;| 辰   | 2 12 15 21 | 申   | 18 28       | 子   | 5        |&#xA;| 巳   | 6 16 19 25     | 酉   | 22          | 丑   | 3 9  |&#xA;&#xA;土五局&#xA;&#xA;| 地支 | 日数           | 地支 | 日数           | 地支 | 日数     |&#xA;| ---- | -------------- | ---- | -------------- | ---- | -------- |&#xA;| 寅   | 5 9 17     | 午   | 1 13 25 29 | 戌   | 21       |&#xA;| 卯   | 10 14 22       | 未   | 6 18  30       | 亥   | 2 26 |&#xA;| 辰   | 3 15 19 27 | 申   | 11 23          | 子   | 7        |&#xA;| 巳   | 8 20 24        | 酉   | 16 28          | 丑   | 4 12 |&#xA;&#xA;火六局&#xA;&#xA;| 地支 | 日数        | 地支 | 日数        | 地支 | 日数        |&#xA;| ---- | ----------- | ---- | ----------- | ---- | ----------- |&#xA;| 寅   | 6 11 21 | 午   | 2 16 30 | 戌   | 7 26        |&#xA;| 卯   | 12 17 27    | 未   | 8 22        | 亥   | 3 13    |&#xA;| 辰   | 4 18 23 | 申   | 14 28       | 子   | 9 19        |&#xA;| 巳   | 10 24 29    | 酉   | 1 20    | 丑   | 5 15 25 |&#xA;&#xA;故寅起步数实际公式为&#xA;$$步数 = (-1)^{局数 - 余}  (局数 - 余)$$&#xA;例如 13 的火六局起步数为 $(-1)^15 = -5$，逆走五步。&#xA;或，余 = 0 的情况下，步数为 0。&#xA;此后加上 $\lfloor 13 / 6 \rfloor = 2$，顺走两步即可。&#xA;&#xA;def ziwei(day: int, wuxing: int) -  int:&#xA;  m = day % wuxing&#xA;  if m:&#xA;    start = pow(-1, wuxing - m) * (wuxing - m)&#xA;  else:&#xA;    start = -1&#xA;  step = 3 + start + day // wuxing&#xA;  return step&#xA;&#xA;4 定天府星&#xA;天府和紫微永远关于寅-申线对称。&#xA;从天文的角度来说，寅申是立春和立秋，而紫微天府分别代表北斗和南斗星系的主星。&#xA;设 a 为紫微地支，b 为天府地支，则它们符合 $(a + b) \mod 3 = 6$ 的关系。&#xA;因为关于寅-申对称，所以上一步逆时针数就会得到天府星。&#xA;&#xA;完成上面几步后，命盘看起来是这样：&#xA;6fBeRf.png&#xA;&#xA;5 定南北斗中天诸星&#xA;&#xA;  紫微逆去天機星，隔一太陽武曲辰，連接天同空二宮，廉貞居處方是真。&#xA;  天府順行有太陰，貪狼而後巨門臨，隨來天相天梁繼，七殺空三是破軍。&#xA;&#xA;按字面意思，便是：&#xA;&#xA;（逆时针）紫微-天机-空-太阳-武曲-天同-空-空-廉贞-空-空-空&#xA;（顺时针）天府-太阴-贪狼-巨门-天相-天梁-七杀-空-空-空-破军-空&#xA;&#xA;从寅到申一共有六种组合：&#xA;&#xA;6fs1dH.png 6fslee.png 6fsMLD.png&#xA;6fs3od.png 6fsGFA.png 6fsJJI.png&#xA;&#xA;旋转 180° 后可得到另外六种组合，在此不列出。&#xA;&#xA;通过安星法则，不难看出：&#xA;&#xA;天府系的杀破狼永远在彼此的三方位置&#xA;紫微系的紫武廉永远在彼此的三方位置&#xA;紫微系诸星的对宫没有紫微系星 (因为紫微系星只有 6 颗，保证其序数 mod 6 不重复即可)&#xA;天府对宫永远是七杀&#xA;天相对宫永远是破军&#xA;巨门不可能逢天府系星&#xA;太阳不可能逢紫微系星&#xA;剩下的星星里，天机-天同，天府-天相，太阴-天梁均成三合 (120°) 关系&#xA;&#xA;最后得到的命盘如图：&#xA;6fy1XT.png&#xA;&#xA;6 安辅弼昌曲空劫&#xA;辰宫开始顺时针数生月为左辅，戌宫开始逆时针数生月为右弼。&#xA;辰宫开始顺时针数生时为文曲，戌宫开始逆时针数生时为文昌。&#xA;亥宫开始顺时针数生时为地劫，亥宫开始逆时针数生时为地空。&#xA;&#xA;由上可得：&#xA;&#xA;辅弼、昌曲都关于丑未线对称（辰戌丑未是四墓地）&#xA;空劫关于巳亥线对称&#xA;同时辰出生则昌曲、空劫位置相同&#xA;同月出生则辅弼位置相同&#xA;若要命三方四正同时逢昌曲，则需昌曲分别在卯亥、巳酉、未、丑、辰戌的位置，且满足一定生时条件。经计算，有下列几种情况：&#xA;&#xA;| 昌曲在 | 命在 | 生时 | 生月 |&#xA;| --- | --- | --- | --- |&#xA;| 卯亥 | 卯亥未 (4 8 12) | 未亥 | 正月 五月 九月 |&#xA;| 巳酉 | 巳酉丑 (6 10 2) | 丑巳 | 正月 五月 九月 |&#xA;| 未 | 卯亥未丑 (2 4 8 12) | 卯 | 正月 三月 五月 九月 |&#xA;| 丑 | 卯亥未丑 (2 4 8 12) | 酉 | 三月 七月 九月 十一月 |&#xA;| 辰戌 | 辰戌 (5 11) | 子午 | 三月 九月 五月 十一月 |&#xA;&#xA;结论：偶数月份出生者不会同时逢昌曲。&#xA;&#xA;若要命被辅弼夹，则需命在丑未：&#xA;&#xA;| 辅弼在 | 命在 | 生月 | 生时 |&#xA;| --- | --- | --- | --- |&#xA;| 寅子 | 丑 | 九月 | 酉 |&#xA;| 寅子 | 丑 | 十一月 | 亥 |&#xA;| 午申 | 未 | 三月 | 酉 |&#xA;| 午申 | 未 | 五月 | 亥 |&#xA;&#xA;结论：偶数月份出生者命不会被辅弼夹。&#xA;&#xA;地空文昌、地劫文曲不可能同宫&#xA;&#xA;7 安魁钺&#xA;&#xA;  甲戊庚之年丑未，乙己之年子申，辛年午寅，壬癸之年卯巳，丙丁之年亥酉。&#xA;&#xA;由此可看出魁钺关于辰-戌线对称。&#xA;&#xA;若要命三方四正同时逢魁钺，则：&#xA;&#xA;| 魁钺在 | 命在 | 生年 |&#xA;| --- | --- | --- |&#xA;| 亥酉 | 卯 | 丙丁 | &#xA;| 子申 | 子申辰 | 乙己 |&#xA;| 丑未 | 丑未 | 甲戊庚 |&#xA;| 午寅 | 午寅亥 | 辛 |&#xA;| 卯巳 | 酉 | 壬癸 |&#xA;&#xA;8 安禄存擎羊陀罗（年干）&#xA;&#xA;  甲祿到寅宮，乙祿居卯府，丙戊祿在巳，丁己祿在午，庚祿定居申，辛祿酉上補，壬祿亥中藏，癸祿居子戶，祿前羊刃當，祿後陀羅府。&#xA;&#xA;如果在命盘上顺时针走，必会先后连续遇到陀罗、禄存、擎羊&#xA;四墓地没有禄存&#xA;四陷地没有陀罗&#xA;四马地没有擎羊（亦即，擎羊不在角上）&#xA;&#xA;9 安天马（年支）&#xA;&#xA;  寅午戍年馬在申，申子辰年馬在寅，巳酉丑年馬在亥，亥卯未年馬在巳。&#xA;&#xA;天马必在四马地（必不逢擎羊）&#xA;&#xA;10 安火星铃星（年支+生时）&#xA;&#xA;  申子辰人寅戌揚，寅午戌人丑卯方，巳酉丑人卯戌位，亥卯未人酉戌房。&#xA;&#xA;这个口诀的意思是，根据年支，火铃分别从两个地支的宫位开始，顺时针数到生时。以范例命盘为例，丑年生人从卯戌数起到亥时，即寅酉。&#xA;&#xA;TBC]]&gt;</description>
      <content:encoded><![CDATA[<p>本系列文章将研究紫微斗数的一些数学性质。</p>

<p>首先介绍紫微斗数排盘算法，为示范起见，以阳历 2021 年 3 月 19 日（农历辛丑年二月初七）亥时出生的女性为例。</p>

<p>0 安十二宫
起寅宫，先顺数生月 (2) 到卯，再逆数生时 (12) 到辰。命宫的位置在辰。注意这一步计数是<strong>包括起点宫位</strong>的。
<img src="https://s4.ax1x.com/2021/03/19/6fdjm9.png" alt="6fdjm9.png"></p>

<p>0.5 安身宫
起寅宫，先顺数生月 (2) 到卯，再顺数生时 (12) 到寅。
易证：身宫只可能为命、夫、财、迁、官、福之一，因为它和命宫之间隔着双倍的生时。</p>

<p>1 起寅首
出生年为辛年 (8)，则寅宫宫干为 $(8*2+1) \mod 10 = 7$，为庚。</p>

<table>
<thead>
<tr>
<th>$x$</th>
<th>$(x * 2 + 1)\mod 10$</th>
<th>$x$</th>
<th>$(x * 2 + 1)\mod 10$</th>
</tr>
</thead>

<tbody>
<tr>
<td>1</td>
<td>3</td>
<td>6</td>
<td>3</td>
</tr>

<tr>
<td>2</td>
<td>5</td>
<td>7</td>
<td>5</td>
</tr>

<tr>
<td>3</td>
<td>7</td>
<td>8</td>
<td>7</td>
</tr>

<tr>
<td>4</td>
<td>9</td>
<td>9</td>
<td>9</td>
</tr>

<tr>
<td>5</td>
<td>1</td>
<td>10</td>
<td>1</td>
</tr>
</tbody>
</table>

<p>由上表可见，其实寅宫宫干只有五种可能性。换句话说，天干地支的阴阳必须相配，不能混搭。</p>

<p>2 定纳音五行局
命宫为壬辰，于是<a href="https://zh.wikipedia.org/wiki/%E7%B4%8D%E9%9F%B3" rel="nofollow">查表</a>，为水二局。
因为五行局是相邻的阴阳干、阴阳支为一组，每一年的寅宫宫干相同，而命盘中相邻时辰必有相邻的阴阳干支，故每天有六个局，且和前后连续。</p>

<p>3 定紫微星
这一步比较复杂，而且是第一个用到出生日的步骤。</p>

<blockquote><p>得出命造五行局後，推判幾倍的命造五行局數可以大於生日數（例如：十六日生人木三局者則六倍，商數+1,得可大與生日數）；下一步判斷得出來的倍數與生日數之差數（(商數+1）<em>五行局數-生日數)，再判斷此差數為奇數或偶數；若差數為奇數，則以倍數減去差數得到一個新的數字；若差數為偶數，則倍數與差數相加而得一新的數字，下一步起寅宮並順時針數到上一步驟得出的數目，此一落宮點便是紫微星的位置；依照上一步驟如果改為逆時針來數，便是安天府星。
生日數 = (商+1)</em>五行局數-差
寅起步數 = ((-1)^差)*差+商+if(餘&gt;0, then 1, else 0)</p></blockquote>

<p>水二局的 4 倍可大于生日数 (7)，余数 = 1。因余数为奇数，所以先从寅顺行 4 步到巳，再倒退 1 步回辰，紫微星在命宫。注意这一步计数是<strong>包括寅宫</strong>的。</p>

<p>由于五行局有 2 3 4 5 6 这几种倍数，农历最多 30 天（大月 30，小月 29），故最多有可能需要顺行 15 步。</p>

<p>在排出表格后，可以发现对于局数 $n$ 来说，从初一到初 $n$ 的位置一旦定下，其余按同余关系顺时针排列即可，每一圈都是 $\mathbb{Z}/{x\in \mathbb{Z}\ |\ x\mod n = 0}$ 的 coset（陪集）。</p>

<p>水二局</p>

<table>
<thead>
<tr>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
</tr>
</thead>

<tbody>
<tr>
<td>寅</td>
<td><strong>2</strong> 3 26 27</td>
<td>午</td>
<td>10 11</td>
<td>戌</td>
<td>18 19</td>
</tr>

<tr>
<td>卯</td>
<td>4 5 28 29</td>
<td>未</td>
<td>12 13</td>
<td>亥</td>
<td>20 21</td>
</tr>

<tr>
<td>辰</td>
<td>6 7 30</td>
<td>申</td>
<td>14 15</td>
<td>子</td>
<td>22 23</td>
</tr>

<tr>
<td>巳</td>
<td>8 9</td>
<td>酉</td>
<td>16 17</td>
<td>丑</td>
<td><strong>1</strong> 24 25</td>
</tr>
</tbody>
</table>

<p>木三局</p>

<table>
<thead>
<tr>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
</tr>
</thead>

<tbody>
<tr>
<td>寅</td>
<td><strong>3</strong> 5</td>
<td>午</td>
<td>7 15 17</td>
<td>戌</td>
<td>19 27 29</td>
</tr>

<tr>
<td>卯</td>
<td>6 8</td>
<td>未</td>
<td>10 18 20</td>
<td>亥</td>
<td>22 30</td>
</tr>

<tr>
<td>辰</td>
<td><strong>1</strong> 9 11</td>
<td>申</td>
<td>13 21 23</td>
<td>子</td>
<td>25 23</td>
</tr>

<tr>
<td>巳</td>
<td>4 12 14</td>
<td>酉</td>
<td>16 24 26</td>
<td>丑</td>
<td><strong>2</strong> 28</td>
</tr>
</tbody>
</table>

<p>金四局</p>

<table>
<thead>
<tr>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
</tr>
</thead>

<tbody>
<tr>
<td>寅</td>
<td><strong>4</strong> 7 13</td>
<td>午</td>
<td>10 20 23 29</td>
<td>戌</td>
<td>26</td>
</tr>

<tr>
<td>卯</td>
<td>8 11 17</td>
<td>未</td>
<td>14 28 27</td>
<td>亥</td>
<td><strong>1</strong> 30</td>
</tr>

<tr>
<td>辰</td>
<td><strong>2</strong> 12 15 21</td>
<td>申</td>
<td>18 28</td>
<td>子</td>
<td>5</td>
</tr>

<tr>
<td>巳</td>
<td>6 16 19 25</td>
<td>酉</td>
<td>22</td>
<td>丑</td>
<td><strong>3</strong> 9</td>
</tr>
</tbody>
</table>

<p>土五局</p>

<table>
<thead>
<tr>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
</tr>
</thead>

<tbody>
<tr>
<td>寅</td>
<td><strong>5</strong> 9 17</td>
<td>午</td>
<td><strong>1</strong> 13 25 29</td>
<td>戌</td>
<td>21</td>
</tr>

<tr>
<td>卯</td>
<td>10 14 22</td>
<td>未</td>
<td>6 18  30</td>
<td>亥</td>
<td><strong>2</strong> 26</td>
</tr>

<tr>
<td>辰</td>
<td><strong>3</strong> 15 19 27</td>
<td>申</td>
<td>11 23</td>
<td>子</td>
<td>7</td>
</tr>

<tr>
<td>巳</td>
<td>8 20 24</td>
<td>酉</td>
<td>16 28</td>
<td>丑</td>
<td><strong>4</strong> 12</td>
</tr>
</tbody>
</table>

<p>火六局</p>

<table>
<thead>
<tr>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
<th>地支</th>
<th>日数</th>
</tr>
</thead>

<tbody>
<tr>
<td>寅</td>
<td><strong>6</strong> 11 21</td>
<td>午</td>
<td><strong>2</strong> 16 30</td>
<td>戌</td>
<td>7 26</td>
</tr>

<tr>
<td>卯</td>
<td>12 17 27</td>
<td>未</td>
<td>8 22</td>
<td>亥</td>
<td><strong>3</strong> 13</td>
</tr>

<tr>
<td>辰</td>
<td><strong>4</strong> 18 23</td>
<td>申</td>
<td>14 28</td>
<td>子</td>
<td>9 19</td>
</tr>

<tr>
<td>巳</td>
<td>10 24 29</td>
<td>酉</td>
<td><strong>1</strong> 20</td>
<td>丑</td>
<td><strong>5</strong> 15 25</td>
</tr>
</tbody>
</table>

<p>故寅起步数实际公式为
$$步数 = (-1)^{局数 – 余} * (局数 – 余)$$
例如 13 的火六局起步数为 $(-1)^1*5 = -5$，逆走五步。
或，余 = 0 的情况下，步数为 0。
此后加上 $\lfloor 13 / 6 \rfloor = 2$，顺走两步即可。</p>

<pre><code class="language-python">def ziwei(day: int, wuxing: int) -&gt; int:
  m = day % wuxing
  if m:
    start = pow(-1, wuxing - m) * (wuxing - m)
  else:
    start = -1
  step = 3 + start + day // wuxing
  return step
</code></pre>

<p>4 定天府星
天府和紫微永远关于<strong>寅-申</strong>线对称。
从天文的角度来说，寅申是立春和立秋，而紫微天府分别代表北斗和南斗星系的主星。
设 a 为紫微地支，b 为天府地支，则它们符合 $(a + b) \mod 3 = 6$ 的关系。
因为关于寅-申对称，所以上一步逆时针数就会得到天府星。</p>

<p>完成上面几步后，命盘看起来是这样：
<img src="https://s4.ax1x.com/2021/03/19/6fBeRf.png" alt="6fBeRf.png"></p>

<p>5 定南北斗中天诸星</p>

<blockquote><p>紫微逆去天機星，隔一太陽武曲辰，連接天同空二宮，廉貞居處方是真。
天府順行有太陰，貪狼而後巨門臨，隨來天相天梁繼，七殺空三是破軍。</p></blockquote>

<p>按字面意思，便是：</p>
<ol><li>（逆时针）紫微-天机-空-太阳-武曲-天同-空-空-廉贞-空-空-空</li>
<li>（顺时针）天府-太阴-贪狼-巨门-天相-天梁-七杀-空-空-空-破军-空</li></ol>

<p>从寅到申一共有六种组合：</p>

<p><img src="https://s4.ax1x.com/2021/03/20/6fs1dH.png" alt="6fs1dH.png"> <img src="https://s4.ax1x.com/2021/03/20/6fslee.png" alt="6fslee.png"> <img src="https://s4.ax1x.com/2021/03/20/6fsMLD.png" alt="6fsMLD.png">
<img src="https://s4.ax1x.com/2021/03/20/6fs3od.png" alt="6fs3od.png"> <img src="https://s4.ax1x.com/2021/03/20/6fsGFA.png" alt="6fsGFA.png"> <img src="https://s4.ax1x.com/2021/03/20/6fsJJI.png" alt="6fsJJI.png"></p>

<p>旋转 180° 后可得到另外六种组合，在此不列出。</p>

<p>通过安星法则，不难看出：</p>
<ol><li>天府系的杀破狼永远在彼此的三方位置</li>
<li>紫微系的紫武廉永远在彼此的三方位置</li>
<li>紫微系诸星的对宫没有紫微系星 (因为紫微系星只有 6 颗，保证其序数 mod 6 不重复即可)</li>
<li>天府对宫永远是七杀</li>
<li>天相对宫永远是破军</li>
<li>巨门不可能逢天府系星</li>
<li>太阳不可能逢紫微系星</li>
<li>剩下的星星里，天机-天同，天府-天相，太阴-天梁均成三合 (120°) 关系</li></ol>

<p>最后得到的命盘如图：
<img src="https://s4.ax1x.com/2021/03/20/6fy1XT.png" alt="6fy1XT.png"></p>

<p>6 安辅弼昌曲空劫
辰宫开始顺时针数生月为左辅，戌宫开始逆时针数生月为右弼。
辰宫开始顺时针数生时为文曲，戌宫开始逆时针数生时为文昌。
亥宫开始顺时针数生时为地劫，亥宫开始逆时针数生时为地空。</p>

<p>由上可得：</p>
<ul><li>辅弼、昌曲都关于丑未线对称（辰戌丑未是四墓地）</li>
<li>空劫关于巳亥线对称</li>
<li>同时辰出生则昌曲、空劫位置相同</li>
<li>同月出生则辅弼位置相同</li>
<li>若要命三方四正同时逢昌曲，则需昌曲分别在卯亥、巳酉、未、丑、辰戌的位置，且满足一定生时条件。经计算，有下列几种情况：</li></ul>

<table>
<thead>
<tr>
<th>昌曲在</th>
<th>命在</th>
<th>生时</th>
<th>生月</th>
</tr>
</thead>

<tbody>
<tr>
<td>卯亥</td>
<td>卯亥未 (4 8 12)</td>
<td>未亥</td>
<td>正月 五月 九月</td>
</tr>

<tr>
<td>巳酉</td>
<td>巳酉丑 (6 10 2)</td>
<td>丑巳</td>
<td>正月 五月 九月</td>
</tr>

<tr>
<td>未</td>
<td>卯亥未丑 (2 4 8 12)</td>
<td>卯</td>
<td>正月 三月 五月 九月</td>
</tr>

<tr>
<td>丑</td>
<td>卯亥未丑 (2 4 8 12)</td>
<td>酉</td>
<td>三月 七月 九月 十一月</td>
</tr>

<tr>
<td>辰戌</td>
<td>辰戌 (5 11)</td>
<td>子午</td>
<td>三月 九月 五月 十一月</td>
</tr>
</tbody>
</table>

<p>结论：偶数月份出生者不会同时逢昌曲。</p>
<ul><li>若要命被辅弼夹，则需命在丑未：</li></ul>

<table>
<thead>
<tr>
<th>辅弼在</th>
<th>命在</th>
<th>生月</th>
<th>生时</th>
</tr>
</thead>

<tbody>
<tr>
<td>寅子</td>
<td>丑</td>
<td>九月</td>
<td>酉</td>
</tr>

<tr>
<td>寅子</td>
<td>丑</td>
<td>十一月</td>
<td>亥</td>
</tr>

<tr>
<td>午申</td>
<td>未</td>
<td>三月</td>
<td>酉</td>
</tr>

<tr>
<td>午申</td>
<td>未</td>
<td>五月</td>
<td>亥</td>
</tr>
</tbody>
</table>

<p>结论：偶数月份出生者命不会被辅弼夹。</p>
<ul><li>地空文昌、地劫文曲不可能同宫</li></ul>

<p>7 安魁钺</p>

<blockquote><p>甲戊庚之年丑未，乙己之年子申，辛年午寅，壬癸之年卯巳，丙丁之年亥酉。</p></blockquote>

<p>由此可看出魁钺关于辰-戌线对称。</p>

<p>若要命三方四正同时逢魁钺，则：</p>

<table>
<thead>
<tr>
<th>魁钺在</th>
<th>命在</th>
<th>生年</th>
</tr>
</thead>

<tbody>
<tr>
<td>亥酉</td>
<td>卯</td>
<td>丙丁</td>
</tr>

<tr>
<td>子申</td>
<td>子申辰</td>
<td>乙己</td>
</tr>

<tr>
<td>丑未</td>
<td>丑未</td>
<td>甲戊庚</td>
</tr>

<tr>
<td>午寅</td>
<td>午寅亥</td>
<td>辛</td>
</tr>

<tr>
<td>卯巳</td>
<td>酉</td>
<td>壬癸</td>
</tr>
</tbody>
</table>

<p>8 安禄存擎羊陀罗（年干）</p>

<blockquote><p>甲祿到寅宮，乙祿居卯府，丙戊祿在巳，丁己祿在午，庚祿定居申，辛祿酉上補，壬祿亥中藏，癸祿居子戶，祿前羊刃當，祿後陀羅府。</p></blockquote>
<ul><li>如果在命盘上顺时针走，必会先后连续遇到陀罗、禄存、擎羊</li>
<li>四墓地没有禄存</li>
<li>四陷地没有陀罗</li>
<li>四马地没有擎羊（亦即，擎羊不在角上）</li></ul>

<p>9 安天马（年支）</p>

<blockquote><p>寅午戍年馬在申，申子辰年馬在寅，巳酉丑年馬在亥，亥卯未年馬在巳。</p></blockquote>
<ul><li>天马必在四马地（必不逢擎羊）</li></ul>

<p>10 安火星铃星（年支+生时）</p>

<blockquote><p>申子辰人寅戌揚，寅午戌人丑卯方，巳酉丑人卯戌位，亥卯未人酉戌房。</p></blockquote>

<p>这个口诀的意思是，根据年支，火铃分别从两个地支的宫位开始，顺时针数到生时。以范例命盘为例，丑年生人从卯戌数起到亥时，即寅酉。</p>

<p>TBC</p>
]]></content:encoded>
      <guid>https://matterofti.me/idelem/zi-wei-dai-shu-1</guid>
      <pubDate>Fri, 19 Mar 2021 15:09:38 +0000</pubDate>
    </item>
    <item>
      <title>Processing 笔记本 (2)</title>
      <link>https://matterofti.me/idelem/processing-bi-ji-ben-2</link>
      <description>&lt;![CDATA[rgb.pngryb.png&#xA;&#xA;这次做的是一个调色盘 demo 项目，练手顺便实验颜料混合算法（事实证明符合物理直觉的颜料混合非常复杂）。&#xA;&#xA;涉及到的 Processing 知识：&#xA;&#xA;类和对象&#xA;如何显示对象&#xA;从文件读取&#xA;&#xA;需求和交互设计（打草稿！）&#xA;&#xA;首先考虑这个调色盘有哪些功能。最初的设想很简单：&#xA;&#xA;正方形，划分成 16 格，每格里有颜色或空白&#xA;左键混色（落笔），右键吸色（蘸取）&#xA;颜料转移时会被稀释，便于控制添加颜色的量（后来发现很难做，要重构）&#xA;&#xA;程序框架&#xA;&#xA;决定了功能需求后，整个程序的框架也就呼之欲出了：&#xA;&#xA;import java.util.;&#xA;import java.text.;&#xA;&#xA;// 记录当前颜色和透明度的全局变量&#xA;color brushcolor = color(0, 0, 0);&#xA;float brushalpha = 0.0;&#xA;// TODO  我们还需要定义一个 Paint 类来储存和显示颜料，过会儿再说&#xA;// Paint[] palette = new Paint [16];&#xA;&#xA;// 这是启动时执行的函数，用于初始化一些数据&#xA;void setup() {&#xA;  // 分辨率 256256，每格宽高 64&#xA;  size(256, 256);&#xA;  // TODO 在这里初始化和显示所有颜料&#xA;}&#xA;&#xA;// 每帧更新画布的函数&#xA;void draw() {&#xA;  // TODO 每帧画背景和颜料&#xA;}&#xA;&#xA;// 每次鼠标点击后都会触发的函数&#xA;void mousePressed(){&#xA;  if (mouseButton == LEFT) {&#xA;    // 点击左键混色&#xA;  }&#xA;  else if (mouseButton == RIGHT) {&#xA;    // 点击右键取色&#xA;  }&#xA;}&#xA;&#xA;这里还需要解决一个问题：如何确定鼠标点击时选择了哪格颜色？由于颜色平铺在 4x4 的方格里，可以写一个 getcolorindex(int mouseX, int mouseY) 函数，传入点击的屏幕坐标，返回颜色序号（0-15）：&#xA;&#xA;int getcolorindex(int x, int y) {&#xA;  int row = y / 64;&#xA;  int col = x / 64;&#xA;  return row  4 + col;&#xA;}&#xA;&#xA;Paint 类&#xA;&#xA;接下来要给颜料写一个类，设计如下。&#xA;&#xA;属性（要给每格颜料存储的数据）：&#xA;&#xA;当前颜色&#xA;当前透明度&#xA;所在的格子编号（用于绘制该对象）&#xA;&#xA;方法（每格颜料可以进行的操作）：&#xA;&#xA;混色（改变当前颜色）&#xA;吸色&#xA;&#xA;public class Paint {&#xA;  // 颜料对象的属性&#xA;  color paintcolor = color(0,0,0);&#xA;  float paintalpha = 0.0;&#xA;  int index = 0;&#xA;  &#xA;  // 构造方法，用于初始化&#xA;  Paint(color c, float a, int i) {&#xA;    // ...&#xA;  }&#xA;  &#xA;  void mix() {&#xA;    // 混色&#xA;  }&#xA;  &#xA;  void pick() {&#xA;    // 取色&#xA;  }&#xA;  &#xA;  // 用于在屏幕上绘制该颜色&#xA;  void display() {&#xA;    // ...&#xA;  }&#xA;}&#xA;&#xA;从文件加载色板&#xA;&#xA;Processing 支持从 sketch 项目文件夹读取文本、图像等文件，所以我打算让它从外部批量读取色板，方便修改。&#xA;&#xA;txt 文件格式如下，每行一个颜色，用空格分隔 rgb：&#xA;&#xA;255 0 0&#xA;0 255 0&#xA;0 0 255&#xA;&#xA;本来还想自定义透明度，但感觉没必要，就默认为 1.0 了。需要的话可以参考读取浮点数的 API。&#xA;&#xA;至于为什么透明度的范围是 0.0-1.0，这是为了方便 lerp 和做各类曲线变换。&#xA;&#xA;那么加载色板的代码写在 setup() 里：&#xA;&#xA;// 墨和水是两个基础颜料，且需要手动调整透明度，故 hardcode 之&#xA;palette[0] = new Paint(color(0,0,0), 1.0, 0);&#xA;palette[1] = new Paint(color(255,255,255), 0.0, 1);&#xA;  &#xA;// 从 palette.txt 加载，存在一个字符串 array 里&#xA;String[] lines = loadStrings(&#34;palette.txt&#34;);&#xA;for (int i = 0; i &lt; min(lines.length, 14); i++) {&#xA;  int[] rgb = int(split(lines[i], &#39; &#39;));&#xA;  // 初始化颜色&#xA;  palette[i+2] = new Paint(color(rgb[0], rgb[1], rgb[2]), 1.0, i+2);&#xA;}&#xA;&#xA;// 初始化剩下的空白格子&#xA;for (int i = lines.length; i &lt; 14; i++){&#xA;  palette[i+2] = new Paint(color(255,255,255), 0.0, i+2);&#xA;}&#xA;&#xA;混色算法分析&#xA;&#xA;真实的颜色（而不是彩色光）混合是这个项目的难点，好在写工具本来就是为了方便实验各种混合算法。有了可交互框架后，先随便写一个 mix 函数看看效果吧。&#xA;&#xA;Additive blending: the naive approach&#xA;&#xA;void mix() {&#xA;    paintcolor = lerpColor(paintcolor, brushcolor, 0.5); // 暂且略过透明度计算&#xA;    paintalpha = (brushalpha + paintalpha) / 2;&#xA;    display();&#xA;  }&#xA;&#xA;第一个 mix 函数仅仅是简单地把两个颜色相加后除以 2。之前在 ps 里随便挑了几个颜色，研究结果如下：&#xA;&#xA;additive1.png&#xA;&#xA;看着效果还不错，然而没有这么简单——事实表明，写测试用例必须考虑极端情况，挑选中庸的例子是没有用的。而在颜色混合领域，一个经典的极端情况就是蓝黄相加。&#xA;&#xA;additive2.png&#xA;&#xA;（为什么蓝黄相加格外经典：因为在大部分人的生活经验里，蓝加黄就应该是绿色！相比之下红和天蓝、绿和粉红相加得到灰色，好像就没那么不可接受。）&#xA;&#xA;同理，其他互补色平均下来也是 50% 灰色。这在色光混合时是合理的（此处应有一张配图，内容是旋转一个半蓝半黄的色盘，可以看到结果的确是灰色，但我找不到那张图了），但并不是我想要的效果。&#xA;&#xA;所以实验后发现，如果只给红黄蓝三色，将永远调不出绿色，非常可悲。&#xA;&#xA;Subtractive blending: CMYK&#xA;&#xA;检索相关资料，发现之前的做法有两个问题：1. 应该用减法（乘法？）混合，2. 色彩模型不对，网友纷纷推荐用 CMYK。&#xA;&#xA;RGB 模型的乘法混合（正片叠底）解决了普通补色的混合，但依然无法解决蓝黄问题，它们相乘后会变成 #000000 也就是黑色。这并不是我们想看到的，所以否决。&#xA;&#xA;CMYK 模型的蓝色 #00f 和黄色 #ff0 分别对应 (1, 1, 0, 0) 和 (0, 0, 1, 0)，CMYK 的加法运算等于 RGB 的乘法运算，故得到 (1, 1, 1, 0)，也是纯黑色。&#xA;&#xA;但在 photoshop 里，不知何故，#00f 和 #ff0 分别对应 (0.93, 0.75, 0, 0) 和 (0.1, 0, 0.83, 0)，而结果颜色是 (0.93, 0.75, 0.83)，正是一个绿色（不是很绿但也足够绿了）。这是为什么？&#xA;&#xA;搜索一番后，这个答案告诉我：&#xA;&#xA;  The whole idea with RGB to CMYK conversion is to create a CMYK image which on print will look as similar as possible to the original RGB image.&#xA;&#xA;  A CMYK color is not an objective color, it&#39;s simply a set of technical instructions for the printing device on which halftone percentages to use.&#xA;&#xA;也就是说，photoshop 的转换仅仅是为了让打印结果看起来尽可能接近原来的 RGB 图像，和我们要做的空间转换无关（我应该在 RGB 模型的文件里计算 CMYK 颜色混合，而这么做并没有解决蓝黄问题）。&#xA;&#xA;那么，我可不可以也像 photoshop 一样，实现我自己的颜色模拟算法？答案似乎是可以，然而，正如引文所说，一个颜色的 CMYK 表达并不唯一，而和不同的打印机参数有关。我没那个时间研究和寻找一套适合我的色彩参数 (color profile)。所以这个方案也否决了。&#xA;&#xA;至于 photoshop 到底用了什么算法，这个问题太复杂，暂且搁置。&#xA;&#xA;Back to additive: RYB&#xA;&#xA;又检索一番，发现有人提出过基于真实颜料的 RYB 色彩模型：&#xA;&#xA;ryb.png&#xA;&#xA;这个模型完美复刻了红黄蓝混合后获得屎色的现实情况，看起来很有前途。网上流传着两种转换算法，前者要用到三线性插值 trilinear interpolation 和一些魔法数字，把颜色从一个立方体映射到另一个立方体，而且没有从 rgb 转换到 ryb 的方法，遂放弃；后者来自这篇只有一页的论文，实现了对称转换。&#xA;&#xA;第二种算法原理和步骤如下（仅以 RGB to RYB 为例）：&#xA;&#xA;先把所有的白色抠出来 $rgb\{RYB} -= min(rgb\{RGB})$&#xA;从红色分量里把黄色抠出来 $r\{RYB} = r\{RGB} - min(r\{RGB}, g\{RGB})$，黄色晾干备用（因为 RGB 的黄色是红绿组成的）&#xA;从绿色分量里把黄色抠出来，剩下的绿色晾干备用 $g\{RGB} - min(r\{RGB}, g\{RGB})$&#xA;把这部分多余的绿色和蓝色搓在一起，形成新的蓝色分量 $b\{RYB} = (b\{RGB} + g\{RGB} - min(r\{RGB}, g\{RGB}) / 2$（因为 RGB 的绿色要拆进 RYB 的蓝和黄）&#xA;把原本的绿色和之前抠出来的黄色搓在一起，形成新的黄色分量 $y\{RYB} = (g\{RGB} + min(r\{RGB}, g\{RGB}) / 2$&#xA;最后，加回去一个黑色 $rgb\{RYB} += min(1 - rgb\{RGB})$&#xA;&#xA;相关的数学证明就不写了，有兴趣的读者可以推导。代码在文章最后。&#xA;&#xA;透明度混合&#xA;&#xA;（🚧 施工中）&#xA;&#xA;Next&#xA;&#xA;接下来还要优化的功能：&#xA;&#xA;记录每一格的颜料量，以此为混色凭据&#xA;支持从图片读取色板，甚至拖拽入色板（see sDrop, a processing library by andreas schlegel.）&#xA;保存当前调色盘&#xA;&#xA;---&#xA;&#xA;全部代码：&#xA;&#xA;import java.util.;&#xA;import java.text.;&#xA;&#xA;color brushcolor = color(0, 0, 0);&#xA;float brushalpha = 0.0;&#xA;Paint[] palette = new Paint [16];&#xA;&#xA;void setup() {&#xA;  size(256, 256);&#xA;  &#xA;  palette[0] = new Paint(color(0,0,0), 1.0, 0);&#xA;  palette[1] = new Paint(color(255,255,255), 0.0, 1);&#xA;  &#xA;  String[] lines = loadStrings(&#34;palette.txt&#34;);&#xA;  for (int i = 0; i &lt; min(lines.length, 14); i++) {&#xA;    int[] rgb = int(split(lines[i], &#39; &#39;));&#xA;    palette[i+2] = new Paint(color(rgb[0], rgb[1], rgb[2]), 1.0, i+2);&#xA;  }&#xA;&#xA;  for (int i = lines.length; i &lt; 14; i++){&#xA;    palette[i+2] = new Paint(color(255,255,255), 0.0, i+2);&#xA;  }&#xA;}&#xA;&#xA;void draw() {&#xA;  background(128);&#xA;  for (int i=0; i &lt; palette.length; i++) {&#xA;    palette[i].display();&#xA;  }&#xA;}&#xA;&#xA;void mousePressed(){&#xA;  int idx = getcolorindex(mouseX, mouseY);&#xA;  if (mouseButton == LEFT) {&#xA;    palette[idx].mix();&#xA;  }&#xA;  else if (mouseButton == RIGHT) {&#xA;    palette[idx].pick();&#xA;  }&#xA;}&#xA;&#xA;int getcolorindex(int x, int y) {&#xA;  int row = y / 64;&#xA;  int col = x / 64;&#xA;  return row  4 + col;&#xA;}&#xA;&#xA;public class Paint {&#xA;  color paintcolor = color(0,0,0);&#xA;  float paintalpha = 0.0;&#xA;  int index = 0;&#xA;  &#xA;  Paint(color c, float a, int i) {&#xA;    paintcolor = c;&#xA;    paintalpha = a;&#xA;    index = i;&#xA;  }&#xA;  &#xA;  void mix() {&#xA;    float weight = constrain(brushalpha / (paintalpha + brushalpha), 0.0, 1.0);&#xA;    paintcolor = lerpColor(paintcolor, brushcolor, weight);&#xA;    paintalpha = (weight + paintalpha) / 2;&#xA;    display();&#xA;  }&#xA;  &#xA;  void pick() {&#xA;    brushcolor = paintcolor;&#xA;    brushalpha = paintalpha;&#xA;  }&#xA;  &#xA;  void display() {&#xA;    noStroke();&#xA;    fill(paintcolor, paintalpha  255);&#xA;    rect(index % 4  64, index / 4  64, 64, 64);&#xA;  }&#xA;}&#xA;&#xA;---&#xA;&#xA;进阶版补丁（RYB 混合版本）：&#xA;&#xA;color mixRYB(color c1, color c2, float w) {&#xA;    color c1RYB = RGB2RYB(c1);&#xA;    color c2RYB = RGB2RYB(c2);&#xA;    color cRYB = lerpColor(c1RYB, c2RYB, w);&#xA;    color c = RYB2RGB(cRYB);&#xA;    return c;&#xA;  }&#xA;&#xA;// algorithm: http://nishitalab.org/user/UEI/publication/Sugita_SIG2015.pdf&#xA;// code: http://www.deathbysoftware.com/colors/index.html&#xA;  &#xA;  color RGB2RYB(color rgb){&#xA;    float r = red(rgb);&#xA;    float g = green(rgb);&#xA;    float b = blue(rgb);&#xA;    float w = min(r,g,b);&#xA;    r -= w;&#xA;    g -= w;&#xA;    b -= w;&#xA;    float maxg = max(r,g,b);&#xA;    float y = min(r,g);&#xA;    r -= y;&#xA;    g -= y;&#xA;    if (b   0 &amp;&amp; g   0) {&#xA;      b /= 2;&#xA;      g /= 2;&#xA;    }&#xA;    y += g;&#xA;    b += g;&#xA;    float maxy = max(r,y,b);&#xA;    if (maxy   0) {&#xA;      float n = maxg / maxy;&#xA;      r = n;&#xA;      y = n;&#xA;      b = n;&#xA;    }&#xA;    color ryb = color(r+w, y+w, b+w);&#xA;    return ryb;&#xA;  }&#xA;  &#xA;  color RYB2RGB(color ryb){&#xA;    float r = red(ryb);&#xA;    float y = green(ryb);&#xA;    float b = blue(ryb);&#xA;    float w = min(r,y,b);&#xA;    r -= w;&#xA;    y -= w;&#xA;    b -= w;&#xA;    float maxy = max(r,y,b);&#xA;    float g = min(y,b);&#xA;    y -= g;&#xA;    b -= g;&#xA;    if (b   0 &amp;&amp; g   0) {&#xA;      b = 2;&#xA;      g = 2;&#xA;    }&#xA;    r += y;&#xA;    g += y;&#xA;    float maxg = max(r,g,b);&#xA;    if (maxg   0) {&#xA;      float n = maxy / maxg;&#xA;      r = n;&#xA;      g = n;&#xA;      b = n;&#xA;    }&#xA;    color rgb = color(r+w, g+w, b+w);&#xA;    return rgb;&#xA;  }&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://s1.ax1x.com/2020/09/16/wgBpp6.png" alt="rgb.png"><img src="https://s1.ax1x.com/2020/09/16/wgf71H.png" alt="ryb.png"></p>

<p>这次做的是一个调色盘 demo 项目，练手顺便实验颜料混合算法（事实证明符合物理直觉的颜料混合非常复杂）。</p>

<p>涉及到的 Processing 知识：</p>
<ul><li>类和对象</li>
<li>如何显示对象</li>
<li>从文件读取</li></ul>

<h3 id="需求和交互设计-打草稿">需求和交互设计（打草稿！）</h3>

<p>首先考虑这个调色盘有哪些功能。最初的设想很简单：</p>
<ul><li>正方形，划分成 16 格，每格里有颜色或空白</li>
<li>左键混色（落笔），右键吸色（蘸取）</li>
<li>颜料转移时会被稀释，便于控制添加颜色的量（后来发现很难做，要重构）</li></ul>

<h3 id="程序框架">程序框架</h3>

<p>决定了功能需求后，整个程序的框架也就呼之欲出了：</p>

<pre><code>import java.util.*;
import java.text.*;

// 记录当前颜色和透明度的全局变量
color brush_color = color(0, 0, 0);
float brush_alpha = 0.0;
// TODO  我们还需要定义一个 Paint 类来储存和显示颜料，过会儿再说
// Paint[] palette = new Paint [16];

// 这是启动时执行的函数，用于初始化一些数据
void setup() {
  // 分辨率 256*256，每格宽高 64
  size(256, 256);
  // TODO 在这里初始化和显示所有颜料
}

// 每帧更新画布的函数
void draw() {
  // TODO 每帧画背景和颜料
}

// 每次鼠标点击后都会触发的函数
void mousePressed(){
  if (mouseButton == LEFT) {
    // 点击左键混色
  }
  else if (mouseButton == RIGHT) {
    // 点击右键取色
  }
}
</code></pre>

<p>这里还需要解决一个问题：如何确定鼠标点击时选择了哪格颜色？由于颜色平铺在 4x4 的方格里，可以写一个 <code>get_color_index(int mouseX, int mouseY)</code> 函数，传入点击的屏幕坐标，返回颜色序号（0-15）：</p>

<pre><code>int get_color_index(int x, int y) {
  int row = y / 64;
  int col = x / 64;
  return row * 4 + col;
}
</code></pre>

<h3 id="paint-类">Paint 类</h3>

<p>接下来要给颜料写一个类，设计如下。</p>

<p>属性（要给每格颜料存储的数据）：</p>
<ul><li>当前颜色</li>
<li>当前透明度</li>
<li>所在的格子编号（用于绘制该对象）</li></ul>

<p>方法（每格颜料可以进行的操作）：</p>
<ul><li>混色（改变当前颜色）</li>
<li>吸色</li></ul>

<pre><code>public class Paint {
  // 颜料对象的属性
  color paint_color = color(0,0,0);
  float paint_alpha = 0.0;
  int index = 0;
  
  // 构造方法，用于初始化
  Paint(color c, float a, int i) {
    // ...
  }
  
  void mix() {
    // 混色
  }
  
  void pick() {
    // 取色
  }
  
  // 用于在屏幕上绘制该颜色
  void display() {
    // ...
  }
}
</code></pre>

<h3 id="从文件加载色板">从文件加载色板</h3>

<p>Processing 支持从 sketch 项目文件夹读取文本、图像等文件，所以我打算让它从外部批量读取色板，方便修改。</p>

<p>txt 文件格式如下，每行一个颜色，用空格分隔 rgb：</p>

<pre><code>255 0 0
0 255 0
0 0 255
</code></pre>

<p>本来还想自定义透明度，但感觉没必要，就默认为 1.0 了。需要的话可以参考<a href="https://processing.org/reference/floatconvert_.html" rel="nofollow">读取浮点数的 API</a>。</p>

<p>至于为什么透明度的范围是 0.0-1.0，这是为了方便 lerp 和做各类曲线变换。</p>

<p>那么加载色板的代码写在 setup() 里：</p>

<pre><code>// 墨和水是两个基础颜料，且需要手动调整透明度，故 hardcode 之
palette[0] = new Paint(color(0,0,0), 1.0, 0);
palette[1] = new Paint(color(255,255,255), 0.0, 1);
  
// 从 palette.txt 加载，存在一个字符串 array 里
String[] lines = loadStrings(&#34;palette.txt&#34;);
for (int i = 0; i &lt; min(lines.length, 14); i++) {
  int[] rgb = int(split(lines[i], &#39; &#39;));
  // 初始化颜色
  palette[i+2] = new Paint(color(rgb[0], rgb[1], rgb[2]), 1.0, i+2);
}

// 初始化剩下的空白格子
for (int i = lines.length; i &lt; 14; i++){
  palette[i+2] = new Paint(color(255,255,255), 0.0, i+2);
}
</code></pre>

<h3 id="混色算法分析">混色算法分析</h3>

<p>真实的颜色（而不是彩色光）混合是这个项目的难点，好在写工具本来就是为了方便实验各种混合算法。有了可交互框架后，先随便写一个 mix 函数看看效果吧。</p>

<h4 id="additive-blending-the-naive-approach">Additive blending: the naive approach</h4>

<pre><code>void mix() {
    paint_color = lerpColor(paint_color, brush_color, 0.5); // 暂且略过透明度计算
    paint_alpha = (brush_alpha + paint_alpha) / 2;
    display();
  }
</code></pre>

<p>第一个 mix 函数仅仅是简单地把两个颜色相加后除以 2。之前在 ps 里随便挑了几个颜色，研究结果如下：</p>

<p><img src="https://s1.ax1x.com/2020/09/18/whTyLT.png" alt="additive1.png"></p>

<p>看着效果还不错，然而没有这么简单——事实表明，写测试用例必须考虑极端情况，挑选中庸的例子是没有用的。而在颜色混合领域，一个经典的极端情况就是蓝黄相加。</p>

<p><img src="https://s1.ax1x.com/2020/09/18/whHVED.png" alt="additive2.png"></p>

<p>（为什么蓝黄相加格外经典：因为在大部分人的生活经验里，蓝加黄就应该是绿色！相比之下红和天蓝、绿和粉红相加得到灰色，好像就没那么不可接受。）</p>

<p>同理，其他互补色平均下来也是 50% 灰色。这在色光混合时是合理的（此处应有一张配图，内容是旋转一个半蓝半黄的色盘，可以看到结果的确是灰色，但我找不到那张图了），但并不是我想要的效果。</p>

<p>所以实验后发现，如果只给红黄蓝三色，将永远调不出绿色，非常可悲。</p>

<h4 id="subtractive-blending-cmyk">Subtractive blending: CMYK</h4>

<p>检索相关资料，发现之前的做法有两个问题：1. 应该用减法（乘法？）混合，2. 色彩模型不对，网友纷纷推荐用 CMYK。</p>

<p>RGB 模型的乘法混合（正片叠底）解决了普通补色的混合，但依然无法解决蓝黄问题，它们相乘后会变成 #000000 也就是黑色。这并不是我们想看到的，所以否决。</p>

<p>CMYK 模型的蓝色 #00f 和黄色 #ff0 分别对应 (1, 1, 0, 0) 和 (0, 0, 1, 0)，CMYK 的加法运算等于 RGB 的乘法运算，故得到 (1, 1, 1, 0)，也是纯黑色。</p>

<p>但在 photoshop 里，不知何故，#00f 和 #ff0 分别对应 (0.93, 0.75, 0, 0) 和 (0.1, 0, 0.83, 0)，而结果颜色是 (0.93, 0.75, 0.83)，正是一个绿色（不是很绿但也足够绿了）。这是为什么？</p>

<p>搜索一番后，<a href="https://graphicdesign.stackexchange.com/questions/125441/converting-an-rgb-image-to-cmyk-with-a-specific-black-photoshop" rel="nofollow">这个答案</a>告诉我：</p>

<blockquote><p>The whole idea with RGB to CMYK conversion is to create a CMYK image which on print will look as similar as possible to the original RGB image.</p>

<p>A CMYK color is not an objective color, it&#39;s simply a set of technical instructions for the printing device on which halftone percentages to use.</p></blockquote>

<p>也就是说，photoshop 的转换仅仅是为了让打印结果看起来尽可能接近原来的 RGB 图像，和我们要做的空间转换无关（我应该在 RGB 模型的文件里计算 CMYK 颜色混合，而这么做并没有解决蓝黄问题）。</p>

<p>那么，我可不可以也像 photoshop 一样，实现我自己的颜色模拟算法？答案似乎是可以，然而，正如引文所说，<strong>一个颜色的 CMYK 表达并不唯一</strong>，而和不同的打印机参数有关。我没那个时间研究和寻找一套适合我的色彩参数 (color profile)。所以这个方案也否决了。</p>

<p>至于 photoshop 到底用了什么算法，这个问题太复杂，暂且搁置。</p>

<h3 id="back-to-additive-ryb">Back to additive: RYB</h3>

<p>又检索一番，发现有人提出过基于真实颜料的 RYB 色彩模型：</p>

<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Sintesis_ryb_plano.svg/220px-Sintesis_ryb_plano.svg.png" alt="ryb.png"></p>

<p>这个模型完美复刻了红黄蓝混合后获得屎色的现实情况，看起来很有前途。网上流传着两种转换算法，前者要用到三线性插值 trilinear interpolation 和一些魔法数字，把颜色从一个立方体映射到另一个立方体，而且没有从 rgb 转换到 ryb 的方法，遂放弃；后者来自<a href="http://nishitalab.org/user/UEI/publication/Sugita_SIG2015.pdf" rel="nofollow">这篇</a>只有一页的论文，实现了对称转换。</p>

<p>第二种算法原理和步骤如下（仅以 RGB to RYB 为例）：</p>
<ol><li>先把所有的白色抠出来 $rgb_{RYB} –= min(rgb_{RGB})$</li>
<li>从红色分量里把黄色抠出来 $r_{RYB} = r_{RGB} – min(r_{RGB}, g_{RGB})$，黄色晾干备用（因为 RGB 的黄色是红绿组成的）</li>
<li>从绿色分量里把黄色抠出来，剩下的绿色晾干备用 $g_{RGB} – min(r_{RGB}, g_{RGB})$</li>
<li>把这部分多余的绿色和蓝色搓在一起，形成新的蓝色分量 $b_{RYB} = (b_{RGB} + g_{RGB} – min(r_{RGB}, g_{RGB}) / 2$（因为 RGB 的绿色要拆进 RYB 的蓝和黄）</li>
<li>把原本的绿色和之前抠出来的黄色搓在一起，形成新的黄色分量 $y_{RYB} = (g_{RGB} + min(r_{RGB}, g_{RGB}) / 2$</li>
<li>最后，加回去一个黑色 $rgb_{RYB} += min(1 – rgb_{RGB})$</li></ol>

<p>相关的数学证明就不写了，有兴趣的读者可以推导。代码在文章最后。</p>

<h3 id="透明度混合">透明度混合</h3>

<p>（🚧 施工中）</p>

<h3 id="next">Next</h3>

<p>接下来还要优化的功能：</p>
<ul><li>记录每一格的颜料量，以此为混色凭据</li>
<li>支持从图片读取色板，甚至拖拽入色板（see <a href="http://www.sojamo.de/libraries/drop/" rel="nofollow">sDrop, a processing library by andreas schlegel.</a>）</li>
<li>保存当前调色盘</li></ul>

<hr>

<p>全部代码：</p>

<pre><code>import java.util.*;
import java.text.*;

color brush_color = color(0, 0, 0);
float brush_alpha = 0.0;
Paint[] palette = new Paint [16];

void setup() {
  size(256, 256);
  
  palette[0] = new Paint(color(0,0,0), 1.0, 0);
  palette[1] = new Paint(color(255,255,255), 0.0, 1);
  
  String[] lines = loadStrings(&#34;palette.txt&#34;);
  for (int i = 0; i &lt; min(lines.length, 14); i++) {
    int[] rgb = int(split(lines[i], &#39; &#39;));
    palette[i+2] = new Paint(color(rgb[0], rgb[1], rgb[2]), 1.0, i+2);
  }

  for (int i = lines.length; i &lt; 14; i++){
    palette[i+2] = new Paint(color(255,255,255), 0.0, i+2);
  }
}

void draw() {
  background(128);
  for (int i=0; i &lt; palette.length; i++) {
    palette[i].display();
  }
}

void mousePressed(){
  int idx = get_color_index(mouseX, mouseY);
  if (mouseButton == LEFT) {
    palette[idx].mix();
  }
  else if (mouseButton == RIGHT) {
    palette[idx].pick();
  }
}

int get_color_index(int x, int y) {
  int row = y / 64;
  int col = x / 64;
  return row * 4 + col;
}

public class Paint {
  color paint_color = color(0,0,0);
  float paint_alpha = 0.0;
  int index = 0;
  
  Paint(color c, float a, int i) {
    paint_color = c;
    paint_alpha = a;
    index = i;
  }
  
  void mix() {
    float weight = constrain(brush_alpha / (paint_alpha + brush_alpha), 0.0, 1.0);
    paint_color = lerpColor(paint_color, brush_color, weight);
    paint_alpha = (weight + paint_alpha) / 2;
    display();
  }
  
  void pick() {
    brush_color = paint_color;
    brush_alpha = paint_alpha;
  }
  
  void display() {
    noStroke();
    fill(paint_color, paint_alpha * 255);
    rect(index % 4 * 64, index / 4 * 64, 64, 64);
  }
}
</code></pre>

<hr>

<p>进阶版补丁（RYB 混合版本）：</p>

<pre><code>color mixRYB(color c1, color c2, float w) {
    color c1RYB = RGB2RYB(c1);
    color c2RYB = RGB2RYB(c2);
    color cRYB = lerpColor(c1RYB, c2RYB, w);
    color c = RYB2RGB(cRYB);
    return c;
  }

// algorithm: http://nishitalab.org/user/UEI/publication/Sugita_SIG2015.pdf
// code: http://www.deathbysoftware.com/colors/index.html
  
  color RGB2RYB(color rgb){
    float r = red(rgb);
    float g = green(rgb);
    float b = blue(rgb);
    float w = min(r,g,b);
    r -= w;
    g -= w;
    b -= w;
    float maxg = max(r,g,b);
    float y = min(r,g);
    r -= y;
    g -= y;
    if (b &gt; 0 &amp;&amp; g &gt; 0) {
      b /= 2;
      g /= 2;
    }
    y += g;
    b += g;
    float maxy = max(r,y,b);
    if (maxy &gt; 0) {
      float n = maxg / maxy;
      r *= n;
      y *= n;
      b *= n;
    }
    color ryb = color(r+w, y+w, b+w);
    return ryb;
  }
  
  color RYB2RGB(color ryb){
    float r = red(ryb);
    float y = green(ryb);
    float b = blue(ryb);
    float w = min(r,y,b);
    r -= w;
    y -= w;
    b -= w;
    float maxy = max(r,y,b);
    float g = min(y,b);
    y -= g;
    b -= g;
    if (b &gt; 0 &amp;&amp; g &gt; 0) {
      b *= 2;
      g *= 2;
    }
    r += y;
    g += y;
    float maxg = max(r,g,b);
    if (maxg &gt; 0) {
      float n = maxy / maxg;
      r *= n;
      g *= n;
      b *= n;
    }
    color rgb = color(r+w, g+w, b+w);
    return rgb;
  }
</code></pre>
]]></content:encoded>
      <guid>https://matterofti.me/idelem/processing-bi-ji-ben-2</guid>
      <pubDate>Wed, 16 Sep 2020 08:19:14 +0000</pubDate>
    </item>
    <item>
      <title>Processing 笔记本 (1)</title>
      <link>https://matterofti.me/idelem/processing-bi-ji-ben</link>
      <description>&lt;![CDATA[Getting Started&#xA;Getting Started \ Processing.org&#xA;&#xA;import java.util.;&#xA;import java.text.;&#xA;&#xA;void setup() {&#xA;  size(480, 120);&#xA;}&#xA;&#xA;void draw() {&#xA;  if (mousePressed) {&#xA;    fill(0);&#xA;  } else {&#xA;    fill(255);&#xA;  }&#xA;  ellipse(mouseX, mouseY, 80, 80);&#xA;}&#xA;&#xA;void keyPressed() {&#xA;  if (key == &#39;s&#39;) {&#xA;    DateFormat formatter = new SimpleDateFormat(&#34;yyMMddHHmmss&#34;);&#xA;    Date d = new Date();&#xA;    String clock = formatter.format(d);&#xA;    save(&#34;ellipse&#34; + clock + &#34;.png&#34;);&#xA;  }&#xA;}&#xA;&#xA;工程没保存的时候，存在一个临时目录里，Ctrl/Cmd+K 打开临时目录。保存工程就是把这个目录移到自己喜欢的位置。&#xA;&#xA;processing 的工程叫 sketch，下文都会用这个词代指。sketch 由多个代码片段组成。一个 sketch 会有一个专属目录，save() 函数保存的图片就放在那里面。上面这个 sketch 的保存功能还可以这么实现：&#xA;&#xA;saveFrame(&#34;ellipse####.png&#34;);&#xA;&#xA;#### 是填充数字的模板，试验下来发现填充的是帧数。&#xA;&#xA;---&#xA;&#xA;Overview&#xA;&#xA;Processing Overview \ Processing.org&#xA;&#xA;和大部分类似工具一样，processing 提供了一堆现成的 API 函数给你填充，还有一些必要的系统变量：&#xA;&#xA;setup() ：初始化方法&#xA;  size(w, h)：设置窗口分辨率，必须写在 setup() 的第一行。可以用不同的 renderer 来调用 size，例如 size(400, 400, P2D) 是 2D 图像（默认的），size(400, 400, P3D) 是 3D，size(400, 400, PDF, &#34;output.pdf&#34;) 是写入 PDF 文件。&#xA;  width, height：当前窗口宽高&#xA;draw()：每帧刷新&#xA;mousePressed()：鼠标按下时事件&#xA;  mousePressed：判断鼠标是否按下的布尔值&#xA;  mouseX, mouseY：当前鼠标的坐标&#xA;keyPressed()：按键事件&#xA;  key：被按下的 ASCII 字符按键&#xA;  keyCode：被按下的非 ASCII 按键，详见 KeyEvent (Java Platform SE 8 )。&#xA;&#xA;引用外部库：Libraries \ Processing.org&#xA;&#xA;导入数据：sketch 目录下有 data 文件夹，如果没有的话，import file 或把文件拖入窗口，会自动创建。这是为了统一解决各个平台上的文件引用问题，processing 在打包时会自动根据情况调用合适的文件 API。&#xA;&#xA;String[] lines = loadStrings(&#34;something.txt&#34;);&#xA;PImage image = loadImage(&#34;picture.jpg&#34;);&#xA;`]]&gt;</description>
      <content:encoded><![CDATA[<h3 id="getting-started">Getting Started</h3>

<p><a href="https://processing.org/tutorials/gettingstarted/" rel="nofollow">Getting Started \ Processing.org</a></p>

<p><img src="https://i.loli.net/2020/07/09/OSWztCv8HINEMeP.png" alt=""></p>

<pre><code>import java.util.*;
import java.text.*;

void setup() {
  size(480, 120);
}

void draw() {
  if (mousePressed) {
    fill(0);
  } else {
    fill(255);
  }
  ellipse(mouseX, mouseY, 80, 80);
}

void keyPressed() {
  if (key == &#39;s&#39;) {
    DateFormat formatter = new SimpleDateFormat(&#34;yyMMddHHmmss&#34;);
    Date d = new Date();
    String clock = formatter.format(d);
    save(&#34;ellipse_&#34; + clock + &#34;.png&#34;);
  }
}
</code></pre>

<p>工程没保存的时候，存在一个临时目录里，<code>Ctrl/Cmd+K</code> 打开临时目录。保存工程就是把这个目录移到自己喜欢的位置。</p>

<p>processing 的工程叫 sketch，下文都会用这个词代指。sketch 由多个代码片段组成。一个 sketch 会有一个专属目录，<code>save()</code> 函数保存的图片就放在那里面。上面这个 sketch 的保存功能还可以这么实现：</p>

<pre><code>saveFrame(&#34;ellipse_####.png&#34;);
</code></pre>

<p><code>####</code> 是填充数字的模板，试验下来发现填充的是帧数。</p>

<hr>

<h3 id="overview">Overview</h3>

<p><a href="https://processing.org/tutorials/overview/" rel="nofollow">Processing Overview \ Processing.org</a></p>

<p>和大部分类似工具一样，processing 提供了一堆现成的 API 函数给你填充，还有一些必要的系统变量：</p>
<ul><li><code>setup()</code> ：初始化方法
<ul><li><code>size(w, h)</code>：设置窗口分辨率，必须写在 <code>setup()</code> 的第一行。可以用不同的 renderer 来调用 <code>size</code>，例如 <code>size(400, 400, P2D)</code> 是 2D 图像（默认的），<code>size(400, 400, P3D)</code> 是 3D，<code>size(400, 400, PDF, &#34;output.pdf&#34;)</code> 是写入 PDF 文件。</li>
<li><code>width, height</code>：当前窗口宽高</li></ul></li>
<li><code>draw()</code>：每帧刷新</li>
<li><code>mousePressed()</code>：鼠标按下时事件
<ul><li><code>mousePressed</code>：判断鼠标是否按下的布尔值</li>
<li><code>mouseX, mouseY</code>：当前鼠标的坐标</li></ul></li>
<li><code>keyPressed()</code>：按键事件
<ul><li><code>key</code>：被按下的 ASCII 字符按键</li>
<li><code>keyCode</code>：被按下的非 ASCII 按键，详见 <a href="https://docs.oracle.com/javase/8/docs/api/java/awt/event/KeyEvent.html" rel="nofollow">KeyEvent (Java Platform SE 8 )</a>。</li></ul></li></ul>

<p><strong>引用外部库</strong>：<a href="https://processing.org/reference/libraries/" rel="nofollow">Libraries \ Processing.org</a></p>

<p><strong>导入数据</strong>：sketch 目录下有 data 文件夹，如果没有的话，import file 或把文件拖入窗口，会自动创建。这是为了统一解决各个平台上的文件引用问题，processing 在打包时会自动根据情况调用合适的文件 API。</p>

<pre><code>String[] lines = loadStrings(&#34;something.txt&#34;);
PImage image = loadImage(&#34;picture.jpg&#34;);
</code></pre>
]]></content:encoded>
      <guid>https://matterofti.me/idelem/processing-bi-ji-ben</guid>
      <pubDate>Thu, 09 Jul 2020 09:53:38 +0000</pubDate>
    </item>
  </channel>
</rss>