MetaFun 小傳

MetaFun 是 ConTeXt 的一部分,主要用於 MetaPost 的繪圖功能與 ConTeXt 的排版功能的銜接。編程

ConTeXt 專事文字排版,功能匹於 LaTeX,但更易於使用,兩者皆爲 TeX 宏包,即兩者皆基於 TeX 提供的宏編程功能,對 TeX 語言予以封裝,創建更利於文字排版工做的高級語言。TeX 是一種計算機排版語言,供編排科技手稿以及著做出版印刷之用 [1] 。MetaPost 是用於繪製矢量繪圖的計算機語言。segmentfault

目前最新的 ConTeXt 版本爲 MkIV,安裝 ConTeXt Standalone 可得 [2] 。ConTeXt MkIV 的基本用法可參考以前我寫的幾篇文章 [3–7] ,或閱讀 ConTeXt 官方文檔 [8, 9] 。數組

MetaFun 以 MetaPost 生成的矢量圖形做爲頁面特定區域的背景,然後基於 ConTeXt 的排版功能在該背景上實現編排文字。dom

MetaPost

MetaPost 是一種編程語言 注 1 ,其編譯器爲 mpost。用該語言編寫的程序,其輸出結果爲 PostScript 格式的矢量圖形文件 注 2 。MPpage 環境中的 MetaPost 語句即 MetaPost 程序。在使用 context 命令生成單頁面圖形文件的過程當中,context 命令會調用 mpost,由後者處理 MetaPost 程序,生成 PostScript 圖形文件。繼而 context 命令調用 TeX 引擎 注 3 會將 mpost 生成的圖形文件嵌入至單頁面文檔中,並將圖形的寬高做爲頁面寬高。編程語言

注 1:確切地說,MetaPost 是一種宏編程語言。

注 2:PostScript 文件可轉化爲 PDF、SVG 等格式的矢量圖形文件。ide

注 3:TeX 引擎即 TeX 文檔的編譯器。ConTeXt 文檔本質上也是 TeX 文檔,所以要經過 TeX 引擎對其其進行編譯,輸出排版結果。ConTeXt MkIV 的 TeX 引擎爲 LuaTeX,其輸出的排版結果爲 PDF 格式文檔。工具

畫筆

畫筆即 MetaPost 的內置變量 pen。MetaPost 提供了兩種畫筆類型,pencirclepensquare,前者爲 MetaPost 默認,「筆尖」爲圓形,後者「筆尖」爲方形。MetaPost 容許用戶自行定義畫筆類型。佈局

畫筆主要用於控制所繪線條的粗細。線條默認的寬度爲 PostScript 所規定的大點(Big Point)的直徑尺寸,即 1 bp。MetaPost 將 1 bp 做爲基準長度單位,其餘單位皆爲該單位的倍數:post

bp := 1
mm = 2.83464
cm = 28.34645
pc = 11.95517
cc = 12.79213
in := 72
pt = 0.99626
dd = 1.06601

pickup 命令可用於設定畫筆,從而影響隨後的繪圖語句所繪製線條的粗細,這一影響直至 pickup 命令的再次出現爲止。例如,測試

pickup pencircle scaled 1mm;
一系列繪圖語句;
pickup pencircle scaled 2mm;
一系列繪圖語句;

定義了兩個畫筆,筆尖粗度分別爲 1mm 和 2mm,分別會影響位於其後的繪圖過程。scaled 用於數值大小的縮放變換;其餘變換還有 shiftedrotated 以及 slant,分別爲平移、旋轉以及錯切變換。在畫筆的設定中,scaled 1mm 意味着將線條粗細程序由 MetaPost 默認的 1 bp 在水平和豎直方向上同等放大爲 1 mm 注 4 。可使用 xscaledyscaled 對畫筆的水平或豎直方向的粗細進行調整,對於 pencircle 類型的畫筆而言,此舉意味着將筆尖由默認的圓形轉化爲橢圓,而對於 pensquare,則意味着將筆尖由正方形轉化爲矩形。

注 4:在 MetaPost 程序中,數字與單位之間不能出現空格。事實上,在 MetaPost 中,諸如 1mm2cm 此類的長度描述本質上是 mmcm 等變量的倍數,即 1 * mm2 * cm

pickup 的影響範圍內,繪圖語句能夠經過 withpen 命令局部調整線條的粗細,例如

withpen pencircle scaled 1mm

顏色

MetaPost 以含有三個份量的向量表示顏色。向量的三個份量分別表示紅色、綠色和藍色,取值範圍爲 [0, 1],例如 (0.4, 0.5, 0.6)

可將顏色保存到 color 類型的變量中,以備繪圖中重複使用。例如

color darkred;
darkred := (0.625, 0, 0);

因爲 MetaPost 內部已經定義了用於表示紅色的變量 red,所以 darkred 變量的定義也可寫爲

color darkred;
darkred := 0.625 * red;

相似於 1 * cm 能夠寫爲 1cm,倍數也能夠直接做用於顏色:

darkred := 0.625red;

