Lulucat

為什麼我們用印章取代三套螢光筆幾何引擎

張高歌張高歌

真實 Apple Pencil 資料揭露了合成測試遺漏的缺陷,讓 Lulucat Notes 從三套輪廓引擎改用 MaLiang 印章模型。

更新於

Lulucat Notes 一開始採用傳統的螢光筆渲染器:在取樣路徑周圍建立幾何輪廓,再用半透明色彩填滿一次。一次填色可以避免半透明片段重疊處出現較深的色帶,也讓每個接合、自我相交、端帽和短筆畫都成為同一個幾何問題。

三代自訂幾何各自消除了一類缺陷,卻又暴露出另一類缺陷。目前的渲染器使用以 MaLiang 為基礎的柔和圓形印章。MaLiang 是由 Harley-xk 開發、採用 MIT 授權的開源繪圖框架。這種做法移除了一整類輪廓故障,但會讓自我相交處變深,末端也會隨壓力變細。

在 iPad 上擷取的藍色螢光筆手寫內容,幾道筆畫的起點和終點可見細小鉤狀。

原始裝置截圖,放大至 228%。幾道筆畫的兩端都出現細小鉤狀。

真實手寫產生了合成場景沒有的鉤狀

我們的合成場景涵蓋直線、弧線、短促點按、回筆、壓力變化和急轉彎,全部都能正常渲染。真實的中文手寫仍然在許多筆畫的起點和終點產生長約 1–3 點的鉤狀。

因此,我們在 App 中加入儲存按鈕,記錄了 29 道完整筆畫。每個樣本都保留位置、半徑、壓力、方位角、仰角、時間戳記和 UIKit 估算旗標。離線程式解碼了那份 JSON,並編譯 App 使用的同一組 Stroke.swiftStrokeRenderer.swiftHighlighterStrokeBuilder.swift 檔案。其輸出與裝置截圖中的筆畫逐筆吻合,包括那些鉤狀。

離線渲染器只靠記錄的輸入就重現了缺陷,缺陷源於樣本和幾何,與顯示行為無關。我們因此可以讀取實際進入渲染器的精確樣本。

我們的第一個假設把問題歸咎於落筆和離筆時經過估算的 Pencil 方向。資料否定了這個假設。在全部 29 道筆畫中,前八個和後八個樣本內的方位角變化最多為 0.09 弧度,仰角變化最多為 0.02 弧度,而且在那台 iPad 上每個估算旗標都是 false。方向保持穩定,但壓力和方向卻急劇變化。

壓力下限來自資料中的清晰間隔

每道記錄筆畫的最後一個樣本壓力都是 0.000。書寫中的樣本壓力為 0.01 或以上,而 Apple Pencil 離開表面後記錄的樣本壓力為 0.002 或以下。那些離面樣本往往會持續 2–7 點,移動 0.8–9.0 點,方向還可能轉向偏離書寫筆畫約 90 度。

筆畫 2 展示了這個機制。它的主體在 143 到 180 度之間結束。接著,低壓力尾端轉向 −90 到 −67 度,又移動了 8.5 點。輪廓渲染器把最後一個離面點當作最終端點,因此朝那個點拉出一小段狹窄的幾何形狀。

測得的壓力在書寫和離面移動之間留下了幾乎一個數量級的間隔,因此我們把截斷閾值放在這個間隔中:

private static let pressureFloor: CGFloat = 0.005

只移除兩端的低壓力樣本。筆畫中段的低壓力樣本仍然是筆畫的一部分。這條規則遵循測得的事件邊界,不改動書寫路徑。

裁切前的儀器化特寫:綠色輸入樣本轉離筆畫,而紅色輪廓形成狹窄的鉤狀。

裁切前,渲染器將端帽錨定在離面樣本上。綠點是原始樣本;紅線是產生的輪廓。

裁切後的儀器化特寫:藍色填色在最後的書寫樣本處結束,低壓力綠點留在其外。

