精確之美——用TikZ畫硬盤示意圖

序言

備考某等級考試的時候,在教材中碰到了幾個一直不太理解的、關於硬盤的概念:磁道、柱面號、扇區。然而教材沒有配圖,沒法直觀地瞭解這些概念的物理形態。維基百科的硬盤詞條頁中卻是有一副不錯的示意圖,我截圖搬運了過來node

機械硬盤示意圖

原圖是一張SVG圖片,本質上是一堆指令——也就是所謂的語繪啦。我是一個語繪愛好者,也想試試看可否用代碼畫一幅差很少的圖出來。git

在舊文《程序員特有的畫圖方式——語繪工具小入門》中,我演示過幾款寫代碼畫圖的工具,但它們都不適合用來繪製幾何圖形,因此此次它們沒有用武之地。程序員

原本我想試試用MetaPost來畫的,但鑑於「入門」了太屢次,此次仍是換點新花樣吧。這一次,我用LaTeX+TikZ來畫。github

TikZ是什麼及光速入門

著名的壓泡麪神器、麻將桌腳墊《TAOCP》的做者發明了TeX,知名的Raft競品Paxos算法的做者在此基礎上創造了LaTeX,它們都是程序員簡歷論文排版的好幫手。而TikZ則是如虎添翼地在LaTeX中實現了簡單易懂的繪圖功能的一個紅包宏包(macro package,TeX的術語)。簡而言之,TikZ自定義了一套「語言」,能夠在用LaTeX編寫的文檔中畫出各類圖形。算法

百聞不如一見,我演示一下如何用TikZ畫一條線段、一個圓,以及一段圓弧。先將下列的代碼保存到一個文件three_in_one.tex編程

\documentclass{standalone}
\usepackage{tikz}
\usetikzlibrary{shapes.geometric, arrows}
\begin{document}
\begin{tikzpicture}[scale=2]
  %% 畫一條從原點指向(1, 1)的線段
  \draw (0, 0) -- (1, 1);
  %% 畫一個以(1, 1)爲圓心,半徑爲2的圓。
  \draw (1, 1) circle (2);
  %% 畫一段以原點爲圓心,半徑爲1,張開角度爲30度的圓弧。
  \draw (1, 0) arc (0:30:1);
\end{tikzpicture}
\end{document}

再使用xelatex將其編譯成PDF文件(xelatex能夠經過安裝TeXLive 2020得到)函數

xelatex three_in_one.tex

此時便獲得了three_in_one.pdf文件。爲了能夠在文章中顯示,我用ImageMagick將其轉換爲PNG文件工具

convert three_in_one.pdf /tmp/three_in_one.png

最終的圖片以下字體

簡單,就像畫一匹馬同樣簡單。spa

如今該來試試用TikZ復刻維基百科上的硬盤示意圖了。

來點同心圓

在原圖中最引人注目的,當屬那十幾個同心圓了。簡單起見,我只畫六個圓。這六個圓的半徑相差1ptpt是TikZ默認的長度單位),從3pt一直遞增到8pt,它們的圓心都在座標原點(0, 0)上。

%% 爲了節省篇幅,只給出TikZ部分的代碼。
\begin{tikzpicture}
  \draw (0, 0) circle (3);
  \draw (0, 0) circle (4);
  \draw (0, 0) circle (5);
  \draw (0, 0) circle (6);
  \draw (0, 0) circle (7);
  \draw (0, 0) circle (8);
\end{tikzpicture}

來點等分線

原圖中有12根線段,將每個圓等分紅了全等的12份。從前一節的內容可知,要用\draw命令繪製線段,須要的是線段兩端的座標,那麼這批座標要怎麼計算呢?儘管能夠用三角函數計算出這些點的笛卡爾座標,但在TikZ中能夠用更方便的極座標來指定這些點。