在繪圖語句中能夠經過 withcolor 命令設定所繪線條或區域填充的顏色,例如

withcolor 0.625red

因爲顏色的倍數不可能大於 1,所以整數部分一定爲 0,在 MetaPost 語句中能夠省略,例如

darkred := .625red;

若繪圖語句未經過 withcolor 命令設定顏色,則默認顏色爲黑色。

單頁圖

在排版空間中,可安置 MetaPost 圖形之處大體有插圖、單頁圖、頁面元素背景以及頁面背景等類別。若以先習得 MetaPost 的基本用法爲目的,則單頁圖最爲合用,而且生成的圖形易於轉化爲位圖以做他用。

所謂 MetaPost 單頁圖,本質上是 ConTeXt 輸出的排版結果——PDF 文檔,只是文檔頁面的大小剛好容得下圖形。ConTeXt 爲 MetaPost 單頁面提供了 MPpage 環境:

\startMPpage
MetaPost 繪圖語句;
\stopMPpage

例如,假設存在 ConTeXt 文檔 foo.tex,其內容爲

\startMPpage
path p;
u := 10cm; v := 3cm;
p := fullsquare xyscaled (u, v) randomized 0.07u;
drawpath p;
drawpoints p;
\stopMPpage

經過 context 命令即可基於 foo.tex 生成 foo.pdf,即

$ context foo

結果獲得的 foo.pdf 爲單頁文檔,其頁面只包含着一個邊線被隨機擾動的矩形:

線條

線條即畫筆所走的路徑。最簡單的路徑是點。MetaPost 用序對錶示點,例如

pair a;
a := (2cm, 3.5cm)

表示在直接座標系中,橫座標 x2cm 而縱座標 y3.5cm 之處有一個點 adraw 命令用於路徑的繪製,經過它可將點 a 繪製出來,即

draw a;

從一個點到另外一個點,可構成一條線段。例如

pair a, b;
a := (2cm, 3.5cm); b := (5cm, 5cm);

path p; 
p := a -- b;

可構造從點 ab 的線段 a -- b,並將其保存到路徑變量 p 中。使用

draw p withcolor .625green;

便可繪製這條線段。在該條語句中,線條顏色被設爲暗綠色 0.625green

因爲 MetaPost 容許在 draw 語句中直接給出點的座標的形式構造路徑,所以上述 MetaPost 程序可縮減爲一行語句:

draw (2cm, 3.5cm) -- (5cm, 5cm) withcolor .625green;

可是,若要繪製複雜的圖形,藉助變量,會使得 MetaPost 程序更易於編寫和理解。例如

pair a, b; path p;
a := (2cm, 3.5cm); b := (5cm, 5cm);
p := a -- b;

pickup pencircle scaled 2pt;
draw p withcolor .625green;

pickup pencircle scaled 4pt;
color darkred; darkred := .625red;
draw a withcolor darkred;
draw b withcolor darkred;

不只繪製了線段,並且將線段的端點也繪製了出來。

利用線段可繪製任意的多邊形。例如,繪製一個直角三角形,

pair a, b, c; path p;
a := (0, 0); b := (4cm, 0); c := (4cm, 3cm);
p := a -- b -- c -- a;

% 注意:凡以百分號領起的文本爲 MetaPost 代碼註釋。

pickup pencircle scaled 5; % 將畫筆設爲 5 bp
draw p withcolor .8white;

pickup pencircle scaled 4;
draw a; draw b; draw c;

爲了便於圖形的演示,MetaFun 提供了 drawpathdrawpoints 宏,前者用於繪製路徑,後者用於繪製路徑的節點。經過這兩個宏,上例可簡化爲

pair a, b, c; path p;
a := (0, 0); b := (4cm, 0); c := (4cm, 3cm);
p := a -- b -- c -- a;
drawpath p; drawpoints p;

顯然,上述路徑 p 是一條閉合路徑,但 MetaPost 對此並不知情,須要經過 cycle 命令告訴它,即

p := a -- b -- c -- cycle;

不然,雖然咱們認爲 p 是閉合路徑,但 MetaPost 並不苟同,以至在使用 fill 命令對該路徑包圍的區域填充顏色時,會致使 MetaPost 報錯並罷工。

fill 命令可對閉合路徑所包圍的區域着色。例如

pair a, b, c; path p;
a := (0, 0); b := (4cm, 0); c := (4cm, 3cm);
p := a -- b -- c -- cycle;
drawpath p; drawpoints p;
fill p withcolor .8blue;

上例中的路徑 p 皆爲直線插值。MetaPost 支持以曲線插值的方式構造路徑。假若將直線插值符的 -- 替換爲曲線插值符 .. 即可產生一條插值於點 abc 的曲線路徑,

p := a .. b .. c .. cycle;

直線插值符與曲線插值符可並用,例如

p := a .. b .. c -- cycle;

controls 命令可將路徑中的某些結點轉化爲控制點,從而可構造 Bézier 曲線。例如

p := a .. controls b ..c; draw p;

