流動的文字 · 與它讓開的事物
想看文字為移動物件讓路嗎?滿版中文短文會逐字測量,繞著自行漂移、碰撞的物件換行;物件可用滑鼠拖曳後拋出;支援雙欄、三欄切換、暫停、重設與滾輪縮放,並以襯線字體搭配加大留白呈現,單一檔案靜態頁。
流动的文字
与它让开的东西
一篇关于排版的短文 · 逐字测量,实时断行
两栏 · 三栏
暂停复位125 行 · 测量 301 ms(一次) · 重排 2.50 ms
物件自己会动 · 拖一把可以扔出去 · 滚轮缩放 · 全文实时重排
01
一、被挡住的那一行一段文字遇到一样东西,只有三种办法:从它上面跳过去,把它挤到边上,或者绕着它走。前两种容易,第三种一直很难 —— 难到在印刷术的大部分历史里,它都被当作一项要额外收钱的手艺。中世纪的手抄本里已经有绕排了。彩绘首字母占掉版心左上角一大块,抄写员就把接下来三四行依次缩短,让文字沿着字母的轮廓退让。那时候没有什么算法,只有一双眼睛和一整天时间:抄错一行,整页重来。值得注意的是,那些首字母并不是「插图」。它们是版面的一部分,文字为它们让路是理所当然的事。绕排最初不是一种效果,而是一种秩序。真正让绕排变成奢侈品的是金属活字。每一行都是一条铅条,要让第三行比第二行短八个字宽,就得往行末塞进恰好八个字宽的空铅 —— 而空铅只有固定的几种厚度。排一段绕着圆形照片走的文字,是拿一堆矩形去逼近一条曲线,一次一行,全靠手。二、断行是一道最短路径题把绕排先放一放,看更基础的那个问题:一段文字,在给定宽度下,该在哪些地方断行?最朴素的做法是贪心:从左往右塞词,塞不下就换行。打字机和早期的文字处理软件都这么干。它的毛病是只看当下 —— 这一行塞得很满,代价可能是下一行只剩三个词,稀稀拉拉像被拉长的橡皮筋。更好的想法是把整段一起看。给每一行的松紧程度打一个「难看分」,再给整段的总分求最小值。这样一来,某一行主动少放一个词,若能让后面三行都舒服,就是划算的。一旦这么定义,它就变成了一道标准的最短路径问题:
02
每个可以断行的位置是一个节点,从一个断点到下一个断点是一条边,边的权重就是那一行的难看分。求整段最优断行,等于在这张图上求一条最短路。这个转化是关键的一步。它意味着最优解可以用动态规划在线性时间附近算出来,而不需要去试所有断法 —— 后者的数量随段落长度指数爆炸。难看分怎么定,是审美问题不是算法问题。通常会惩罚过松和过紧的行、惩罚连续多行都在行末断出连字符、惩罚段落最后一行只剩一个词。换一套惩罚,就换一种排版风格。三、为什么它一直很贵算法这么清楚,为什么浏览器里的文字绕排至今还是个半成品?答案不在断行,在测量。要判断一行放不放得下,得先知道这些字有多宽。而字的宽度不是把每个字符的宽度加起来那么简单:西文有字距调整,某些字母组合会被合并成一个连字;阿拉伯文的字形随上下文变化;同一段文字换一个字号,宽度不是等比例缩放。所以真正的成本是:每问一次「这一串字有多宽」,都要跑一次真正的文字整形。而最短路径要问的次数,是候选断点数量的平方级。解法是把测量和断行拆开。先把整段文字过一遍,把每一个字素的宽度问清楚,只问一次,缓存下来。之后所有的断行计算都只是在一个数字数组上做加法 —— 从「反复整形文字」变成「反复求和」。这一拆,代价的量级就变了。原本每一次重排都要重新整形一整段文字,现在只需要重新求和。前者是毫秒级甚至几十毫秒,后者是微秒级。接下来才轮到绕排。障碍物的作用,是把每一行的可用宽度从一个常数变成一个随行号变化的数组:第一行可以用整幅,第二行左
03
边被占掉六十个像素,第三行两边都被占掉。断行算法本身一个字都不用改,只是每一行去查一次自己那一行能用多宽。把障碍物投影成「每一行的可用区间」,是整件事里最省力的一步抽象。圆、鱼、塔、云,在断行算法眼里全都一样:一串左右边界。形状的复杂度被挡在了排版逻辑之外。当然,投影本身要小心。一个凹形障碍物会把某一行切成两段互不相连的区间,这时候「一行」就不再是一个矩形。多数实现在这里选择保守:只取最宽的那一段,剩下的空着。四、当障碍物自己会动静态版面里,上面这些做一次就够了。真正有意思的是障碍物会动的时候 —— 你把一个球拖过一段文字,文字要在拖动的每一帧里重新断行。这时候「测量一次、求和很多次」的那道分界线就成了成败所在。字素宽度在物件移动时完全没有变,需要重算的只有可用区间和最短路。一秒六十次地做加法是廉价的;一秒六十次地整形文字不是。还有一个容易被忽略的约束:重排必须是稳定的。物件往右挪一个像素,如果整段文字的断法忽然大改,视觉上就是一次闪烁。最短路径这一点上比贪心好得多 —— 它的解随输入连续变化,不会因为某一行差一个像素而级联到段尾。文字本身也不应该被牺牲。绕排常见的失败是把文字变成图片,或者把每个字塞进一个绝对定位的方块 —— 那样它就不再能被选中、被复制、被朗读、被搜索。真正的做法是只决定断行位置,剩下的仍然交给浏览器去画。于是回到最初那三种办法。跳过去最省事,挤到边上最保险,绕着走最麻烦 —— 而它之
04
所以值得,是因为它承认了页面上不只有文字:还有别的东西在那里,占着位置,有它自己的形状。试着抓住一个物件扔出去。它会一路撞开挡在前面的句子,而每一个字都还在原来的地方 —— 只是换了一行。
流动的文字
一、被挡住的那一行
一段文字遇到一样东西,只有三种办法:从它上面跳过去,把它挤到边上,或者绕着它走。前两种容易,第三种一直很难 —— 难到在印刷术的大部分历史里,它都被当作一项要额外收钱的手艺。
中世纪的手抄本里已经有绕排了。彩绘首字母占掉版心左上角一大块,抄写员就把接下来三四行依次缩短,让文字沿着字母的轮廓退让。那时候没有什么算法,只有一双眼睛和一整天时间:抄错一行,整页重来。
值得注意的是,那些首字母并不是「插图」。它们是版面的一部分,文字为它们让路是理所当然的事。绕排最初不是一种效果,而是一种秩序。
真正让绕排变成奢侈品的是金属活字。每一行都是一条铅条,要让第三行比第二行短八个字宽,就得往行末塞进恰好八个字宽的空铅 —— 而空铅只有固定的几种厚度。排一段绕着圆形照片走的文字,是拿一堆矩形去逼近一条曲线,一次一行,全靠手。
二、断行是一道最短路径题
把绕排先放一放,看更基础的那个问题:一段文字,在给定宽度下,该在哪些地方断行?
最朴素的做法是贪心:从左往右塞词,塞不下就换行。打字机和早期的文字处理软件都这么干。它的毛病是只看当下 —— 这一行塞得很满,代价可能是下一行只剩三个词,稀稀拉拉像被拉长的橡皮筋。
更好的想法是把整段一起看。给每一行的松紧程度打一个「难看分」,再给整段的总分求最小值。这样一来,某一行主动少放一个词,若能让后面三行都舒服,就是划算的。
一旦这么定义,它就变成了一道标准的最短路径问题:每个可以断行的位置是一个节点,从一个断点到下一个断点是一条边,边的权重就是那一行的难看分。求整段最优断行,等于在这张图上求一条最短路。
这个转化是关键的一步。它意味着最优解可以用动态规划在线性时间附近算出来,而不需要去试所有断法 —— 后者的数量随段落长度指数爆炸。
难看分怎么定,是审美问题不是算法问题。通常会惩罚过松和过紧的行、惩罚连续多行都在行末断出连字符、惩罚段落最后一行只剩一个词。换一套惩罚,就换一种排版风格。
三、为什么它一直很贵
算法这么清楚,为什么浏览器里的文字绕排至今还是个半成品?答案不在断行,在测量。
要判断一行放不放得下,得先知道这些字有多宽。而字的宽度不是把每个字符的宽度加起来那么简单:西文有字距调整,某些字母组合会被合并成一个连字;阿拉伯文的字形随上下文变化;同一段文字换一个字号,宽度不是等比例缩放。
所以真正的成本是:每问一次「这一串字有多宽」,都要跑一次真正的文字整形。而最短路径要问的次数,是候选断点数量的平方级。
解法是把测量和断行拆开。先把整段文字过一遍,把每一个字素的宽度问清楚,只问一次,缓存下来。之后所有的断行计算都只是在一个数字数组上做加法 —— 从「反复整形文字」变成「反复求和」。
这一拆,代价的量级就变了。原本每一次重排都要重新整形一整段文字,现在只需要重新求和。前者是毫秒级甚至几十毫秒,后者是微秒级。
接下来才轮到绕排。障碍物的作用,是把每一行的可用宽度从一个常数变成一个随行号变化的数组:第一行可以用整幅,第二行左边被占掉六十个像素,第三行两边都被占掉。断行算法本身一个字都不用改,只是每一行去查一次自己那一行能用多宽。
把障碍物投影成「每一行的可用区间」,是整件事里最省力的一步抽象。圆、鱼、塔、云,在断行算法眼里全都一样:一串左右边界。形状的复杂度被挡在了排版逻辑之外。
当然,投影本身要小心。一个凹形障碍物会把某一行切成两段互不相连的区间,这时候「一行」就不再是一个矩形。多数实现在这里选择保守:只取最宽的那一段,剩下的空着。
四、当障碍物自己会动
静态版面里,上面这些做一次就够了。真正有意思的是障碍物会动的时候 —— 你把一个球拖过一段文字,文字要在拖动的每一帧里重新断行。
这时候「测量一次、求和很多次」的那道分界线就成了成败所在。字素宽度在物件移动时完全没有变,需要重算的只有可用区间和最短路。一秒六十次地做加法是廉价的;一秒六十次地整形文字不是。
还有一个容易被忽略的约束:重排必须是稳定的。物件往右挪一个像素,如果整段文字的断法忽然大改,视觉上就是一次闪烁。最短路径这一点上比贪心好得多 —— 它的解随输入连续变化,不会因为某一行差一个像素而级联到段尾。
文字本身也不应该被牺牲。绕排常见的失败是把文字变成图片,或者把每个字塞进一个绝对定位的方块 —— 那样它就不再能被选中、被复制、被朗读、被搜索。真正的做法是只决定断行位置,剩下的仍然交给浏览器去画。
于是回到最初那三种办法。跳过去最省事,挤到边上最保险,绕着走最麻烦 —— 而它之所以值得,是因为它承认了页面上不只有文字:还有别的东西在那里,占着位置,有它自己的形状。
试着抓住一个物件扔出去。它会一路撞开挡在前面的句子,而每一个字都还在原来的地方 —— 只是换了一行。
断行在浏览器里实时算出来,由浏览器绘制,可以选中和复制。
一頁會繞排的文字:文字會即時繞著自行移動的物件排版,逐字測量換行;物件可拖曳後拋出。
製作一頁文字繞排實驗頁:滿版中文短文,文字會即時繞著自行移動的物件排版,逐字測量換行;物件會漂移、碰撞,可用滑鼠拖曳後拋出;支援雙欄/三欄排版切換、暫停/動起來與重設控制、滾輪縮放;襯線字體搭配加大留白,維持流暢閱讀。單一檔案靜態頁。
製作一頁文字繞排實驗頁:滿版中文短文會即時繞著自行移動的物件排版,逐字測量換行;物件會漂移、碰撞,可用滑鼠拖曳後拋出;支援雙欄/三欄排版切換、暫停/動起來與重設控制、滾輪縮放;以襯線字體搭配加大留白,維持流暢閱讀。單一檔案靜態頁。
製作一頁文字繞排實驗頁:滿版中文短文會即時繞著自行移動的物件排版,逐字測量換行;物件會漂移、碰撞,可用滑鼠拖曳後拋出;支援雙欄/三欄排版切換、暫停/動起來與重設控制、滾輪縮放;襯線字體搭配加大留白,維持流暢閱讀。單一檔案靜態頁。
使用方向鍵或 WASD 移動,按住空白鍵射擊;貼近彈幕擦彈可累積能量,按 Shift 釋放炸彈清除全螢幕彈幕。分數、連擊、生命、能量與首領血條即時顯示;行動裝置可拖曳並自動射擊,照抄提示詞即可開局。

滑鼠轉向、左鍵發射電漿彈可按住連發,Shift 推進會過熱,需冷卻,空白鍵垂直點火可瞬間拉升;被命中只會離線 2 秒,撞上格柵才會墜毀,結算頁提供命中統計。提示詞可直接複製。
