Florilegium:让 LaTeX 的每一页长出一座花园
一个用 Lua 现算的 LaTeX 宏包,按页面剩下的空间长出代尔夫特蓝的花草,每一页都不一样。
中文
复制

缘起
Reddit 帖子问的是如何在 LaTeX 里做出这样的花纹。原图是怎么做的,我并不清楚,这个项目也没打算复刻它。它只是其中一种做法,也是我的做法,手工编写图元,参数随机化,用 Lua 绘制,再由一套程序化布局摆放位置;其中“填满空白区域”这一步借用了 1998 年装饰纹样文献里的一个思路。最终效果与原图相近。
这是什么
为 LaTeX 程序化生成的代尔夫特蓝花卉页边。不用图片,不导入路径,不依赖外部资源。每一片花瓣、每一片叶子、每一根茎,都是页面写入过程中由 Lua 计算出的参数曲线,以水彩的方式绘制,直接写进 PDF。每一页都有自己的花园。

同一生长构图的六个种子。代码相同,种子不同,文字留出多少空间,花园就填满多少。
两份文档共用一个包,另外两份(showcase.tex、spread.tex)展示了它还能做什么。
florilegium.tex | florilegium-grow.tex | |
|---|---|---|
| 构图 | 手工编排,茎、叶、花逐一放置,每个参数都经过采样 | 生长而成,一株植物由贪心式填空循环生长,直到填满页面 |
| 适应对象 | 无(按 A4、68 mm 页边距调好) | 页面布局,以及末页正文下方的空白区域 |
两份 PDF 都自带说明,正文讲的就是页边距是怎么画出来的。


300 dpi 裁切图。斑驳的晕染,其间提亮出白色条纹;具裂片的叶子带着白色叶脉,勿忘我的聚伞花序,茎干后面一朵浅淡的幽灵花。
看它生长


种子 6,从空白页边到成稿页面。茎先立起来,叶子在尖端越过节点时展开,每根茎的末端都开出一朵花。接着贪心循环接管。右侧每个虚线圆都是它找到的最大空白圆盘,就在一条枝干伸向它之前。大洞先出现,来得慢;小洞最后出现,来得快。蝴蝶和蜜蜂飞进来,鸟滑落到枝头,蜗牛沿着茎向上爬,直到最后一帧,那正是这个种子每次都会给出的页面。
生长过程只记录一次,就在页面排版的时候。每根茎还会以逐渐加长的前缀形式绘制,每个图案也会在几个较小的尺度上绘制,重放同样的随机抽取。然后每一帧按原本的绘制顺序,把时间已到的片段写出来。make grow-6 用这种方式渲染一个种子(build/grow-6.gif 和 .mp4,约 45 秒);加上 EXPLAIN=1 可显示虚线圆。
页边读取文本

showcase.tex,“页边的一年”,第一页是春,然后是夏,最后一页是秋。每一节描述它所展示的那个特性。
长成的页边能对周围的文档作出回应。
- 首字下沉。
\florilegiuminitial[5]{E}把段落的首字母排进一个五行高的方块里。字母的轮廓从 OpenType 字体中读取(默认是 Latin Modern Bold),像花瓣一样绘制出来;一条藤蔓依次从笔画上方和下方穿过,能放叶子的地方长叶子,空隙里开花,花常常开在字母的封闭空间里。这个字母同时还会被隐形地排版一遍,所以复制段落时得到的仍然是“Every”。 - 标题旁的花。 加上选项
sections,每个带编号的章节和小节都会在标题旁的页边得到一朵花,用强调色所用的铜色绘制,就像从前用红色标出标题那样。 - 文字要求处开花。
\florilegiumbloom会在它所在的那一行旁边放一朵花;\florilegiumbloom[lily]则要求花园里的任意一种图案。这些标记在页面输出时于页面盒中查找,因此一次运行即可生效。 - 四季。 加上选项
aging,花园会从第一页的春天(大多是花苞、小而浅的叶子)渐渐走到最后一页的秋天(种穗、飘落的花瓣、几片铜色的叶子)。这需要运行两次,因为要先知道页数;\florilegiumage{0.8}则改为手动设定一个季节。


春、初夏、晚夏、秋各一粒种子。四季改变的是长什么,不是长在哪。
访客

大多数页面都有访客。开阔处有一两只蝴蝶和蜜蜂,一只蜗牛沿着某根主茎往上爬,大约每五页里有一页,还会有一只鸟停在自己的细枝上。它们来自各自独立的随机流,所以没有这些访客的页面,和原本的页面一模一样;novisitors 选项会把它们略去。
任意墨水

整套配色都出自同一个颜色 ink,代尔夫特蓝、棕褐、绿、绯红、紫罗兰、青绿。从最深的笔触到最淡的渲染,这一整条色阶都在 OKLCH 中由该色相算出,因此浅色一端永远不会发灰。
主题