構造的是一條二次 Bézier 曲線路徑,此時點 b 成爲控制點,曲線只插值於點 ab。MetaFun 提供了 drawcontrollines 以及 drawcontrolpoints 宏,分別用於繪製 Bézier 曲線的控制形及控制點,例如,

p := a .. controls b ..c;
drawpath p; drawpoints p;
drawcontrollines p; drawcontrolpoints p;

三次 Bézier 曲線須要在路徑中設定 2 個控制點,例如

pair a, b, c, d; path p;
a := (0, 0); b := (4cm, 0); c := (4cm, 3cm); d := (0, 3cm);

p := a .. controls b and c .. d;
drawpath p; drawpoints p;
drawcontrollines p; drawcontrolpoints p;

不管是插值曲線仍是 Bézier 曲線,MetaPost 最高支持三次曲線。不過,對於形狀較爲複雜的路徑,MetaPost 支持以多段插值直線、曲線以及 Bézier 曲線拼接 注 5 的方式構造路徑。

注 5:對於一組曲線,MetaPost 會以切向連續而且近似曲率連續的方式予以光滑拼接。

變換

爲了便於對所繪圖形做縮放、旋轉、平移、錯切以及隨機擾動等處理,MetaPost 提供了一種數據類型——變換,即含有六個份量的向量:

$$ T = (t_x, t_y, t_{xx}, t_{xy}, t_{yx}, t_{yy}) $$

對於任意一點 \(p=(p_x, p_y)\),MetaPost 的 transform 命令可將 \(T\) 做用於 \(p\),即 p transform T,可將 \(p\) 變換爲

$$ q = (t_{xx}p_x + t_{xy}p_y + t_x, t_{yx}p_x + t_{yy}p_y + t_y) $$

實質上,若以仿射座標的形式看待 \(p\),並採用列向量 \(\left[\begin{matrix}p_x \\ p_y \\ 1\end{matrix}\right]\) 表示其座標,則 \(T\) 的 6 個份量可造成座標變換矩陣

$$ M = \left[\begin{matrix} t_{xx} & t_{xy} & t_x \\\\ t_{yx} & t_{yy} & t_y \\\\ 0 & 0 & 1\end{matrix}\right] $$

此時,p transform T 語句所描述的座標變換,即可表示爲 \(q = Mp\)。座標變換矩陣 \(M\) 所描述的是平移、旋轉、縮放以及錯切等變換的組合,亦即這些特定的變換皆爲 \(M\) 的特例。所以,在應用 transform 的時候,一般並不直接提供六元組形式的變換,而是以 scaledshifted 以及 rotated 等變換的組合構造一個變換。

假設在邊長爲 8cm 的正方形區域

numeric sidelength, u; 
sidelength := 8cm; u := 0.5sidelength;
drawpath fullsquare scaled sidelength dashed (evenly scaled 1mm);

有四個點

pair a, b, c, d;
a := (-0.5, -0.5) * u;
b := (-0.5, 0.5) * u;
c := (0.5, 0.5) * u;
d := (0.5, -0.5) * u;

它們構成路徑 p

path p; p := a -- b -- c -- d;
drawpath p; drawpoints p;

如今將 p 縮小爲原來的 0.5 倍,可爲此構造變換 T

transform T;
T := identity scaled 0.5;

identity 是 MetaPost 內置的恆等變換,其值爲向量 (0, 0, 1, 0, 0, 1),將其寫爲齊次座標變換矩陣,可得

$$ \left[\begin{matrix} 1 & 0 & 0 \\\\ 0 & 1 & 0 \\\\ 0 & 0 & 1\end{matrix}\right] $$

所以,實際上 identity 表示的是單位矩陣。所以 identity scaled 0.5 所構造的變換,本質上是以一個單位矩陣乘以由 scaled 0.5 構造的縮放變換矩陣

$$ \left[\begin{matrix} 0.5 & 0 & 0 \\\\ 0 & 0.5 & 0 \\\\ 0 & 0 & 1\end{matrix}\right] $$

在這裏,identity 的惟一做用是餵給 scaled 命令,令其得以工做。由於 MetaPost 全部的特定座標變換命令在工做時要求它的前面必須存在一個表達式,這個表達式能夠是一個變換,也能夠是一條路徑。所以 identity 可以知足這些命令的須要,並且不影響它們的行爲。

使用 transformed 可將 T 做用於路徑 p

path q; q := p transformed T;
drawpath q withcolor .7green;
drawpoints q withcolor .7red;

T 的基礎上能夠繼續增長變換。例如,經過 shifted 讓通過了縮放變換的 p 向左平移 0.7 * u

T := T shifted (-0.7 * u, 0);
q := p transformed T;
drawpath  q withcolor .7blue; drawpoints q withcolor .7yellow;

接下來,在 T 的基礎上,再增長一個旋轉變換,令通過了縮放和平移變換後的 p,即 p transformed T,繞其中心點逆時針轉動 90 度。經過 rotated 命令可構造旋轉變換,可是該命令是以原點爲中心對路徑進行旋轉。若對通過了縮放和平移變換後的 p 繞其中心做旋轉變換,首先須要肯定 p 在通過縮放和平移以後的中心點。因爲 p 的初始中心點可根據它的 4 個節點計算出來,結果爲 (0, 0),亦即原點,所以只需對 p 的初始中心點予以 T 變換,即可獲得變換後的 p 的中心點,即

