MetaPost中的各种事情

用来绘制矢量图片的最佳工具是什么? 对于我以及可能对许多其他人来说,答案非常明显:Illustrator,或者也许是Inkscape。 至少当我被要求为一本物理教科书绘制大约八百张图时,我就是这么想的。 没什么例外,只有一堆黑白插图,上面有球体,弹簧,滑轮,镜头等。 到那时,已经知道这本书将使用LaTeX制作,并且给了我许多带有嵌入式图像的MS Word文档。 其中一些是其他书籍的扫描图片,一些是铅笔素描。 想像日夜不停地渗入这些东西使我感到头晕,所以很快我发现自己幻想着采用一种更加自动化的解决方案。 由于某种原因, MetaPost成为了这些幻想的焦点。





使用MetaPost(或类似的)解决方案的主要优点是每张图片可以是多个变量的一种功能。 可以针对布局中任何无法预料的情况快速调整此类图片,而不会破坏插图的重要内部关系(我真正担心的事情),而这是使用更传统的工具很难实现的。 同样,与传统工具在相同的时间限制下所允许的相比,可以使重复元素(所有这些球体和弹簧)在视觉上更加有趣。

我想用一些阴影线来制作图片,这与您在旧书中遇到的情况没什么不同。



首先,我需要能够生成一些不同厚度的曲线。 这里的主要复杂之处在于构建一条曲线,该曲线在变化的距离处跟随原始曲线。 我可能使用了最原始的工作方法 ,归结为简单地将连接贝塞尔曲线控制点的线段移动给定距离,只是该距离沿曲线变化。



在大多数情况下,它工作正常。



范例程式码
从这里开始,假定已下载该库并input fiziko.mp; 在MetaPost代码中。 最快的方法是使用ConTeXt(这样就不需要beginfigendfig ):

\starttext
\startMPcode
input fiziko.mp;
% the code goes here
\stopMPcode
\stoptext


或LuaLaTeX:

\documentclass{article}
\usepackage{luamplib}
\begin{document}
\begin{mplibcode}
input fiziko.mp;
% the code goes here
\end{mplibcode}
\end{document}


beginfig(3);
path p, q; % MetaPost's syntax is reasonably readable, so I'll comment mostly on my stuff
p := (0,-1/4cm){dir(30)}..(5cm, 0)..{dir(30)}(10cm, 1/4cm);
q := offsetPath(p)(1cm*sin(offsetPathLength*pi)); % first argument is the path itself, second is a function of the position along this path (offsetPathLength changes from 0 to 1), which determines how far the outline is from the original line
draw p;
draw q dashed evenly;
endfig;



可以将两个轮廓组合起来以为厚度可变的笔划绘制轮廓线。



范例程式码
beginfig(4);
path p, q[];
p := (0,-1/4cm){dir(30)}..(5cm, 0)..{dir(30)}(10cm, 1/4cm);
q1 := offsetPath(p)(1/2pt*(sin(offsetPathLength*pi)**2)); % outline on one side
q2 := offsetPath(p)(-1/2pt*(sin(offsetPathLength*pi)**2)); % and on the other
fill q1--reverse(q2)--cycle;
endfig;



线的粗细应该有一个下限,否则,有些线会太细而无法正确打印,这看起来并不好。 选项之一(我选择的一种)是将太细的线设置为虚线,以使每单位长度的墨水总量与预期的较细线大致相同。 换句话说,该算法不是减少线条两侧的墨水量,而是从线条本身获取一些墨水。



范例程式码
beginfig(5);
path p;
p := (0,-1/4cm){dir(30)}..(5cm, 0)..{dir(30)}(10cm, 1/4cm);
draw brush(p)(1pt*(sin(offsetPathLength*pi)**2)); % the arguments are the same as for the outline
endfig;



一旦有了可变厚度的线,就可以绘制球体。 球体可以描述为一系列同心圆,线的粗细根据计算球体上特定点的亮度的函数的输出而变化。



范例程式码
beginfig(6);
draw sphere.c(1.2cm);
draw sphere.c(2.4cm) shifted (2cm, 0);
endfig;



另一个方便的构造块是“管”。 粗略地说,这是一个可以弯曲的圆柱体。 只要直径恒定,就非常简单。



范例程式码
beginfig(7);
path p;
p := subpath (1,8) of fullcircle scaled 3cm;
draw tube.l(p)(1/2cm); % arguments are the path itself and the tube radius
endfig;



如果直径不是恒定的,则情况会变得更加复杂:行程数应根据管的厚度而变化,以便在考虑照明之前保持每单位面积的墨水量恒定。



范例程式码
beginfig(8);
path p;
p := pathSubdivide(fullcircle, 2) scaled 3cm; % this thing splits every segment between the points of a path (here—fullcircle) into several parts (here—2)
draw tube.l(p)(1/2cm + 1/6cm*sin(offsetPathLength*10pi));
endfig;