生长循环并不知道自己在画什么。它读取一张图案表(每个图案占据的圆盘大小、权重、绘制函数),然后按名字调用每一个图元。一个主题就是一个 Lua 文件 theme-<name>.lua,它替换掉其中一些图元,也可以往表里添加行,或者自带一套构图。
theme=geometric 是用平面形状剪出来的同一株植物(theme-geometric.lua)。它的生长方式和那些花完全一样,变的只是绘制。茎在节点之间走直线,叶子是风筝形和带齿的多边形,折成明暗两个面,叶脉是笔直的白色线条,花是菱形拼成的花结或层叠的星形多边形,卷须是方形螺旋,鸟和蝴蝶是低多边形。颜色是墨的平涂色调,没有渲染;晶体(柏拉图立体,像切割宝石那样平面着色)在花丛间生长。一页的大小大约是花卉主题页面的十分之一。


theme=math 根本不是植物(theme-math.lua,取自 figures.lua)。数学沿着页边倾泻而下,横贯页面底部,彼此略微重叠,融为一体。角落处、即栏与底边相接的地方,放着一个大图形(Apollonian 圆堆、深一层的 Koch 雪花、Vogel 圆盘、一个多面体)。沿栏向下排布着一两个族,图形在行进中不断变化,由小小的铜色箭头相连,Koch 雪花多出一级,多边形在通往圆的路上增加边数,立方体截角成八面体,弦 k → mk mod n 从心脏线变为肾形线,Vogel 圆盘只在黄金角下才均匀铺满。成立的等式(e^{iπ} + 1 = 0、V − E + F = 2、……)沿底部延伸,并在最后一页钻到文字下方;其余的一切,从最大的空洞开始,都是图形(柏拉图立体、纽结、Hilbert 曲线、Lissajous 曲线、内摆线、Maurer 玫瑰、Petersen 图、……)、由 TeX 以多种字号并略微倾斜排出的字母、数字和符号,以及点。它填满页面留下的任何空间,就像花园一样;density 决定填多少。

\usepackage[grown,theme=geometric]{florilegium} % or theme=math; the whole document
\florilegiumtheme{math} % or from the page this stands on
不带主题的文档,输出结果与主题功能出现之前逐字节相同。
页边更少

一整座花园放在公式旁边,实在太多了。有两种办法可以把它调小,任何主题都适用。
density=0.4让图案长得更少,循环在更大的孔洞处就停下,每个图案周围保留更多纸面,垂下的叶子和填充物也更少。把旋钮调到最低,植物本身就会变稀疏,先是两根主茎,然后只剩一根,页脚也没有了;调到0.05时,只剩一根藤蔓带着它的花、几片叶子和一两簇花枝。配合数学主题,就是东西更少。intensity=0.5把整条页边都涂得更淡,和花园背后那些幽灵花是同一种画法。它只改颜色,不改细节。
\usepackage[grown,density=0.4,intensity=0.6]{florilegium}
在默认值下(1 和 1),每一页都和之前完全一样。
正文里的一处装饰

也可以完全不留边距。\florilegiumornament{leaf} 在行内放置一个图元,装进一个字母大小的盒子里,比如用来开启一个段落。文档里的每个装饰图形都有自己的形状,并且从一次构建到下一次构建都保持不变;seed=random 每次生成 PDF 时都会重新绘制一个,seed=7 则绘制一个固定的。
\usepackage[nomargin]{florilegium} % ornaments only, no garden in the margin
\florilegiumornament{leaf} A paragraph that opens with a leaf.
\florilegiumornament[size=2.4em, ink=5B2A1F, angle=30, seed=random]{flower}
任何图元都可以使用(leaf、leaflet、flower、blossom、bud、bell、lily、cyme、umbel、butterfly、bee、bird、hedera、sparkle……),当前主题的图元也可以(crystal 配 geometric;knot、polyhedron、koch……配 math)。
页面的两侧

spread.tex,第 2、3 页,双面文档,外侧页边距较宽。花园在左手页上沿左侧生长,镜像后则在右手页上沿右侧生长。最后一页还会切换到 geometric 主题。
花园生长在较宽的那一侧页边距上,因此无需任何设置。在双面文档中(twoside、outer 的页边距较宽),它会随页面换边。单面文档若右侧页边距较宽,则每页的花园都位于右侧。镜像页面的排版如同从镜中看去,再经一次反射绘制而成,每个点本来就要经过这最后一次变换。它恰好就是同一颗种子在左侧长出的花园的镜像。
在 PostScript 中