pair pcenter;
pcenter := (0, 0) transformed T;

若讓 p transformed T 圍繞 pcenter 逆時針旋轉 90 度角,須要先對 p transformed T 進行平移變換,令其中心與原點對準,即

p transofmed T shifted (-(xpart pcenter), -(ypart pcenter))

xpartypart 分別用於提取任意一點的橫座標與縱座標份量。而後,對此刻的 p 逆時針旋轉 90 度角,即

p transofmed T shifted (-(xpart pcenter), -(ypart pcenter)) rotated 90

接下來,經過 shifted 將此刻的 p 移回原位,即

p transofmed T shifted (-(xpart pcenter), -(ypart pcenter)) 
               rotated 90 
               shifted ((xpart pcenter), (ypart pcenter))

若將上述的變換疊加到 T 中,即

T := T shifted (-(xpart pcenter), -(ypart pcenter)) 
     rotated 90 
     shifted ((xpart pcenter), (ypart pcenter));

T 做用於 p,即可實現 p transformed T 圍繞 pcenter 逆時針旋轉 90 度角,即

drawpath p transformed T withcolor .7red;
drawpoints p transformed T withcolor .7cyan;

不過,MetaPost 的 rotatedaround 變換已經實現了上述的圍繞指定點對路徑進行旋轉的功能,所以上述的 T 可簡寫爲

T := T rotatedaround (pcenter, 90);

如今,在 T 的基礎上,增長一個鏡象變換,例如,以過原點 (0, 0) 且斜率爲 1 的一條直線爲鏡線,將 p transformed T 變換爲自身的影像。爲了便於觀察,先將鏡線繪製出來,

pair mb, me;
mb := (-1, -1) * u;
me := (1, 1) * u;
drawarrowpath mb -- me;

drawarrowpath 宏可繪製路徑及其走向。顯然,mirrorline 過原點 (0, 0) 且斜率爲 1,基於它,可構造一個鏡象變換。並將其疊加至 T,即

T := T reflectedabout (mb, me);

T 做用於 p 即可獲得 p 的鏡象,

drawpath p transformed T withcolor .7red;
drawpoints p transformed T withcolor .7cyan;

路徑合成

不只變換能夠疊加合成,路徑也能夠如此。例如,對於上一節所給出的路徑 p,對其做旋轉、平移變換,生成路徑 q,而後經過 -- 可將兩者鏈接起來,即

path q[];
q[1] := p scaled 0.5;
q[2] := q[1] shifted (s, 0);
q[3] := q[1] -- q[2];
drawpath q[3]; drawpoints q[3];

在 MetaPost 中,相似 q 這樣的變量稱爲帶有後綴的變量。能夠用此類變量模擬數組。

從簡單到複雜

經過圖形變換和路徑合成,可基於簡單圖形,構造複雜圖形。下面以 Hilbert 曲線的繪製爲例,在實踐中感覺 MetaPost 的魅力。

首先,回顧路徑 p

numeric sidelength, u;
sidelength := 8cm; u := 0.5sidelength;
drawpath fullsquare scaled 2s dashed (evenly scaled 1mm);

pair a, b, c, d;
a := (-0.5, -0.5) * u;
b := (-0.5, 0.5) * u;
c := (0.5, 0.5) * u;
d := (0.5, -0.5) * u;

path p; 
p := a -- b -- c -- d;
drawpath p; drawpoints p;

此時的 p,稱爲 1 階 Hilbert 曲線。

接下來,構造四個變換:

transform sw, nw, ne, se;
  sw := identity
        scaled 0.5
        reflectedabout ((0, 0), (1, 1))
        shifted (-0.5u, -0.5u);
  nw := identity
        scaled 0.5
        shifted (-0.5u, 0.5u);
  ne := identity
        scaled 0.5
        shifted (0.5u, 0.5u);
  se := identity
        scaled 0.5
        reflectedabout ((0, 0), (1, -1))
        shifted (0.5u, -0.5u);

將這四個變換分別做用於 p 並將生成的新路徑鏈接起來,

p := p transformed sw
     -- p transformed nw
     -- p transformed ne
     -- p transformed se;
drawpath p; drawpoints p;

所得結果稱爲 2 階 Hilbert 曲線。對 p 再次作上述變換,即可構造出 3 階 Hilbert 曲線,即

p := p transformed sw
     -- p transformed nw
     -- p transformed ne
     -- p transformed se;

p := p transformed sw
     -- p transformed nw
     -- p transformed ne
     -- p transformed se;

drawpath p; drawpoints p;

依此類推,可繼續構造更高階的 Hilbert 曲線。隨着階數的升高,曲線很快會將一個正方形區域填滿,例如 5 階曲線,

所以,Hilbert 曲線一般被稱爲空間填充曲線。利用 Hilbert 曲線,可將多維空間轉化爲一維連續空間。