裁切低於 0.005 的樣本後,端帽在最後一個書寫點結束。目前的印章渲染器仍保留這個壓力下限。

這個下限移除了長長的離面鉤狀。壓力保持在 0.005 以上的短促落筆拖曳和離筆弧線仍然保留,因為那是書寫動作。在輪廓模型中處理這些動作,導向了接下來幾輪幾何調整。

三套幾何引擎只是把缺陷移來移去

第一代:一個輪廓,一次填色

第一代感壓引擎採用以下流程:先平滑中軸線,計算左右偏移,再將兩者接成一個多邊形,最後一次填滿該多邊形。筆尖是由 Apple Pencil 的方位角和仰角驅動的橢圓,因此其橫截面會隨書寫方向改變。短促點按有獨立的形狀,而扁平端帽則用些微不對稱來避免零繞數凹洞。

一次填色解決了筆畫內部的透明度累積問題,也要求每個轉彎處都有有效的多邊形。落筆拖曳會在端帽和主體之間形成楔形。回筆可能形成白色的新月形凹洞。急轉彎可能讓內側輪廓自我相交,留下未填滿的缺口。

第二代:修補輪廓靠近端點的部分

第二套引擎保留輪廓,並加入局部修補:回筆裁切、早期估算屬性的阻尼、端點拉直、暫態方向裁切,以及小幅度的端帽延伸。每個修補都修正了一個已重現的失敗案例。合在一起後,端點行為取決於好幾組閾值和視窗。

接著,真實書寫產生了超出這些視窗的案例。固定的 24 點拉直視窗會抹掉短筆畫中刻意留下的鉤狀。自適應視窗保留了那些鉤狀,但離筆弧線仍可能變成看得見的角。方向感知裁切移除了更多暫態動作,代價是,它在一筆記錄下來的筆畫中丟掉了真正彎曲起筆的約 2.5 點。

每個修補都修正了各自的目標案例,但底層模型仍然要求一個能在含噪、彎曲輸入下持續有效的硬輪廓。

第三代:帶裁切方形端帽的覆蓋聯集

第三套引擎不再建立單一輪廓。它把每個線段畫成梯形,把每個內部樣本畫成圓盤,將它們繪入灰階遮罩,取其覆蓋聯集,然後只套用一次螢光筆色彩。回筆新月形、自我相交變深和零繞數孔洞都消失了,因為渲染器不再依賴多邊形繞數。

聯集會使末端變圓,因此我們按照兩個端點半平面裁切覆蓋範圍,恢復方形端帽。這個裁切修正了壓力恆定的直線,卻引入了更嚴重的缺陷:彎曲筆畫的主體可能合理地穿過端點半平面,而端帽裁切接著會移除筆畫中段的一部分。即時輸入期間,端點方向持續改變,因此被裁切的區域也會移動,筆畫就會閃爍。

端帽裁切修正前的彎曲藍色筆畫,上方主體沿著對角線被切掉;綠點顯示完整的取樣路徑。

全域端帽約束切過彎曲筆畫的主體。

加入局部弧長閘門後的同一道彎曲藍色筆畫,綠色取樣路徑周圍的兩個分支都恢復了。

將端帽裁切限制在距離端點不超過一個半寬的圖元內,就能恢復這道筆畫。

弧長閘門讓端帽裁切變成局部操作,修復了每個記錄案例。極端的合成鉤狀仍會在低壓力尖端接觸裁切端帽處出現一個小白色缺口。第三套引擎又一次用一項幾何失敗換來了另一項。

MaLiang 移除了硬邊界

MaLiang 不是用一個封閉輪廓,而是用反覆的紋理印章建立筆畫。它的路徑產生器使用穿過相鄰樣本中點的二次 Bézier 線段,而線條渲染器則沿著該路徑以固定步距放置印章。印章大小、旋轉、色彩和不透明度都可以沿著筆畫變化。