postscript/florilegium.ps,纯 PostScript 绘制的生长边距,一个自包含文件。Ghostscript 或打印机在解释页面时计算每一片花瓣,两页文字说明了其中的原理。
绘图核心、每个图元以及生长循环都从 ink.lua、flora.lua 和 grow.lua 逐个过程移植而来。它不需要 TeX,也不需要其他文件。
gs -dALLOWPSTRANSPARENCY -sDEVICE=pdfwrite -dSeed=7 -o garden.pdf postscript/florilegium.ps
make postscript # the same -> build/postscript-7.pdf; SEED=12 for another garden
-dSeed=N 选择花园;不指定时由时钟挑选,因此每次打印都是新的。-dAge=0 到 1 设定季节,-dNoVisitors 则省去蝴蝶、蜜蜂、蜗牛和鸟。两页大约耗时 2.5 秒。Lua 中的大部分逻辑可以直接对应。PostScript 自带的变换矩阵负责放置每个图元(translate、rotate、scale),路径和贝塞尔曲线是原生支持的,运行时构建的过程则替代了 Lua 的闭包(花瓣的半宽就是一个传给轮廓构建器的过程)。有四处无法直接对应。
- 图层。 花园按一种顺序生长,却要从后往前绘制,因此每次填充和描边都用
pathforall捕获到按图层划分的列表中,最后再回放这些列表。 - 透明度。 PostScript 没有透明度。Ghostscript 通过
-dALLOWPSTRANSPARENCY提供。没有这个标志,或者在打印机上,每个半透明填充都会与它大概率会落在其上的颜色混合,然后以不透明方式绘制。结果更扁平,但仍是同一座花园。 - 随机性。 生成器是 32 位整数运算的 Park-Miller,而 PostScript 的实数只有单精度,所以这里的种子 7 与 LaTeX 中的种子 7 得到的是不同的花园。
- 文字。 文件用 Times 自行排版它的两页内容。它知道标题在哪里、正文到哪里结束,因此标题处的铜色花朵和最后一行下方的花园都能像在 LaTeX 中一样工作。它没有首字下沉、动画、主题、镜像页面、密度或强度、正文中的装饰,也没有人工编排的构图。