循環

使用 MetaPost 的 for 循環語句對高階 Hilbert 曲線的構造代碼予以簡化。例如構造 5 階 Hilbert 曲線,只需

for i := 2 upto 5:
    p := p transformed sw
         -- p transformed nw
         -- p transformed ne
         -- p transformed se;
endfor;
drawpath p; drawpoints p;

若採用更爲通用的 for 語句,上述的 for 代碼可改成

for i := 2 step 1 until 5:
    p := p transformed sw
         -- p transformed nw
         -- p transformed ne
         -- p transformed se;
endfor;

step 能夠控制循環變量 i 的步長。

for 也可用於對象序列的迭代訪問。例如

p := p transformed sw
     -- p transformed nw
     -- p transformed ne
     -- p transformed se;

可寫爲

p := p transformed sw for j := nw, ne, se: -- p transformed j endfor;

MetaPost 容許表達式中出現循環語句,並且循環的最終結果是每一輪循環所包含的內容的鏈接。

在一個 MetaPost 程序裏,除了數據以及註釋語句以外,剩下的幾乎都是宏。mpost 會將程序中全部的宏展開,從而獲得最爲基本的繪圖語句的組合,繼而 mpost 將這些基本的繪圖語句翻譯爲 PostScript 語句,從而獲得 PostScript 格式的文檔。

宏的展開,其基本原理是文本替換。例如

for i := 1 upto 4:
    MetaPost 語句;
endfor;

其中的 upto 就是一個宏,mpost 會將它的展開爲 step 1 until。之因此如此,是由於 upto 的定義

def upto = step 1 until enddef;

upto 沒有參數,它的展開本質上是單純的文本替換。有參數的宏能夠經過參數調整宏的展開結果;宏的參數,本質上是宏展開文本中可變的部分。

經過有參數的宏,可實現更具通常性的 Hilbert 曲線的構造過程。對於 Hilbert 曲線的構造過程而言,可變的部分有 Hilbert 曲線所填充的正方形區域的邊長以及 Hilbert 曲線的階數,若將兩者分別用 numeric 類型的變量 sidelengthn 表示,那麼通常性的 Hilbert 曲線的構造過程可表示爲

numeric u; u := 0.5sidelength;
pair a, b, c, d;
a := (-0.5, -0.5) * u;
b := (-0.5, 0.5) * u;
c := (0.5, 0.5) * u;
d := (0.5, -0.5) * u;

path p; 
p := a -- b -- c -- d;

transform sw, nw, ne, se;
sw := identity
      scaled 0.5
      reflectedabout ((0, 0), (1, 1))
      shifted (-0.5u, -0.5u);
nw := identity
      scaled 0.5
      shifted (-0.5u, 0.5u);
ne := identity
      scaled 0.5
      shifted (0.5u, 0.5u);
se := identity
      scaled 0.5
      reflectedabout ((0, 0), (1, -1))
      shifted (0.5u, -0.5u);

for i := 2 upto n:
  p := p transformed sw for j := nw, ne, se: -- p transformed j endfor;
endfor;

drawpath p; drawpoints p;

將上述語句做爲宏 hilbert 的替換文本,並將 sidelengthn 做爲 hilbert 宏的參數,則 hilbert 宏可定義爲

def hilbert(expr sidelength, n) = 
  numeric u; u := 0.5sidelength;
  pair a, b, c, d;
  a := (-0.5, -0.5) * u;
  b := (-0.5, 0.5) * u;
  c := (0.5, 0.5) * u;
  d := (0.5, -0.5) * u;
  
  path p; 
  p := a -- b -- c -- d;
  
  transform sw, nw, ne, se;
  sw := identity
        scaled 0.5
        reflectedabout ((0, 0), (1, 1))
        shifted (-0.5u, -0.5u);
  nw := identity
        scaled 0.5
        shifted (-0.5u, 0.5u);
  ne := identity
        scaled 0.5
        shifted (0.5u, 0.5u);
  se := identity
        scaled 0.5
        reflectedabout ((0, 0), (1, -1))
        shifted (0.5u, -0.5u);
  
  for i := 2 upto n:
    p := p transformed sw for j := nw, ne, se: -- p transformed j endfor;
  endfor;
  
  drawpath p; drawpoints p;
enddef;

(expr sidelength, n)hilbert 的參數列表,expr 表示參數 sidelengthn 的類型皆爲 MetaPost 的表達式。除了 expr 以外,MetaPost 還支持 textsuffix 類型的參數。text 類型的參數能夠是任意 MetaPost 語句,但結尾必須爲 ;suffix 表示含有後綴的變量,可將該類變量其理解爲數組。須要注意,宏的參數,在其替換文本中不能再從新聲明或賦值。

如今調用 hilbert 宏,即可將其展開爲任意階數的 Hilbert 曲線的構造及繪製語句。例如,在邊長爲 8cm 的正方形區域內構造並繪製 4 階的 Hilbert 曲線,只需

hilbert(8cm, 4);

條件