也有带有横剖面线的管。 在这种情况下,保持墨水量恒定的问题变得更加棘手,因此,这种管常常显得有些毛茸茸。



范例程式码
beginfig(9);
path p;
p := pathSubdivide(fullcircle, 2) scaled 3cm;
draw tube.t(p)(1/2cm + 1/6cm*sin(offsetPathLength*10pi));
endfig;



管子可用于构造各种物体:从圆锥体,圆柱体到栏杆。



范例程式码
beginfig(10);
draw tube.l ((0, 0) -- (0, 3cm))((1-offsetPathLength)*1cm) shifted (-3cm, 0); % a very simple cone
path p;
p := (-1/2cm, 0) {dir(175)} .. {dir(5)} (-1/2cm, 1/8cm) {dir(120)} .. (-2/5cm, 1/3cm) .. (-1/2cm, 3/4cm) {dir(90)} .. {dir(90)}(-1/4cm, 9/4cm){dir(175)} .. {dir(5)}(-1/4cm, 9/4cm + 1/5cm){dir(90)} .. (-2/5cm, 3cm); % baluster's envelope
p := pathSubdivide(p, 6);
draw p -- reverse(p xscaled -1) -- cycle;
tubeGenerateAlt(p, p xscaled -1, p rotated -90); % a more low-level stuff than tube.t, first two arguments are tube's sides and the third is the envelope. The envelope is basically a flattened out version of the outline, with line length along the x axis and the distance to line at the y. In the case of this baluster, it's simply its side rotated 90 degrees.
endfig;



库中包含一些可以由这些原语制成的构造。 例如,地球基本上是一个球体。



范例程式码
beginfig(11);
draw globe(1cm, -15, 0) shifted (-6/2cm, 0); % radius, west longitude, north latitude, both decimal
draw globe(3/2cm, -30.280577, 59.939461);
draw globe(4/3cm, -140, -30) shifted (10/3cm, 0);
endfig;



但是,这里的阴影是纬度的,并且控制线密度比使用“同心”阴影的常规球要困难得多,因此它是另一种球。



范例程式码
beginfig(12);
draw sphere.l(2cm, -60); % diameter and latitude
draw sphere.l(3cm, 45) shifted (3cm, 0);
endfig;



配重是由两种类型的管制成的简单结构。



范例程式码
beginfig(13);
draw weight.s(1cm); % weight height
draw weight.s(2cm) shifted (2cm, 0);
endfig;



还有一个打结管的工具。



示例代码。 为简便起见,只打一个结。
beginfig(14);
path p;
p := (dir(90)*4/3cm) {dir(0)} .. tension 3/2 ..(dir(90 + 120)*4/3cm){dir(90 + 30)} .. tension 3/2 ..(dir(90 - 120)*4/3cm){dir(-90 - 30)} .. tension 3/2 .. cycle;
p := p scaled 6/5;
addStrandToKnot (primeOne) (p, 1/4cm, "l", "1, -1, 1"); % first, we add a strand of width 1/4cm going along the path p to the knot named primeOne. its intersections along the path go to layers "1, -1, 1" and the type of tube is going to be "l".
draw knotFromStrands (primeOne); % then the knot is drawn. you can add more than one strand.
endfig;



结中的管子会互相照应地散下阴影。 从理论上讲,可以在其他情况下使用此功能,但是由于我没有计划深入研究第三维,因此用户界面有所欠缺,阴影仅适用于某些对象。



范例程式码
beginfig(15);
path shadowPath[];
boolean shadowsEnabled;
numeric numberOfShadows;
shadowsEnabled := true; % shadows need to be turned on
numberOfShadows := 1; % number of shadows should be specified
shadowPath0 := (-1cm, -2cm) -- (-1cm, 2cm) -- (-1cm +1/6cm, 2cm) -- (-1cm + 1/8cm, -2cm) -- cycle; % shadow-dropping object should be a closed path
shadowDepth0 := 4/3cm; % it's just this high above the object on which the shadow falls
shadowPath1 := shadowPath0 rotated -60;
shadowDepth1 := 4/3cm;
draw sphere.c(2.4cm); % shadows work ok only with sphere.c and tube.l with constant diameter
fill shadowPath0 withcolor white;
draw shadowPath0;
fill shadowPath1 withcolor white;
draw shadowPath1;
endfig;