以原圖中從X軸開始逆時針旋轉遇到的第一條線段爲例,它在半徑爲3pt的圓上的點的座標爲(30:3)(30是極座標中的角度,3是半徑長度),而在半徑爲8pt的圓上的點的座標爲(30:8),所以能夠用\draw (30:3) -- (30:8)來畫出這根線段。

經過調整其中的角度能夠畫出剩餘的其它線段。

\begin{tikzpicture}
  \draw (0, 0) circle (3);
  \draw (0, 0) circle (4);
  \draw (0, 0) circle (5);
  \draw (0, 0) circle (6);
  \draw (0, 0) circle (7);
  \draw (0, 0) circle (8);

  \draw (0:3) -- (0:8);
  \draw (30:3) -- (30:8);
  \draw (60:3) -- (60:8);
  \draw (90:3) -- (90:8);
  \draw (120:3) -- (120:8);
  \draw (150:3) -- (150:8);
  \draw (180:3) -- (180:8);
  \draw (210:3) -- (210:8);
  \draw (240:3) -- (240:8);
  \draw (270:3) -- (270:8);
  \draw (300:3) -- (300:8);
  \draw (330:3) -- (330:8);
\end{tikzpicture}

來張色圖

原圖大體的骨架已經畫完了,如今來嘗試給它上色。在TikZ中,能夠用\fill命令給一段封閉的曲線上色。好比用\fill[red] (0, 0) -- (1, 0) -- (1, 1) -- (0, 1) -- cycle能夠將左下角在原點、邊長爲1pt的正方形塗成紅色。

先給原圖中的區域B上色。區域B是一個扇形,它由兩根長度爲8pt的半徑和一段夾角爲30度的圓弧構成。要描述這段封閉曲線,能夠藉助入門一節中介紹的arc命令。

\begin{tikzpicture}
  %% 給區域B上色。
  \fill[blue] (0, 0) -- (30:8) arc (30:60:8) -- cycle;

  \draw (0, 0) circle (3);
  \draw (0, 0) circle (4);
  \draw (0, 0) circle (5);
  \draw (0, 0) circle (6);
  \draw (0, 0) circle (7);
  \draw (0, 0) circle (8);

  \draw (0:3) -- (0:8);
  \draw (30:3) -- (30:8);
  \draw (60:3) -- (60:8);
  \draw (90:3) -- (90:8);
  \draw (120:3) -- (120:8);
  \draw (150:3) -- (150:8);
  \draw (180:3) -- (180:8);
  \draw (210:3) -- (210:8);
  \draw (240:3) -- (240:8);
  \draw (270:3) -- (270:8);
  \draw (300:3) -- (300:8);
  \draw (330:3) -- (330:8);
\end{tikzpicture}

\fill命令那一行最後的cycle的意思,是讓曲線回到起點組成一個封閉的形狀。另外,\fill命令須要寫在\draw命令以前,是爲了不藍色顏料將區域內的圓弧給蓋住了。

對於區域C和區域D,方法是同樣的,只是描述封閉曲線的座標不一樣罷了。

\begin{tikzpicture}
  %% 給區域B上色。
  \fill[blue] (0, 0) -- (30:8) arc (30:60:8) -- cycle;
  %% 給區域C上色。
  \fill[purple] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4);
  %% 給區域D上色。
  \fill[green] (240:6) -- (240:7) arc (240:330:7) -- (330:6) -- (330:6) arc (330:240:6);

  \draw (0, 0) circle (3);
  \draw (0, 0) circle (4);
  \draw (0, 0) circle (5);
  \draw (0, 0) circle (6);
  \draw (0, 0) circle (7);
  \draw (0, 0) circle (8);

  \draw (0:3) -- (0:8);
  \draw (30:3) -- (30:8);
  \draw (60:3) -- (60:8);
  \draw (90:3) -- (90:8);
  \draw (120:3) -- (120:8);
  \draw (150:3) -- (150:8);
  \draw (180:3) -- (180:8);
  \draw (210:3) -- (210:8);
  \draw (240:3) -- (240:8);
  \draw (270:3) -- (270:8);
  \draw (300:3) -- (300:8);
  \draw (330:3) -- (330:8);