hilbert 宏有一個 Bug,它沒法構造 1 階 Hilbert 曲線——路徑 p 的初始狀態。要修復這個 Bug,須要使用條件語句

if 條件:
  語句;
elseif 條件:
  語句;
else:
  語句;
fi

其中,elseif 部分可選。

可在構造 Hilbert 曲線的循環中,利用條件語句,將 n = 1 視爲特殊狀況,在這種狀況中不對 p 進行變換,如此即可獲得正確階樹的 Hilbert 曲線,亦即,將 hilbert 宏的替換文本中的

for i := 2 upto n:
  p := p transformed sw for j := nw, ne, se: -- p transformed j endfor;
endfor;

修改成

if n > 1:
  for i := 2 upto n:
    p := p transformed sw for j := nw, ne, se: -- p transformed j endfor;
  endfor;
fi

如此,便修復了 hilbert 宏在曲線階數上的 Bug。

數據與繪圖分離

hilbert 宏的定義還存在一個問題,它作的事情太多了,不只負責 Hilbert 曲線的構造,還負責曲線的繪製。作的事情多,並不意味着功能更強大。若須要對線條的顏色以及粗細虛實予以調整,須要修改 hilbert 宏的定義。應對這些變化,最簡單的方法是讓 hilbert 不負責繪圖,只負責生成 Hilbert 曲線路徑。爲達到這一目的,須要用 vardef 來定義 hilbert 宏,即

vardef hilbert(expr sidelength, n) = 
  numeric u; u := 0.5sidelength;
  pair a, b, c, d;
  a := (-0.5, -0.5) * u;
  b := (-0.5, 0.5) * u;
  c := (0.5, 0.5) * u;
  d := (0.5, -0.5) * u;
  
  path p; 
  p := a -- b -- c -- d;
  
  transform sw, nw, ne, se;
  sw := identity
        scaled 0.5
        reflectedabout ((0, 0), (1, 1))
        shifted (-0.5u, -0.5u);
  nw := identity
        scaled 0.5
        shifted (-0.5u, 0.5u);
  ne := identity
        scaled 0.5
        shifted (0.5u, 0.5u);
  se := identity
        scaled 0.5
        reflectedabout ((0, 0), (1, -1))
        shifted (0.5u, -0.5u);
  
  for i := 2 upto n:
    p := p transformed sw for j := nw, ne, se: -- p transformed j endfor;
  endfor;
  p
enddef;

使用 vardef 定義的宏,其替換文本的最後一句即爲宏返回的結果。將 p 做爲 hilbert 宏的替換文本的最後一句,即可使得 hilbert 返回 Hilbert 曲線路徑。

若測試 hilbert 宏可否知足需求,只需

path p; p := hilbert(8cm, 3);
drawpath p; drawpoints p;

使用 randomized 命令對 p 做輕微的隨機擾動,可以使得 Hilbert 曲線具有一絲藝術氣息,

path p; p := hilbert(8cm, 3) randomized 5mm;
drawpath p; drawpoints p;

randomized 可以對出如今它以前的對象按指定幅度予以隨機擾動。路徑出現於 randomized 以前,則路徑中的全部節點的位置會被隨機擾動。

變量的做用域

調用 hilbert 宏,即便將其將其返回的路徑賦予變量 q,但依然能夠用 p 訪問 hilbert 所生成的路徑:

path q; q := hilbert(5cm, 3);
drawpath p withcolor .625gren;
drawpoints p withcolor .625red;

這意味着在 hilbert 宏的定義中出現的變量 p,在 hilbert 宏的外部也是可見的。之因此出現這樣的結果,緣由在於 MetaPost 語言中,除了循環結構的變量以外,幾乎全部的變量默認皆爲全局變量。例如,

for i := 1 upto 5:
    path p;
    p := fullsquare scaled (i * 1cm) shifted (i * 1cm, 0);
endfor;
drawpath p;

其中,p 爲全局變量,但 i 爲局部變量。

若要構造一些局部變量,須要使用 begingroup ... endgroup 以及 save 語句。例如,

begingroup
save s, p;
numeric s; path q;
s := 5cm;
q := fullsquare scaled s shifted (s, 0);
endgroup;

drawpath p;

在繪製路徑 p 時,mpost 會報錯,由於所繪製的路徑並不存在。

begingroup ... endgroup 構造了一個做用域,save 則用於聲明局部變量的名字。若該結構在宏的定義中使用,即可以對宏內所用的一些不想被外部所知的變量給予保護。

線性方程

mpost 具有線性方程求解的功能。基於這一功能,mpost 可動態肯定變量類型。例如,

a = 1;

這裏的 = 並不是賦值運算符。MetaPost 的賦值運算是上文中一直使用的 :=。這裏的 = 表示方程或等式。在上文講述條件結構的時候,已見識了它。mpost 會對這個方程進行求解,結果是變量 a 的值爲 1,所以這條語句等價於

numeric a;
a := 1;

對於

a + 2b = 5;
3b = 7;

mpost 的求解結果爲

a = 0.33333;
b = 2.33333;

對於

a = (2cm, 3cm);

