美文网首页
MetaFun 01:涂鸦

MetaFun 01:涂鸦

作者: 明夷 | 来源:发表于2021-05-09 21:42 被阅读0次

    有了画布,就可以涂鸦。画布是代码,涂的也就是代码,确切地说,是 MetaPost 代码,然而更确切地说,是 ConTeXt 里的 MetaPost 代码。ConTeXt 里的 MetaPost,就是 MetaFun。

    原点

    最简单的鸦,是原点:

    \environment card-env
    \startuseMPgraphic{绘图代码}
      draw (0, 0) withpen pencircle scaled 4pt withcolor darkgreen;
    \stopuseMPgraphic
    
    \starttext
    \startstandardmakeup[align=middle]
    \strut\canvas{画布}\strut
    \stopstandardmakeup
    \stoptext
    

    排版结果为

    上述示例代码里的

    draw (0, 0) withpen pencircle scaled 4pt withcolor darkgreen;
    

    可解读为:画(draw)一个点,它的坐标为 (0, 0),用 4pt 粗的圆头笔(pencircle)来画,颜色为暗绿色(darkgeen)。这是非常典型的英文句式,只是省略了主语「我」。

    上述 MetaPost 语句,可拆为两句:

    pickup pencircle scaled 4pt;
    draw (0, 0) withcolor darkgreen;
    

    亦即,抓起一支画笔,画一个点。

    更多的点

    抓起的画笔可以一直用下去,直到抓起另一支画笔。

    例如,

    pickup pencircle scaled 4pt;
    draw (0, 0) withcolor darkgreen;
    draw (5mm, 0) withcolor darkgreen;
    draw (0, 5mm) withcolor darkgreen;
    draw (-5mm, 0) withcolor darkgreen;
    draw (0, -5mm) withcolor darkgreen;
    

    鸦就变为

    在 MetaPost 语言里,点坐标就是序对(pair),而且可以给点坐标取名字:

    pair O, a, b, c, d;
    O := (0, 0);
    a := (5mm, 0); b := (0, 5mm); c := (-5mm, 0); d := (0, -5mm);
    pickup pencircle scaled 4pt;
    draw O withcolor darkgreen;
    draw a withcolor darkgreen;
    draw b withcolor darkgreen;
    draw c withcolor darkgreen;
    draw d withcolor darkgreen;
    

    在 MetaPost 语言里,有名字的东西,就是变量,例如笔是变量,颜色是变量,点是变量。:= 表示给变量赋值——赋以意义。

    迭代

    上一节最后的代码有些繁冗,可使用 MetaPost 的迭代语法予以简化:

    pair O, a, b, c, d;
    O := (0, 0);
    a := (5mm, 0); b := (0, 5mm); c := (-5mm, 0); d := (0, -5mm);
    
    pickup pencircle scaled 4pt;
    for i = O, a, b, c, d:      
      draw i withcolor darkgreen;
    endfor;                      
    

    上述代码里,5mm 被重复使用多次,倘若需要将其修改为 4mm,不得不对代码做较多改动,因此应当为 5mm 也取个名字:

    numeric u; u := 5mm;
    pair O, a, b, c, d;
    O := (0, 0);
    a := (u, 0); b := (0, u); c := (-u, 0); d := (0, -u);
    pickup pencircle scaled 4pt;
    for i = O, a, b, c, d:      
      draw i withcolor darkgreen;
    endfor;                      
    

    路径

    欧几里得说,两点之间,线段最短。MetaPost 也遵守这样的几何学。下面的代码,除了可以画 5 个点之外,还可以画穿过它们的 2 条线段:

    numeric u; u := 5mm;
    pair O, a, b, c, d;
    O := (0, 0);
    a := (u, 0); b := (0, u); c := (-u, 0); d := (0, -u);
    pickup pencircle scaled 4pt;
    for i = O, a, b, c, d:      
      draw i withcolor darkgreen;
    endfor; 
    
    path ac, bd;
    ac := a -- c; bd := b -- d;
    pickup pencircle scaled 2pt;
    for i = ac, bd:
      draw i withcolor darkgreen;
    endfor;
    

    结果为

    组合与变换

    表面上,我现在的涂鸦作品是一个十字图形,但是在代码里,它还是一团散碎——一些点,一些线条……难以像鸦那样在画布里飞翔。MetaPost 语言深深意识到了这个问题的严重性,为了搞好涂鸦工作,就像提供 pairpath 这样的变量类型之外,也提供了 picture 类型。

    现在,试着将我的作品合成为 picture

    numeric u;
    u := 5mm;
    pair O, a, b, c, d;
    O := (0, 0);
    a := (u, 0); b := (0, u); c := (-u, 0); d := (0, -u);
    pickup pencircle scaled 4pt;
    for i = O, a, b, c, d:      
      draw i withcolor darkgreen;
    endfor;
    
    path ac, bd;
    ac := a -- c; bd := b -- d;
    pickup pencircle scaled 1pt;
    for i = ac, bd:
      draw i withcolor darkgreen;
    endfor;
    
    picture darkgreencrow;
    darkgreencrow := currentpicture;
    

    上述代码的重点在于最后两行。我创建了一只名字叫暗绿色的乌鸦(darkgreen crow),然后将当前涂鸦的结果 currentpicture 赋予了它。

    这只乌鸦能飞吗?

    能!

    请看,

    draw darkgreencrow shifted (2.75u, 0);
    draw darkgreencrow shifted (-2.75u, 0);
    draw darkgreencrow shifted (0, 2.75u);
    draw darkgreencrow shifted (0, -2.75u);
    

    结果是一个阵势:

    也可以让它飞起来的样子别那么严肃:

    draw darkgreencrow rotated 15 shifted (2.75u, 0);
    draw darkgreencrow rotated 30 shifted (-2.75u, 0);
    draw darkgreencrow rotated 45 shifted (0, 2.75u);
    draw darkgreencrow rotated 60 shifted (0, -2.75u);
    

    注意,乌鸦从起飞位置,先旋转(rotated)再移动(shifted),与先移动再旋转,是两回事,后者是下面这样:

    draw darkgreencrow shifted (2.75u, 0) rotated 15;
    draw darkgreencrow shifted (-2.75u, 0) rotated 30;
    draw darkgreencrow shifted (0, 2.75u) rotated 45;
    draw darkgreencrow shifted (0, -2.75u) rotated 60;
    

    之所以如此,是因为旋转需要一个中心,这个中心是原点。移动前绕原点旋转,跟移动后再绕原点旋转,自然是有着很大不同。

    image 宏

    那个作为 currentpicture 的鸦,能不能也像后来这 4 只呢,表现为 picture。大家都是鸦,理应有一致性的本质。答案是,能,但是需要用 MetaPost 的 image 宏。下面重新画一次原始鸦:

    picture darkgreencrow;  
    
    numeric u;
    u := 5mm;
    pair O, a, b, c, d;
    O := (0, 0);
    a := (u, 0); b := (0, u); c := (-u, 0); d := (0, -u);
    darkgreencrow := image (
      pickup pencircle scaled 4pt;
      for i = O, a, b, c, d:      
        draw i withcolor darkgreen;
      endfor;
    );
    
    path ac, bd;
    ac := a -- c; bd := b -- d;
    addto darkgreencrow also image (
      pickup pencircle scaled 1pt;
      for i = ac, bd:
        draw i withcolor darkgreen;
      endfor;
    );
    
    draw darkgreencrow;
    

    上述代码的最后一句,真正地画出了原始鸦。可通过在实践里意会的办法理解 image(...) 以及addto ... also ... 语法。

    有了上述重新定义的 darkgreencrow 之后,可以再用 rotatedshifted 让它飞:

    draw darkgreencrow rotated 15 shifted (2.75u, 0);
    draw darkgreencrow rotated 30 shifted (-2.75u, 0);
    draw darkgreencrow rotated 45 shifted (0, 2.75u);
    draw darkgreencrow rotated 60 shifted (0, -2.75u);
    

    上述 4 句 draw darkgreencrow ...,有很多重复的代码,可再用 for 简化一番呢?

    试试看,

    numeric n; n := 0;
    for i = (2.75u, 0), (-2.75u, 0), (0, 2.75u), (0, -2.75u):
      n := n + 15;
      draw darkgreencrow rotated n shifted i;
    endfor; 
    

    可行!

    能否将

    draw darkgreencrow;
    

    也合并到上述 for 语句里呢?没有什么不可以的,

    numeric n; n := 0;
    for i = (0, 0), (2.75u, 0), (-2.75u, 0), (0, 2.75u), (0, -2.75u):
      draw darkgreencrow rotated n shifted i;
      n := n + 15;
    endfor;
    

    数组和另一种 for

    MetaPost 允许将它支持的任何类型的数据存放在数组里……是不是很像 Lua 表?

    可将旋转角度值存放在数组里,

    numeric angles[];
    for i = 0 upto 4:
      angles[i] := 15 * i;
    endfor;
    

    上述代码里有另一种 for……意会吧!

    也可以将点存到数组里,

    pair points[];
    points[0] := (0, 0);
    points[1] := (2.75u, 0);
    points[2] := (-2.75u, 0);
    points[3] := (0, 2.75u);
    points[4] := (0, -2.75u);
    

    有了上述数组,与上一节最后的代码等价的代码如下:

    for i = 0 upto 4:
      draw darkgreencrow rotated angles[i] shifted points[i];
    endfor;
    

    扰动

    ConTeXt 里的 MetaPost(确切地说,是 MetaFun)有 randomized 宏,它可以让涂鸦的结果不那么精确。不精确的结果,似乎能有机会跻身艺术殿堂。

    下面,重新给出一份完整的涂鸦作品,亦即一份 ConTeXt 源文件 foo.tex,其内容如下:

    \environment card-env
    \startuniqueMPgraphic{绘图代码}
    picture darkgreencrow;
    numeric u;
    u := 5mm;
    pair a, b, c, d;
    a := (u, 0); b := (0, u); c := (-u, 0); d := (0, -u);
    path ac, bd;
    ac := a -- c; bd := b -- d;
    darkgreencrow := image (
      pickup pencircle scaled 4pt;
      draw ac withcolor darkgreen;
      draw bd withcolor darkgreen;
    );
    draw darkgreencrow;
    \stopuniqueMPgraphic
    
    \starttext
    \startstandardmakeup[align=middle]
    \strut\canvas{画布}\strut
    \stopstandardmakeup
    \stoptext
    

    对应的排版结果为

    现在,将上述代码里负责涂鸦的代码的最后一句

    draw darkgreencrow;
    

    修改为

    draw drakgreencrow randomized .5u;
    

    其中,randomized .25u 的作用是,让 darkgreencrow 这个 picture 里的内容在 [-0.25 * 5mm, 0.25 * 5mm] 这个区间内随机晃动,结果如下图所示,

    在类似于如上图所示的随机扰动的图形之后,倘若执行

    $ context --purgeall
    

    清除 ConTeXt 的 TeX 编译器为 foo.tex 生成的中间文件,然后再次执行

    $ context foo
    

    可以得到新的随机图形。之所以如此,是因为 ConTeXt 的 TeX 编译器在处理含有随机扰动图形的源文件时,会在扩展名为 .tuc 的文件里存放一个随机数的种子。倘若删除 .tuc 文件,ConTeXt 的 TeX 编译器会再次生成新的 .tuc 文件,里面会包含新的随机数种子。基于不同的随机数种子,可以生成不同的图形。

    如果让这只随机的原始鸦的涂画语句换成以下代码,可能会让画面出现些许艺术气息:

    numeric angles[];
    for i = 0 upto 4:
      angles[i] := 15 * i;
    endfor;
    pair points[];
    points[0] := (0, 0);
    points[1] := (2.75u, 0);
    points[2] := (-2.75u, 0);
    points[3] := (0, 2.75u);
    points[4] := (0, -2.75u);
    
    for i = 0 upto 4:
      draw darkgreencrow randomized .25u rotated angles[i] shifted points[i];
    endfor;
    

    这五只鸦凌空飞舞的画作如下:

    倘若再大胆一些,可以创造群鸦齐飞的盛况:

    对应的绘图代码如下:

    for i = 0 upto 50:
      draw darkgreencrow randomized .5u
           rotated (0 randomized 60)
           shifted ((0, 0) randomized (OverlayWidth, OverlayHeight));
    endfor;
    

    其中,(0 randomized 60) 可为 rotated 生成一个位于 [-30, 30] 这个区间的随机角度值。为什么要用括号将随机值括起来呢?因为不将 randomized 放在括号内,TeX 编译器会认为 randomized 修饰的是 draw 要绘制的对象。

    上述代码里的

    ((0, 0) randomized (OverlayWidth, OverlayHeight))
    

    可为 shifted 生成一个随机的点,这个点的横坐标的取值区间是 [-0.5 * OverlayWidth, 0.5 * OverlayWidth],纵坐标的取值区间是 [-0.5 * OverlayHeight, 0.5 * OverlayHeight]。

    OverlayWidth 和 OverlayHeight 是什么?它们是画布的长度和宽度值啊,还记得画布的定义么?

    \defineoverlay
      [foo]
      [\uniqueMPgraphic{绘图代码}]
    
    \defineframed
      [canvas]
      [width=\textwidth,
        height=\textheight,
        framecolor=darkred,
        rulethickness=.25em,
        location=lohi,
        empty=yes,
        background=foo]
    

    这个叫 foo 的 overlay,它的宽度和长度在 ConTeXt 里对应的值分别是 \overlaywidth\overlayheight。当 foo 作为框框的背景时,框框的长度和宽度便是 foo 的宽度和长度。

    我原本也不知道 randomized 后面跟随一个 numeric 类型的值或者跟随一个 pair 类型的值,它产生的扰动范围是多大,但是根据上述示例的取值,再结合群鸦齐飞的结果,便推断出来了。

    五颜六色的鸦

    随机扰动,也能修改颜色值。在 MetaPost 语言里,一个颜色,就是三元组。例如,

    color foo;
    foo := (0, .625, 0);
    

    foo 实际上就是 darkgreen,只不过后者是 ConTeXt 已定义的颜色值而已。

    white 也是已定义的颜色值,它的值是 (1, 1, 1)。倘若 whiterandomized 扰动,就可以生成一种不可预知的颜色:

    white randomized (1, 1, 1);
    

    randomized 后面跟随三元组的时候,它不会生成负数,而是生成一个随机的三元组,其中每个元素的值是 [0, 1] 区间内的小数。当 randomized 前面是颜色值的时候,它将随机三元组的值分别与颜色分量相乘。这是我的猜测……我没在 MetaFun 手册里看到 Hans 对此的解释。

    现在试试用随机颜色涂鸦:

    for i = 0 upto 50:
      draw darkgreencrow randomized .5u
           rotated (0 randomized 60)
           shifted ((0, 0) randomized (OverlayWidth, OverlayHeight))
           withcolor (white randomized (1, 1, 1));
    endfor;
    

    结果为

    结语

    也许画画不好的人,才会喜欢 MetaPost 吧。MetaFun,出郭相扶将。

    相关文章

      网友评论

          本文标题:MetaFun 01:涂鸦

          本文链接:https://www.haomeiwen.com/subject/mawudltx.html