序言
备考某等级考试的时候,在教材中碰到了几个一直不太理解的、关于硬盘的概念:磁道、柱面号、扇区。然而教材没有配图,无法直观地了解这些概念的物理形态。维基百科的硬盘词条页中倒是有一副不错的示意图,我截图搬运了过来
原图是一张SVG图片,本质上是一堆指令——也就是所谓的语绘啦。我是一个语绘爱好者,也想试试看能否用代码画一幅差不多的图出来。
在旧文《程序员特有的画图方式——语绘工具小入门》中,我演示过几款写代码画图的工具,但它们都不适合用来绘制几何图形,所以这次它们没有用武之地。
本来我想试试用MetaPost来画的,但鉴于“入门”了太多次,这次还是换点新花样吧。这一次,我用LaTeX+TikZ来画。
TikZ是什么及光速入门
著名的压泡面神器、麻将桌脚垫《TAOCP》的作者发明了TeX,知名的Raft竞品Paxos算法的作者在此基础上创造了LaTeX,它们都是程序员简历论文排版的好帮手。而TikZ则是如虎添翼地在LaTeX中实现了简单易懂的绘图功能的一个红包宏包(macro package,TeX的术语)。简而言之,TikZ自定义了一套“语言”,可以在用LaTeX编写的文档中画出各种图形。
百闻不如一见,我演示一下如何用TikZ画一条线段、一个圆,以及一段圆弧。先将下列的代码保存到一个文件three_in_one.tex
中
1 2 3 4 5 6 7 8 9 10 11 12 13
| \documentclass{standalone} \usepackage{tikz} \usetikzlibrary{shapes.geometric, arrows} \begin{document} \begin{tikzpicture}[scale=2] \draw (0, 0) -- (1, 1); \draw (1, 1) circle (2); \draw (1, 0) arc (0:30:1); \end{tikzpicture} \end{document}
|
再使用xelatex
将其编译成PDF文件(xelatex
可以通过安装TeXLive 2020获得)
1
| xelatex three_in_one.tex
|
此时便得到了three_in_one.pdf
文件。为了可以在文章中显示,我用ImageMagick将其转换为PNG文件
1
| convert three_in_one.pdf /tmp/three_in_one.png
|
最终的图片如下
简单,就像画一匹马一样简单。
现在该来试试用TikZ复刻维基百科上的硬盘示意图了。
来点同心圆
在原图中最引人注目的,当属那十几个同心圆了。简单起见,我只画六个圆。这六个圆的半径相差1pt
(pt
是TikZ默认的长度单位),从3pt
一直递增到8pt
,它们的圆心都在坐标原点(0, 0)
上。
1 2 3 4 5 6 7 8 9
| \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)
来画出这根线段。
通过调整其中的角度可以画出剩余的其它线段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| \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
命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| \begin{tikzpicture} \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,方法是一样的,只是描述封闭曲线的坐标不同罢了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| \begin{tikzpicture} \fill[blue] (0, 0) -- (30:8) arc (30:60:8) -- cycle; \fill[purple] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4); \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的环形没办法用这种方式来描述。不过没关系,只要将其视为上下半两部分,再分别上色即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| \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); \fill[blue] (0, 0) -- (30:8) arc (30:60:8) -- cycle; \fill[purple] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4); \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
命令定义四个新的颜色的名字。
1 2 3 4 5
| \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
命令中的颜色名即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| \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); \fill[areaB] (0, 0) -- (30:8) arc (30:60:8) -- cycle; \fill[areaC] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4); \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
命令即可。比如说,我可以把四条线定义如下,其中的坐标和线段的长度纯粹是个人偏好
1 2 3 4
| \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
,以及一段用花括号包裹的文本即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| \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); \fill[areaB] (0, 0) -- (30:8) arc (30:60:8) -- cycle; \fill[areaC] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4); \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行代码才完成的等分线
1 2
| \foreach \x in {0,30,60,90,120,150,180,210,240,270,300,330} \draw (\x:3) -- (\x:8);
|
TikZ的更多潜力和乐趣,就由各位读者自己探索吧。