mpost 會報錯,它認爲一個數值與一個點沒法構成方程,可是將變量名稱寫成以 z 開頭帶有後綴的形式,即可構成方程,例如

z1 = (2cm, 3cm);

mpost 會將 z1 視爲一個點 (x1, y1),所以上述方程本質上是

(x1, y1) = (2cm, 3cm);

反之,假若 mpost 求解了如下方程

x1 = 3cm; 
y1 = 4cm;

就至關於定義了點 z1 = (3cm, 4cm)。變量名稱的後綴,能夠是數字,也能夠是數字 + 字母,還能夠是 . + 數字或字母,例如:

z3 = z1; z3r = z1; z.3 = z1; z.3r = z1;

除了可用於節省變量的聲明以外,利用 mpost 求解線性方程的功能肯定兩條線段的交點也極爲方便。例如,

path p, q;
z0 = (0, 0); z1 = (7cm, 5cm); z2 = (0, 3cm); z3 = (7cm, 3cm);
p := z0 -- z1;
q := z2 -- z3;
z4 = whatever[z0, z1] = whatever[z2, z3];
drawpath p; drawpath q;
drawpoints z4;

whatever[z0, z1] 表示 z0 -- z1 上的任意一點。若寫爲 0.5[z0, z1] 表示線段 z0 -- z1 的中點。若寫爲 1/3[z0, z1] 則表示 z0 -- z1 距離 z0 最近的三等分點。[z0, z1] 這樣的寫法表示由線段 z0 -- z1 構成的區間。兩個數也能構成區間,例如 [2, 4],再例如 0.5[2, 4] 的結果爲 3。

MetaFun:MetaPost + ConTeXt

MetaPost 繪製的圖形,經過 MetaFun 即可與 ConTeXt 的排版元素取得結合,從而顯著加強 ConTeXt 的排版能力。例如,能夠將一條 Hilbert 曲線做爲文本框的背景。固然,只要可以繪製 Hilbert 曲線,將其保存爲單頁面文件,幾乎任何一個功能健全的排版軟件都可以以該圖形做爲文本框的背景,可是一旦圖形被保存爲文件,這就意味着圖形失去了可變性,只適於做爲特定尺寸的文本框的背景。

假設將一條三階 Hilbert 曲線以矢量圖的形式保存爲單頁面文件 hilbert-3.pdf,那麼在 ConTeXt 中可經過覆蓋(Overlay)的方式將其做爲文本框的背景圖片,即

\usemodule[zhfonts]

\defineoverlay[hilbert][{\externalfigure[hilbert-3.pdf]}]
\setupframed
  [background=hilbert,
    width=8cm,
    height=4cm,
    align=middle,
    location=lohi,
    align={middle,lohi,broad}]

\starttext
\framed{\bfd 天地一指也\\ 萬物一馬也}
\stoptext

若直接以 hilbert-3.pdf 文件所包含的圖形做爲文本框(即 \framed)的背景,那麼背景圖片的尺寸默認是 hilbert-3.pdf 文件所包含的圖形的尺寸。顯然,這個尺寸太大了,背景圖片超出了文本框。

理想的文本框背景應該與文本框的尺寸相等。可經過變量(TeX 宏) \overlaywidth\overlayheight 得到當前的文本框的寬度和高度,並基於這兩個尺寸,對背景圖片的尺寸進行調整,使之適應文本框,即

\usemodule[zhfonts]

\defineoverlay
  [hilbert]
  [{\externalfigure
      [hilbert-3.pdf]
      [width=\overlaywidth, height=\overlayheight]}]
\setupframed
  [background=hilbert,
    width=8cm,
    height=4cm,
    align=middle,
    location=lohi,
    align={middle,lohi,broad}]

\starttext
\framed{\bfd 天地一指也\\ 萬物一馬也}
\stoptext

如今,背景圖片被硬性地塞入了文本框,結果致使 Hilbert 曲線的線條變細,而且橫向的線條被圧扁了。這正是以圖形文件中的圖形做爲文本框背景的弊端所在,即背景圖形中的線條失真。此外,通過縮放的 Hilbert 曲線,雖然剛好可以充滿文本框,但實際上並不正確,由於 3 階的 Hilbert 曲線是不可能剛好充滿它所填充的空間。這些失真在 MetaPost 繪圖過程當中不會出現。當 MetaPost 經過 MetaFun 與 ConTeXt 取得融合時,ConTeXt 的排版元素便可以享有這一優點。

爲實現 MetaPost 與 ConTeXt 排版元素的融合,MetaFun 提供了 uniqueMPgraphic 環境,在該環境內編寫 MetaPost 程序,而後這個環境能夠像插圖那樣在 ConTeXt 排版元素中使用。例如,