柔和的印章不帶獨立的端帽、接合或繞數規則。方向的急劇變化會保持為一連串重疊的痕跡。回筆不可能形成空的多邊形凹洞,端點半平面也不可能切掉筆畫中段,因為這個模型根本沒有端點裁切。

我們採用了這個模型,沒有照搬程式碼。目前的 HighlighterStrokeBuilder 執行四項操作:

  1. 只移除壓力低於 0.005 的端點樣本。
  2. 使用中點二次 Bézier 線段平滑位置。
  3. 沿弧長逐點放置一個圓形印章。
  4. 根據正規化壓力設定印章直徑,並以 source-over alpha 合成印章。

建構器從 686 行縮減至 190 行。方位角、仰角、輪廓建立、聯集遮罩、端點方向視窗、端帽延伸和端帽裁切,都從螢光筆路徑中移除。我們仍然記錄 Pencil 方向,因為輸入模型支援這項資訊,而螢光筆本身已不再讀取這些欄位。

MaLiang 的壓力公式需要裝置校準

MaLiang 使用等價於下式的公式設定印章大小:

這個公式假設 force 使用了 0 到 1 之間一段有用的範圍,但我們的裝置資料並非如此。在約 3,400 個 Apple Pencil 樣本中,壓力中位數是 0.047,最大值是 0.178。直接把這些數值代入公式,會讓一般書寫比名義筆刷大小細得多。

因此,我們以觀察到的中位數為基準正規化,再套用指數並限制結果範圍:

現在,書寫壓力中位數會對應到名義上的 12 點直徑。非常輕的接觸仍會以名義大小的 35% 保持可見,而重壓會在 120% 停止增長。

這項校準屬於輸入裝置和筆刷行為,而不是 MaLiang 本身。若不測量我們自己的壓力分布,直接複製原始公式,雖然會保留架構,卻會產生錯誤的筆刷。

每個印章的不透明度來自已知的重疊層數

MaLiang 以經驗式 alpha ÷ overlapping × 2.5 補償半透明印章的重疊。我們的筆刷有固定的 12 點名義直徑,以及 1 點的印章步距,因此其中線會接收約 12 層。在我們的情況下,重疊層數是已知的,可以直接求出補償值,而不必估算。

對於目標筆畫不透明度 個相同的 source-over 層,一個印章的不透明度 會產生

因此一個印章需要

時,每個印章使用約 的不透明度。中心在反覆合成後會達到預期色彩,而邊緣接收的印章較少,保留一到兩點的羽化過渡。

最終柔和印章模型繪製的各種藍色螢光筆筆畫,包括迴圈、鉤狀、短筆畫和自我相交。

最終渲染器重播記錄的裝置路徑。鉤狀、對角線切口和白色凹洞都不見了。

我們接受的兩項代價

自我相交會變深。source-over 合成會計算每一個印章,包括先前一次經過同一區域時放下的印章,而覆蓋聯集引擎原本避免了這點。我們接受較深的重疊,因為它可預測,也能保留筆畫形狀。

端點會隨壓力變窄。真實的離筆會降低壓力,進而縮小印章直徑,因此筆畫會逐漸變細,而不是以全寬方形端帽結束。在三套幾何引擎中,我們一直把方形端帽視為必須具備的特性。放棄它便移除了造成上述幾項失敗的裁切和端點機制,因此我們接受了這個變細的末端。

因此,目前使用的模型比之前的模型做得更少。它保留測得的書寫事件,在裝置校準後回應壓力,並且不需要全域有效的輪廓,就能渲染彎曲和自我相交的路徑。斜角筆尖、完美的方形端帽,以及原始樣本之間的精確折線,都是我們放棄的功能。

這次分析顯示,壓力下限最容易直接移植到其他專案。0.005 這個數值來自真實輸入中兩個群集之間的間隔:書寫樣本在 0.01 以上,離面追蹤則在 0.002 以下。同一批記錄筆畫接著顯示了每個幾何修補在哪裡失敗,這為改變模型提供了依據,也界定了我們保留下來的兩項代價。