\end{tikzpicture}

給環形上色

聰明的讀者也許已經發現了,區域A的環形沒辦法用這種方式來描述。不過不要緊,只要將其視爲上下半兩部分,再分別上色便可。

\begin{tikzpicture}
  %% 環的上半部分
  \fill[red] (4, 0) -- (5, 0) arc (0:180:5) -- (-4, 0) -- (-4, 0) arc (180:0:4);
  %% 環的下半部分
  \fill[red] (4, 0) -- (5, 0) arc (360:180:5) -- (-4, 0) -- (-4, 0) arc (180:360:4);
  %% 給區域B上色。
  \fill[blue] (0, 0) -- (30:8) arc (30:60:8) -- cycle;
  %% 給區域C上色。
  \fill[purple] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4);
  %% 給區域D上色。
  \fill[green] (240:6) -- (240:7) arc (240:330:7) -- (330:6) -- (330:6) arc (330:240:6);

  \draw (0, 0) circle (3);
  \draw (0, 0) circle (4);
  \draw (0, 0) circle (5);
  \draw (0, 0) circle (6);
  \draw (0, 0) circle (7);
  \draw (0, 0) circle (8);

  \draw (0:3) -- (0:8);
  \draw (30:3) -- (30:8);
  \draw (60:3) -- (60:8);
  \draw (90:3) -- (90:8);
  \draw (120:3) -- (120:8);
  \draw (150:3) -- (150:8);
  \draw (180:3) -- (180:8);
  \draw (210:3) -- (210:8);
  \draw (240:3) -- (240:8);
  \draw (270:3) -- (270:8);
  \draw (300:3) -- (300:8);
  \draw (330:3) -- (330:8);
\end{tikzpicture}

潤色一下

用macOS的「數碼測色計」看了一下原圖中各個區域的顏色的RGB值,區域A大概是(236, 133, 130)、區域B大概是(122, 127, 237)、區域C大概是(131, 132, 139)、區域D大概是(0, 151, 27)。接下來我讓TikZ以這四種指定的顏色填充圖中的四個區域,先用LaTeX的\definecolor命令定義四個新的顏色的名字。

%% 下列四行代碼置於document環境以前
\definecolor{areaA}{RGB}{236,133,130}
\definecolor{areaB}{RGB}{122,127,237}
\definecolor{areaC}{RGB}{131,32,139}
\definecolor{areaD}{RGB}{0,151,27}

再替換掉\fill命令中的顏色名便可

\begin{tikzpicture}
  %% 環的上半部分
  \fill[areaA] (4, 0) -- (5, 0) arc (0:180:5) -- (-4, 0) -- (-4, 0) arc (180:0:4);
  %% 環的下半部分
  \fill[areaA] (4, 0) -- (5, 0) arc (360:180:5) -- (-4, 0) -- (-4, 0) arc (180:360:4);
  %% 給區域B上色。
  \fill[areaB] (0, 0) -- (30:8) arc (30:60:8) -- cycle;
  %% 給區域C上色。
  \fill[areaC] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4);
  %% 給區域D上色。
  \fill[areaD] (240:6) -- (240:7) arc (240:330:7) -- (330:6) -- (330:6) arc (330:240:6);

  \draw (0, 0) circle (3);
  \draw (0, 0) circle (4);
  \draw (0, 0) circle (5);
  \draw (0, 0) circle (6);
  \draw (0, 0) circle (7);
  \draw (0, 0) circle (8);

  \draw (0:3) -- (0:8);
  \draw (30:3) -- (30:8);
  \draw (60:3) -- (60:8);
  \draw (90:3) -- (90:8);
  \draw (120:3) -- (120:8);
  \draw (150:3) -- (150:8);
  \draw (180:3) -- (180:8);
  \draw (210:3) -- (210:8);
  \draw (240:3) -- (240:8);
  \draw (270:3) -- (270:8);
  \draw (300:3) -- (300:8);
  \draw (330:3) -- (330:8);