\startuniqueMPgraphic{hilbert-3}
vardef hilbert(expr sidelength, n) = 
  u = 0.5sidelength;
  z1 = (-0.5, -0.5) * u;
  z2 = (-0.5, 0.5) * u;
  z3 = (0.5, 0.5) * u;
  z4 = (0.5, -0.5) * u;
  
  path p; p := z1 -- z2 -- z3 -- z4;
  
  transform sw, nw, ne, se;
  sw := identity
        scaled 0.5
        reflectedabout ((0, 0), (1, 1))
        shifted (-0.5u, -0.5u);
  nw := identity
        scaled 0.5
        shifted (-0.5u, 0.5u);
  ne := identity
        scaled 0.5
        shifted (0.5u, 0.5u);
  se := identity
        scaled 0.5
        reflectedabout ((0, 0), (1, -1))
        shifted (0.5u, -0.5u);
  
  for i = 2 upto n:
    p := p transformed sw for j := nw, ne, se: -- p transformed j endfor;
  endfor;
  p
enddef;

path p; p := hilbert(OverlayWidth, 3);
drawpath p yscaled (OverlayHeight / OverlayWidth);
\stopuniqueMPgraphic

在上述名爲 hilbert-3uniqueMPgraphic 環境中,對 hilbert 宏所生成的 Hibert 曲線,根據變量 OverlayWitdhOverlayHeight 的值給出了適應性的縮放,亦即在 uniqueMPgraphic 環境中,MetaPost 程序能夠共享 ConTeXt 排版元素的一些變量。

若將上述 uniqueMPgraphic 環境做爲文本框的背景圖片,只需

\defineoverlay[hilbert][\uniqueMPgraphic{hilbert-3}]
\setupframed
  [background=hilbert,
    width=8cm,
    height=4cm,
    align=middle,
    location=lohi,
    align={middle,lohi,broad}]

\starttext
\framed{\bfd 天地一指也\\ 萬物一馬也}
\stoptext

結果可得

使用 \framedframe=off 能夠隱藏文本框的邊框,這樣即可獲得以 3 階 Hilbert 曲線做爲背景的文本框,並且背景的尺寸可以適應文本框的尺寸的變化。例如,

\setupframed[frame=off]
\midaligned{\framed{\bfd 天地一指也\\ 萬物一馬也}}
\blank[1cm]
\midaligned{\framed[width=12cm, height=3cm]{\bfd 天地一指也\\ 萬物一馬也}}

以上僅以文本框爲例,簡單介紹了 ConTeXt 與 MetaPost 的結合。事實上,對於 ConTeXt 的任一排版元素,只要它具有 background 選項,即可以利用 MetaPost 圖形爲其構建背景圖形。即便一些排版元素不具有 background 選項,可是隻要它們具有 command 選項,即可以經過嵌入文本框的方式與 MetaPost 圖形結合。

結語

有關 MetaFun 更爲詳細的介紹見荷蘭人 Hans Hagen 所寫的 MetaFun 手冊 [10] 。Hans Hagen 便是 ConTeXt 的開發者,也是 MetaFun 的開發者。

對於以編程的方式繪製精確二維矢量圖這種任務而言,MetaPost 是一種功能強大的編程語言。不過,適合這一任務的功能強大的編程語言並很多,譬如 LaTeX 的小夥伴 pgf/tikz,擅長繪製三維矢量圖的 Asymptote,擅長繪製圖表的 gnuplot、MathGL 等。與這些同類相比,MetaPost 勝出之處在於語法的優雅。

MetaPost 語法的優雅一方面來自於它的宏編程特質。像每一種優雅都來自刻苦地訓練同樣,MetaPost 的優雅也並不是朝發夕至之工可致。在編寫這篇文章的一些簡單示例的過程當中,mpost 崩潰次數難以歷數,並且它的每次崩潰幾乎都會給出冗長的出錯信息,須要像偵探同樣從中查出端倪。所以,MetaPost 的優雅只是會向那些繪製精確矢量圖這種任務樂此不疲的人綻開。另外一方面,MetaPost MetaPost 繪圖命令與英文的語法相近,即「謂語 + 賓語 + 定語 + 狀語」的形式,例如,

主語 I(省略) + 謂語 draw + 路徑 p + 定語 scaled 0.5 + 狀語 withcolor .8red;

MetaFun 的出現,爲 MetaPost 在排版領域開闢了用武之地。在文檔排版方面,利用 MetaPost 所繪製的精確的矢量圖形爲一些排版元素構造背景,使得文檔的排版更爲精美。

繪圖是一門藝術。排版也是一門藝術。藝術的重要性在於它可以開拓人類的思惟空間。使用 MetaPost 繪圖,使用 ConTeXt 對文檔進行排版則是技術。藝術的空間須要藉助技術去探索或開拓。MetaFun 貫通了 MetaPost 和 ConTeXt,意味着具有了探索或開拓計算機繪圖與排版相融合的藝術空間的一種工具。


引用的文獻:

[1]  序幕有些長

[2]  睦鄰友好的 ConTeXt Standalone

[3]  先寫做,後排版

[4]  ConTeXt MkIV 中文支持

[5]  文稿的物理結構

[6]  文稿的邏輯結構

[7]  頁面佈局

[8]  ConTeXt Mark IV an excursion

[9]  ConTeXt Reference

[10]  MetaFun Manual

相關文章
相關標籤/搜索