兩種橡皮擦,以及為什麼擦除屬於筆畫
把橡皮擦筆畫作為獨立元素,是一種直觀的設計;但物件移動會破壞它。因此,在 Lulucat Notes 中,擦除歸屬於被擦除墨跡的筆畫。
在筆記本裡擦除時,人們指的是兩件不同的事。有時某個記號錯了,整個記號都該刪掉:輔助線、劃掉的字,或手掌擱在紙面上時留下的多餘筆畫。有時只有筆畫的一部分有問題:半個字元、圖表中的一根分支,或螢光筆掃過句子後多出的尾端。Lulucat Notes 現在把兩種擦除做成兩個獨立工具。
更難的設計問題是:擦除應該歸屬於哪裡。
筆畫橡皮擦先出現
第一種橡皮擦會刪除整道筆畫。每次觸碰都會測試所有筆畫:邊界框會排除幾乎所有筆畫,剩下的筆畫則會進行點到線段的平方距離測試,判斷時使用的半徑是橡皮擦半徑加上沿線段插值的筆畫半徑。筆畫寬度會隨筆壓變化,因此使用單一名義寬度,會刪掉使用者沒有碰到的細筆畫,也會漏掉碰到的粗筆畫。
一個手勢會合併成一個復原項目,而該項目會連同每道被刪除筆畫的原始索引一起儲存。復原時按照反向順序把它們插回去,因此堆疊順序會完全恢復原狀。
這種橡皮擦快速、可預測,但無法擦掉半個字。
MaLiang 讓橡皮擦成為筆刷
MaLiang 是採用 MIT 授權的繪圖框架,我們先前已從中借用螢光筆印章模型;它用 24 行程式碼處理局部擦除。它的 Eraser 是 Brush 的子類別,只覆寫混合選項,讓操作從加入變成減去。橡皮擦筆畫和其他筆畫一樣,都是同一份清單中的元素。復原會移除這個元素。既有筆畫不會被修改,因為擦除只在依清單順序重播時發生。
我們採用了這個模型,但它在兩個裝置工作階段中失效了。
物件移動揭示了缺少擁有者
第一份回報是:用套索選取筆畫並拖曳它們,缺口卻留在原地。

同一批被擦除的筆畫向右移動。上方:橡皮擦是獨立元素。下方:擦除歸屬於筆畫。
選取讓問題更糟。被選取的筆畫會從點陣圖中取出,放進一個直接繪製它們的覆蓋層,而不包含橡皮擦元素。因此,只要選取一道被擦除的筆畫,它就會立刻恢復完整。
第二份回報是:橡皮擦浮在螢光筆上方。我們的渲染器分兩個階段重播:先繪製螢光筆,再繪製一般筆,因為螢光筆墨跡必須位於其他墨跡下方。橡皮擦元素必須參與兩個階段,否則無法同時擦除兩種筆跡。參與一般筆階段,表示它會在所有螢光筆繪製完後執行,因此也會擦掉時間上較晚繪製的螢光筆筆畫。
這兩個失敗有同一個根源。MaLiang 沒有套索、移動功能,也沒有筆刷之間的圖層順序,所以在那裡,沒有擁有者的元素就夠用了。我們的畫布三者都有,沒有擁有者的擦除卻無法明確回答墨跡移動時會發生什麼,以及這個操作作用在哪個圖層。
擦除歸屬於筆畫
現在每個 Stroke 都帶有自己的擦除路徑。橡皮擦會測試自己碰到哪些筆畫,再以畫布座標把一條路徑附加到每道筆畫上。繪製一道筆畫時,會在透明圖層內繪製它的筆墨和自己的擦除路徑:
context.beginTransparencyLayer(auxiliaryInfo: nil)
drawInk(stroke, in: context, clip: clip, colorOverride: colorOverride)
context.setBlendMode(.destinationOut)
for path in stroke.erasePaths { fillErasePath(path, in: context) }
context.endTransparencyLayer()
Destination-out 會保留目的地,並按來源 alpha 的比例將其移除:
在透明圖層內,目的地只有這道筆畫,因此這項操作不會觸及紙張、下方的筆畫,或稍後繪製的筆畫。沒有擦除路徑的筆畫幾乎都不需要這個圖層,因此不會增加成本;它們會走原本的路徑。
擁有者關係直接解決這兩個問題。平移會讓擦除路徑跟著取樣點移動,因此移動後的筆畫會帶著缺口。選取覆蓋層會繪製同一個筆畫物件,所以被選取的筆畫也會顯示相同的缺口。圖層順序不再重要,因為擦除永遠只作用於自己的筆畫。
復原以手勢為單位:一個項目會列出該手勢產生的每一組 (stroke, path),復原時移除那些路徑,重做時再附加回去。擦除下方的筆墨從未被編輯,因此不必重建任何東西。
即時回饋透過圖塊點陣圖處理
正在繪製的筆畫通常會以覆蓋層形式,渲染在合成頁面上方。擦除無法在那裡運作,因為該覆蓋層同時位於紙張和筆墨上方,而 destination-out 會穿透紙張。
因此,橡皮擦會立即寫入筆畫,而每批新取樣只會觸發受影響矩形區域的局部重播。圖塊管線本來就會清除一個矩形,再重播與其相交的筆畫;橡皮擦重用這條路徑,不會加入第二套繪圖語意。
兩種橡皮擦並列
工具列有兩個橡皮擦按鈕,而不是一個藏著模式的按鈕。局部橡皮擦會擦掉筆畫的一部分;筆畫橡皮擦一接觸就會移除整道筆畫。它們的半徑是分開的常數,目前都是 4 點。
刪除整道筆畫時,其擦除路徑也會隨之刪除,因為它們是該筆畫的一部分;復原時兩者會一起恢復。擦除路徑也會儲存在檔案中,量化方式與其他資料相同:半徑和座標都以點的十分之一為單位。
還有一項限制。一道筆畫即使被擦到完全消失,仍然會以資料形式存在。它渲染時看不見,但套索仍然可以選取它,筆畫橡皮擦仍然可以刪除它,而且它仍會佔用文件空間。要清理完全被擦除的筆畫,需要我們尚未撰寫的覆蓋範圍測試,因此目前仍會保留它們。
當另一個專案的模型在你的專案裡停止運作時,問題通常不在你複製的程式碼,而在那個專案沒有的功能。