当然,您将需要木材纹理(更新:自从本文的俄语版发布以来,该库在我所知的真实项目中使用的第一种情况就已经发生了 ,这就是后来出现的木材纹理。派上用场,所以这最终不是一个玩笑。 细枝及其生长如何影响年轮的模式是一些认真研究的主题。 我能想到的最简单的工作模型如下:年轮是平行的平坦表面,会因树枝成长而变形。 因此,在不同位置通过一系列不太复杂的“细枝功能”对表面进行了修改,并将表面的等值线作为年轮样式。



范例程式码
beginfig(16);
numeric w, b;
pair A, B, C, D, A', B', C', D';
w := 4cm;
b := 1/2cm;
A := (0, 0);
A' := (b, b);
B := (0, w);
B' := (b, wb);
C := (w, w);
C' := (wb, wb);
D := (w, 0);
D' := (wb, b);
draw woodenThing(A--A'--B'--B--cycle, 0); % a piece of wood inside the A--A'--B'--B--cycle path, with wood grain at 0 degrees
draw woodenThing(B--B'--C'--C--cycle, 90);
draw woodenThing(C--C'--D'--D--cycle, 0);
draw woodenThing(A--A'--D'--D--cycle, 90);
eyescale := 2/3cm; % scale for the eye
draw eye(150) shifted 1/2[A,C]; % the eye looks in 150 degree direction
endfig;



上图的眼睛张开或斜视,瞳孔也改变了大小。 这可能没有任何实际意义,但是机械上相似的眼睛看起来很无聊。



范例程式码
beginfig(17);
eyescale := 2/3cm; % 1/2cm by default
draw eye(0) shifted (0cm, 0);
draw eye(0) shifted (1cm, 0);
draw eye(0) shifted (2cm, 0);
draw eye(0) shifted (3cm, 0);
draw eye(0) shifted (4cm, 0);
endfig;



大多数情况下,插图并没有那么复杂,但要采取更严格的方法,需要解决教科书中的许多问题才能正确地说明它们。 说,L'Hôpital的滑轮问题(不在那本教科书中,但无论如何):在长度上 l在该点暂停 滑轮挂着; 它钩在另一根绳子上,悬挂在该点上 B与重量 C在其末端。 问题是:如果滑轮和绳索都什么都不称重,重量会流向何方? 令人惊讶的是,该问题的解决方案和构造并不那么简单。 但是通过玩几个变量,您可以使图片看起来恰好适合页面,同时保持准确性。



范例程式码
vardef lHopitalPulley (expr AB, l, m) = % distance AB between the suspension points of the ropes and their lengths l and m. “Why no units of length?”, you may ask. It's because some calculations inside can cause arithmetic overflow in MetaPost.
save A, B, C, D, E, o, a, x, y, d, w, h, support;
image(
pair A, B, C, D, E, o[];
path support;
numeric a, x[], y[], d[], w, h;
x1 := (l**2 + abs(l)*((sqrt(8)*AB)++l))/4AB; % the solution
y1 := l+-+x1; % second coordinate is trivial
y2 := m - ((AB-x1)++y1); % as well as the weight's position
A := (0, 0);
B := (AB*cm, 0);
D := (x1*cm, -y1*cm);
C := D shifted (0, -y2*cm);
d1 := 2/3cm; d2 := 1cm; d3 := 5/6d1; % diameters of the pulley, weight and the pulley wheel
w := 2/3cm; h := 1/3cm; % parameters of the wood block
o1 := (unitvector(CD) rotated 90 scaled 1/2d3);
o2 := (unitvector(DB) rotated 90 scaled 1/2d3);
E := whatever [D shifted o1, C shifted o1]
= whatever [D shifted o2, B shifted o2]; % pulley's center
a := angle(AD);
support := A shifted (-w, 0) -- B shifted (w, 0) -- B shifted (w, h) -- A shifted (-w, h) -- cycle;
draw woodenThing(support, 0); % wood block everything is suspended from
draw pulley (d1, a - 90) shifted E; % the pulley
draw image(
draw A -- D -- B withpen thickpen;
draw D -- C withpen thickpen;
) maskedWith (pulleyOutline shifted E); % ropes should be covered with the pulley
draw sphere.c(d2) shifted C shifted (0, -1/2d2); % sphere as a weight
dotlabel.llft(btex $A$ etex, A);
dotlabel.lrt(btex $B$ etex, B);
dotlabel.ulft(btex $C$ etex, C);
label.llft(btex $l$ etex, 1/2[A, D]);
)
enddef;
beginfig(18);
draw lHopitalPulley (6, 2, 11/2); % now you can choose the right parameters
draw lHopitalPulley (3, 5/2, 3) shifted (8cm, 0);
endfig;



那教科书呢? las,当几乎所有的插图和布局都准备好了时,发生了什么事,教科书被取消了。 也许因为这个原因,我决定从头开始重写原始库的大多数功能(我选择不使用任何原始代码,尽管这是间接的,但我付了钱) 并将其放在GitHub上 。 原始库中存在一些内容,例如绘制汽车和拖拉机的功能,我没有在其中包括,还添加了一些新功能,例如打结。

它运行不很快:在我的i5-4200U 1.6 GHz笔记本电脑上使用LuaLaTeX制作本文的所有图片大约需要一分钟。 这里到处都使用伪随机数生成器,因此没有两张相似的图片是绝对相同的(这是一个功能),并且每次运行都会产生略有不同的图片。 为了避免意外,您可以简单地设置randomseed := some number ,每次运行都享受相同的结果。

非常感谢ord博士Mikael Sundqvist对本文英文版的帮助。

Source: https://habr.com/ru/post/zh-CN454376/


All Articles