同一个文件两次发送到 Epson 喷墨打印机,使用 lp postscript/florilegium.ps,未指定种子。计算机的打印系统会为每个打印任务重新运行程序,因此每次打印都从时钟取种子,长出不同的花园。打印机拿到的是不透明版本,因为它没有透明度。
构建
LuaLaTeX,一遍编译,每页约一秒(showcase.tex 需要两遍,因为它的季节依赖页数)。
make # the four PDFs
make seed-7 # one page from seed 7 -> build/seed-7.pdf
make gallery # gallery/seeds.png + seeds.gif, inks.png, seasons.png, themes.png, dials.png (python3, Pillow, pdftoppm)
make grow-6 # growth animation of seed 6 -> build/grow-6.gif + .mp4 (ffmpeg)
make grow-6 EXPLAIN=1 # the same with the free discs marked -> build/grow-6-explain.*
没有安装 TeX?构建一次镜像,然后在其中运行所有内容。
docker build -t florilegium-tex tools
make RUN=tools/in-docker
或者将 florilegium.sty、九个 .lua 文件以及其中一个 .tex 文件粘贴到 Overleaf 项目中,并将编译器设为 LuaLaTeX。
在自己的文档中使用。
\usepackage[grown]{florilegium} % or plain \usepackage{florilegium} for the authored one
% options for the grown one: sections (flowers at the headings), aging (two runs),
% novisitors (no butterflies, bees, snails or birds); for both: theme=geometric or theme=math,
% density=0.4 (grown only), intensity=0.6
% two-sided? nothing to set: the garden grows in the wider side margin
\definecolor{ink}{HTML}{5B2A1F} % optional: the hue of the whole palette (this is sepia)
\definecolor{accent}{HTML}{B4662F} % optional: the copper of the hairlines
...
\florilegiuminitial[4]{T}his paragraph starts with a decorated T.
... a flower in the margin beside this line\florilegiumbloom ...
\florilegiumage{0.8} % grown only: autumn from here on
\florilegiumtheme{geometric} % another theme, from the page this stands on
\florilegiumornament{leaf} % one leaf in the text, the size of a letter
\florilegiumtextend % grown only: at the very end of the text
用固定种子代替页码:lualatex "\def\florilegiumseed{7}\input{florilegium-grow.tex}"。
任何图元都可以在 TikZ 图片中单独绘制,遵循当前作用域的平移、旋转和缩放。
\begin{tikzpicture}
\begin{scope}[shift={(2,2)},rotate=20]\florilegiumdraw{flower}{1.3}\end{scope}
\begin{scope}[shift={(5,0)}]\florilegiumdraw{leaf}{3.0, {depth=0.6}}\end{scope}
\begin{scope}[shift={(8,1)}]\florilegiumdraw{ghost}{"lily", {s=1.4}}\end{scope}
\begin{scope}[shift={(11,1)}]\florilegiumdraw{butterfly}{1.5}\end{scope}
\end{tikzpicture}
使用主题时,其图元也可以按名称绘制:\florilegiumdraw{crystal}{1.0} 在 theme=geometric 下(参见 specimen-geometric.tex),\florilegiumdraw{knot}{1.2} 在 theme=math 下。
工作原理
绘制(ink.lua)。所有几何图形都在 Lua 中计算,并作为 PDF 路径操作符写入每页的一个字面节点;TeX 不做任何算术运算。图元在仿射变换下的局部坐标系中绘制,当图元在作用域内被调用时,该变换从 TikZ 读取。颜色来自基于唯一文档颜色 ink 推导出的色调渐变,在 ink 的色相上从藏青到雾色,在 OKLCH 中插值,使浅色端保持蓝色而不变成灰色。渐细的笔画是两条偏移的贝塞尔曲线,而非采样的带状。
图元(flora.lua)。按照水彩画家的顺序绘制。一层平涂,上面叠着更深的斑块,颜料在边缘积聚,用白色提亮出条纹,几根细脉,几乎没有轮廓线。宽大的花瓣和花朵,倾斜视角的星形百合,花朵和小花,花蕾和风铃草;浅裂和全缘的叶子;像蝎子尾巴一样卷曲的勿忘我聚伞花序,分枝的小枝,伞形花序,卷须,种子头;蝴蝶、蜜蜂、蜗牛和一只鸟;铜色闪光、玫瑰花饰,以及茎变成页眉线的常春藤叶花饰。其中任何一个都可以画成淡色的幽灵。
手工构图(authored.lua)。一株植物从左下角的根部生长出来。三条茎穿过手工挑选的控制点,第四条沿页面底部延伸,作为页脚;叶子按选定比例挂在茎上;其余每个图案都长在一根枝条上,枝条从茎上切向离开,再沿图案自身的轴线抵达,因此每朵花都能一路追溯到根部。
生成构图(grow.lua),思路取自 Wong、Zongker 与 Salesin 的
Computer-Generated Floral Ornament
(SIGGRAPH 1998)。茎沿着栏向上摆动,形态是随高度变化的平滑噪声;叶挂在茎的节点上,前提是那里有空间。接下来反复执行。在茎的触及范围内取最大的空白圆盘,选一个能放进去的图案,从最近的茎上一点长出一根枝条通向它;这根枝条随即也成为茎。先占大空洞,所以花朵和百合出现得早,小花枝和叶子填充中段,循环收尾时把小叶、花苞和小花塞到茎上。同一遍里用 \pagetotal 量出文本的结尾,因此最后一页偏短时,枝条能一次编译就长进它下方的空间。
读取页面(florilegium.lua)。标题或 \florilegiumbloom 会在文本中留下一个不可见的标记节点。页面输出时,逐节点遍历它的盒子,按排版时的实际值累加高度、深度和胶,求出每个标记在页面上的高度;花园先给这些行配上花,然后才挂叶。年龄会重新调整图案的权重,春天是花苞和成串的花苞,秋天是种子头,此时有些叶子用由 accent 而非墨色构建的渐变来上色。
首字下沉(initial.lua)。字母的轮廓由 luaotfload 的 OpenType 读取器以贝塞尔段给出。展平成多边形后,得到一个奇偶内外测试;再用一张到字母和边框的距离网格找出最大的空隙,用来放花。藤蔓是一条横跨图块的 Catmull-Rom 曲线;与字母相交处依次从上方、下方穿过,从上方穿过的股线再画一遍,裁剪到字母形状,并带一圈薄薄的纸色光晕。
程序化 ≠ 随机。 种子由页码推导而来,因此第 n 页永远对应第 n 个花园。正是这种可复现性,让文档稳定到足以排版。
代码结构、可调参数以及尚未实现的想法,见 CLAUDE.md,写法类似交接文档。
致谢
- 提出这个问题的 Reddit 帖子(见缘起)。
- 生长循环沿用 Wong、Zongker 与 Salesin 的论文 Computer-Generated Floral Ornament(SIGGRAPH 1998)。
- 色调渐变在 Oklab 中计算,这是 Björn Ottosson 提出的感知色彩空间;
ink.lua中的转换矩阵出自他手,已进入公有领域。 - 随机数来自 mulberry32,即 Tommy Ettinger 的公有领域生成器。
\florilegiumornament、它的ink与seed=random键,以及density选项对应的回答,来自 rubyFeedback 的 issue #4。- 数学主题中的图形保留了各自来源人物的名字,Maurer 玫瑰线、Vogel 向日葵模型、Temple Fay 蝴蝶曲线、Petersen 图、Koch 雪花、Hilbert 曲线、Lissajous 图形、Apollonius 圆堆。
许可证
MIT,见 LICENSE。Copyright (c) 2026 Kevin Raymond。