Lil中的矢量图形
摘要
本文探讨了Lil脚本语言在二维矢量图形中的应用,解释了如何表示点、笔划和路径,以及如何使用Decker生态系统中的Hershey矢量字体来操作它们。
暂无内容
查看缓存全文
缓存时间: 2026/06/24 10:50
## Lil中的矢量图形
艾伦·文森特·赫西博士(Dr. Allen Vincent Hershey)开发了最早的数字字体之一,这些字体由简单的直线段序列描述。它们最初发表在他1967年的报告《Calligraphy for Computers》[1](http://beyondloom.com/blog/vectorgraphics.html#fn:1)中。
Decker生态系统中的 `hershey` 模块 [2](http://beyondloom.com/decker/hershey.html) 专注于使用这些字体进行文本排版。Decker 的脚本语言 Lil [3](http://beyondloom.com/decker/index.html) 是一种受APL影响的独特设计。本文将围绕二维矢量图形探讨 Lil,并扩展上述交互式文档中讨论的示例。
## 基础
一个**点**是一个 `(x,y)` 数字对,描述**画布**(即绘图表面)上的直角坐标。点在 Lil 中表示为长度为2的列表:
```
point:(3,-5)
```
一个**笔画**是一个点的列表,代表要在画布上绘制的一系列直线,因此也是一个数字列表的列表。给定四个点,笔画将第一个点连接到第二个,第二个点到第三个,第三个点到第四个。如果一个笔画中有 *N* 个点,则它描述 *N-1* 条连接的直线。我们也可以将笔画称为“折线”:
```
stroke:((list 3,-5),(list 2,5),(list 0,3))
```
一个**路径**是笔画的列表,因此也是点的列表的列表,以及数字的列表的列表的列表。一个路径可以表示构成单个形状(例如字母“A”)的笔画,或者表示整个单词或句子的一系列字母。路径中的笔画是**参差的**:它们的长度可能不同。
```
path:(list (list 1,2),(list 3,4)), (list (list 5,6),(list 7,8),(list 9,10))
```
[`hershey.textpath[]`](http://beyondloom.com/decker/hershey.html#hershey_textpath) 函数根据 Lil 字符串使用字体的字形组装一个复杂的路径,每个字形本身就是一个更简单的路径:
```
p:hershey.textpath[hf_futura "ABC"]
```
构建完成后,我们可以使用显式的 `each` 循环或等效的简写 `@` 运算符将文本路径绘制到画布 `c` 上。Decker 的画布小部件 [4](http://beyondloom.com/decker/decker.html#canvasinterface) 可以使用 `canvas.line[]` 函数绘制笔画,只需提供一个合适的数字列表的列表:
```
each stroke in p c.line[stroke]endc.line @ p
```
只要可能,Decker 的脚本 API 都被设计为**集体操作**,一次作用于大型数据结构。绘制单个直线段是更一般的折线绘制操作的**简单情况**。如果 `canvas.line[]` 只能绘制单个直线段,我们就需要显式管理路径中 *N* 个点与它们所代表的 *N-1* 条线之间的差一关系。例如,
```
each stroke in p each point i in stroke if i>0 canvas.line[stroke[i-1] point] end endend
```
像这样微小的可用性缺陷如果出现在整个程序中会被放大,从而掩盖了程序员实际意图的简洁性。[^1]
## 路径操作
现在我们已经能够获取并渲染有趣的路径,来看看如何操作它们。
组合路径很容易:Lil 的 `,` 运算符可以拼接列表。将一个笔画列表与另一个笔画列表拼接,结果就是一个笔画列表。
缩放路径更有趣。正如[之前讨论过](http://beyondloom.com/blog/conforming.html)的,Lil 的许多算术运算符会隐式地*自适应*到嵌套的列表结构上。将一个标量数字乘以一个数字列表,会将乘法*扩散*到列表的每个元素,同样的过程对嵌套列表也递归有效。因此,我们可以通过简单的乘法来统一缩放点、笔画或路径[^2]:
```
(11,22,33)*2# (22,44,66)((list 3,-5),(list 2,5),(list 0,3))*2# ((6,-10),(4,10),(0,6))
```
非均匀缩放会遇到问题。现在我们希望将路径中每个点的 x 坐标与 y 坐标乘以不同的值。如果我们将路径乘以一对数字,`*` 运算符并不会隐式地知道我们希望将这种自适应过程“推入”到最内层的数字对。当 `*` 左右两侧的列表长度不同时,它会将右侧列表视为被重复或截断以匹配左侧列表的长度:
```
(1,2,3,4)*(10,100)# (10,200,30,400)
```
如果我们先用 `list` **括起**右操作数,`*` 会重复该长度-1列表中的元素,并将其与左参数的每个元素配对:
```
(1,2,3,4)*list(10,100)# ((10,100),(20,200),(30,300),(40,400))
```
对于路径,我们需要将右操作数括两次:一次让缩放扩散到路径的每个笔画,然后再一次让缩放扩散到笔画的每个点:
```
p * list list scalex,scaley
```
将 x 轴或 y 轴缩放为负数,会分别水平或垂直镜像路径:
```
p * list list -1,1
```
平移路径可以使用与缩放相同的技巧,但用 `+` 代替 `*`:
```
p + list list transx,transy
```
要进行水平剪切,我们需要将路径中每个点的 x 坐标按 y 坐标成比例偏移。我们可以使用 `last` 原语来提取路径中所有点的 y 坐标。`@` 运算符允许我们将 `last` “推入”到右操作数中。路径的 `last` 是一个笔画。每个笔画的 `last` 是一个点的列表。每个路径的`last`的`last`是一个 y 坐标的列表的列表:
```
p: (list (list 1,2),(list 3,4)),(list (list 5,6),(list 7,8),(list 9,10))last p# ((5,6),(7,8),(9,10))last @ p# ((3,4),(9,10))last @ @ p# ((2,4),(6,8,10))
```
我们将这种从路径点中派生出的标量列表的列表称为一个**剥离量**。
要将 y 坐标剥离量转回完整的路径,我们可以将每个标量乘以一对数字,重用我们的非均匀缩放惯用法:
```
(last @ @ p)*list list(10,0)# (((20,0),(40,0)),((60,0),(80,0),(100,0)))
```
因此,完整的水平剪切为:
```
p + (last @ @ p)*list list shearx,0
```
垂直剪切看起来非常相似:
```
p + (first @ @ p)*list list 0,sheary
```
如果你想转置一个路径,可以分别剥离出 x 和 y 坐标,然后使用同样的惯用法将它们混合回去,但你也可以直接使用 `rev`(反转)原语来交换每个点的元素:
```
rev @ @ p
```
单独的转置并不常用,但结合镜像和转置可以产生90度旋转。
更通用的旋转路径方法是利用 Lil 的 `heading`、`mag` 和 `unit` 原语。
`mag` 原语计算一个数字或数字列表的向量模长。对于单个数字,这就是绝对值。对于一对数字(比如一个点),它给出该点到原点的欧几里得距离。它还有特殊的自适应行为:它会递归地深入嵌套列表,直到遇到一个数字或扁平的数字列表。这种行为使其非常适合从路径生成剥离量:
```
p: (list (list 1,2),(list 3,4)),(list (list 5,6),(list 7,8),(list 9,10))mag p# ((2.236068,5),(7.81025,10.630146,13.453624))
```
`heading` 原语像 `mag` 一样自适应,但会产生一个角度(弧度)的剥离量,表示原点到每个点的方向:
```
heading p# ((1.107149,0.927295),(0.876058,0.851966,0.837981))
```
结合使用,`mag` 和 `heading` 可以将路径(直角坐标)分解为极坐标。`unit` 原语将从 `heading` 获得的角度的剥离量转换成一个路径,其中每个点距离原点的距离都是**单位距离**(`mag` 为1)。对于任何路径 `p`[^3],
```
p ~ (mag p)*unit heading p
```
因此,绕原点旋转 `angle` 弧度的完整路径旋转为:
```
(mag p)*unit angle+heading p
```
像 `unit` 这样的原语也提供了无循环的方式来构造各种笔画,如正多边形、圆弧和椭圆。用 `list` 括起结果即可得到一个路径:
```
on ngon sides radius do list radius*unit 2*pi*(range sides+1)/sidesend
```
这完成了路径仿射变换的一组 Lil 惯用法。
## 复杂效果
我们还能做什么?
如果我们从一个关于原点中心对称的路径开始,我们可以用每个点自身的 `mag` 来缩放该点,从而产生透视扭曲。给定一个在0到1之间变化的补间值 `t` 和几个适当选择的常数:
```
p*.3+(mag p)*.01*t
```
如果我们生成一个由随机[-1,1]偏移量组成的笔画,我们可以将它添加到路径中的每个笔画上,从而产生一种粗略的线条抖动效果。随机笔画的长度无关紧要,因为它会被 `+` 自动截断或扩展,但让它的点数大致等于路径中最大笔画的点数,结果会看起来更好一些。假设25个点:
```
p+list 2 window random[3 2*25]-1
```
用多个偏移正弦波来偏移路径的点会产生一种不同的波动效果,更像旗帜在风中飘动或液体表面荡漾。为了分别操纵每个轴,我们将使用与剪切效果类似的惯用法。在这个例子中,`f` 是一个持续递增的帧计数器,比如 `sys.frame`:
```
px:first @ @ p((sin(f/4)+.20*px)*list list 1,0)+((sin(f/5)+.30*px)*list list 0,1)+p
```
将更多正弦波混合到两个轴中,可以获得更混乱、更微妙丰富效果:
```
px:first @ @ ppy:last @ @ p((sin(f/4)+.20*px)*list list 1.5,0.6)+((sin(f/5)+.30*px)*list list 0.6,0.2)+((cos(f/7)+.05*py)*list list 0.0,1.2)+((cos(f/2)+.04*py)*list list 0.1,0.2)+p
```
这些技术只是 Lil 几行代码所能实现的能力的冰山一角!
何不亲自尝试一下 `hershey` 模块交互式文档中的示例呢?[5](http://beyondloom.com/decker/hershey.html#path_wave)
[返回](http://beyondloom.com/blog/index.html)
---
[^1]: http://beyondloom.com/blog/vectorgraphics.html#fn:1
[^2]: http://beyondloom.com/blog/vectorgraphics.html#fn:2
[^3]: http://beyondloom.com/blog/vectorgraphics.html#fn:3
相似文章
Hershey是一种文本化的矢量字体格式
本文介绍了Hershey矢量字体格式,这是一种使用坐标编码系统的字形文本表示法,其中字母映射到有符号值,并包含示例和相关资源链接。
Show HN:一个ASCII 3D渲染引擎
GlyphCSS是一个JavaScript库,它使用ASCII字符在DOM中渲染带纹理的3D网格,支持多种3D格式,并与原生JS、React和Vue集成。
Granite 4.1 3B SVG 鹈鹕画廊
IBM 在 Apache 2.0 许可下发布了 Granite 4.1 系列 LLM,Simon Willison 尝试使用该 3B 模型的 21 种不同量化变体生成骑自行车的鹈鹕 SVG 图像。
@geekbb: pi + DeepSeek 画的,才发现这个技能不需要生图模型,是通过 LLM 将自然语言描述转为结构化 JSON → Node.js 渲染器用纯几何算法生成 SVG → 注入自包含 HTML。 https://github.com/tt…
介绍 Archify,一个 Claude Skill,可将自然语言描述转化为结构化 JSON,再通过 Node.js 渲染器生成纯几何算法 SVG,注入自包含 HTML,支持多种技术图表和导出格式。
用NSString替代Photoshop (2015)
作者讨论了在Cocoa中使用NSString和CoreGraphics以编程方式绘制简单矢量图形来替代Photoshop的方法,阐述了其中的好处与挑战,同时推广了自己开发的app Findings。