\end{tikzpicture}

圖文並茂

剩下的須要復刻的東西就是原圖中的文字以及標註用的線了。線很容易畫,只要規定了座標後用\draw命令便可。好比說,我能夠把四條線定義以下,其中的座標和線段的長度純粹是我的偏好

\draw (75:4.5) -- (75:9);
\draw (40:7.5) -- (40:9);
\draw (50:4.5) -- (50:9);
\draw (285:6.5) -- (285:9);

線畫完了,再到每一根線的「終點」標上文字說明,這須要用到TikZ的node功能。用法很簡單,就是在須要標註文字的座標後,緊跟着關鍵字node,以及一段用花括號包裹的文本便可

\documentclass{standalone}
\usepackage{tikz}
\usepackage{xeCJK}
\setCJKmainfont{Songti TC}
\usetikzlibrary{shapes.geometric, arrows}
\definecolor{areaA}{RGB}{236,133,130}
\definecolor{areaB}{RGB}{122,127,237}
\definecolor{areaC}{RGB}{131,32,139}
\definecolor{areaD}{RGB}{0,151,27}
\begin{document}
\begin{tikzpicture}
  %% 環的上半部分
  \fill[areaA] (4, 0) -- (5, 0) arc (0:180:5) -- (-4, 0) -- (-4, 0) arc (180:0:4);
  %% 環的下半部分
  \fill[areaA] (4, 0) -- (5, 0) arc (360:180:5) -- (-4, 0) -- (-4, 0) arc (180:360:4);
  %% 給區域B上色。
  \fill[areaB] (0, 0) -- (30:8) arc (30:60:8) -- cycle;
  %% 給區域C上色。
  \fill[areaC] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4);
  %% 給區域D上色。
  \fill[areaD] (240:6) -- (240:7) arc (240:330:7) -- (330:6) -- (330:6) arc (330:240:6);

  \draw (0, 0) circle (3);
  \draw (0, 0) circle (4);
  \draw (0, 0) circle (5);
  \draw (0, 0) circle (6);
  \draw (0, 0) circle (7);
  \draw (0, 0) circle (8);

  \draw (0:3) -- (0:8);
  \draw (30:3) -- (30:8);
  \draw (60:3) -- (60:8);
  \draw (90:3) -- (90:8);
  \draw (120:3) -- (120:8);
  \draw (150:3) -- (150:8);
  \draw (180:3) -- (180:8);
  \draw (210:3) -- (210:8);
  \draw (240:3) -- (240:8);
  \draw (270:3) -- (270:8);
  \draw (300:3) -- (300:8);
  \draw (330:3) -- (330:8);

  \draw (75:4.5) -- (75:9) node {磁道};
  \draw (40:7.5) -- (40:9) node {扇面};
  \draw (50:4.5) -- (50:9) node {扇區};
  \draw (285:6.5) -- (285:9) node {簇};
\end{tikzpicture}
\end{document}

須要留意的是,我在源代碼開頭的位置,引入了xeCJK宏包(\usepackage{xeCJK}),而且指定了中文內容用的字體爲宋體(\setCJKmainfont{Songti TC}),這樣才能成功編譯。

至此,復刻算是完成了。

後記

本文只是管中窺豹,TikZ還能夠畫出其它更復雜更美輪美奐的圖形,有興趣的讀者能夠移步這裏觀賞。此外,TikZ也能夠「編程」,好比下面的兩行代碼便足矣畫出上文中12行代碼才完成的等分線

\foreach \x in {0,30,60,90,120,150,180,210,240,270,300,330}
\draw (\x:3) -- (\x:8);

TikZ的更多潛力和樂趣,就由各位讀者本身探索吧。

閱讀原文

相關文章
相關標籤/搜索