神筆馬良——基於 OpenGL 的塗鴉框架

Harley-xk發表於2018-05-07

取這個名字有投機取巧的嫌疑,希望能對得起先賢 >_<

這是什麼?

MaLiang 是 iOS 平臺一個基於 OpenGL ES3 的塗鴉繪相簿,使用純 Swift 實現,支援自定義紋理、壓力感應、自動筆觸等特性,並且提供了一定的自定義擴充套件的空間。

這篇文章可以看作是對 Github 上 README 說明的詳細擴充套件和補充說明。

使用

我的理念是儘量製造簡單、優雅的東西,雖然有時候要做到這一點其實很難,但是儘量往這方面靠吧。MaLiang 的整合和使用都很簡單,我把大量對使用者來說沒有什麼用也沒有必要了解的內部邏輯都隱藏了。當然了,如果你的好奇心很重,可以自己去看原始碼。這篇文章也會介紹一些內部實現的思路。

整合

MaLiang 已經推送到了 Cocopods 的官方 repo,所以,你只需要在 Podfile 增加一條 Pod 指令然後 install 就可以在專案中使用了:

pod 'MaLiang'
複製程式碼

然後在需要使用的地方引入 Mudule。當然,首先需要編譯一下,否在會報找不到 Moudle 的錯誤

import MaLiang
複製程式碼

幾個主要的類

1. Canvas

畫布是 MaLiang 最基礎的元件,所有的塗鴉都發生在 Canvas 上。Canvas 本質上是一個 UIView,所以你可以使用任何你原來建立 UIView 的方法來建立一個畫布,並將它新增到你的介面上。

  • 如果你偏好程式碼流,那麼直接呼叫 UIView 的通用建構函式 init(frame:) 就可以了。

  • 如果你覺得 IB 流才是正道,只要在 xib 或者 storyboard 中拖一個 UIView 到介面上,然後將類名改成 Canvas 後回車就可以了,Xcode 應該會自動將 Module 設定成 MaLiang

Canvas 設定正確的佈局約束,然後你就可以開始塗鴉了,比如寫一個像下面這樣的東東 :)

神筆馬良——基於 OpenGL 的塗鴉框架

嗯,想畫成這樣,確實還缺少一些東西 :)

Canvas 繼承自 MLView(ML是 MaLiang 的縮寫,不是那個機器學習的東東),MLView 做了幾乎所有與 OpenGL 打交道的事情,雖然它被定義成一個 open 的類,但實際使用中基本是用不到的。不過了解一些原理也無傷大雅麼~

OpenGL 塗鴉的核心是紋理(Texture),本質上就是沿著手指軌跡,不斷地將紋理疊加到畫布上的過程。所以能畫出什麼樣的筆跡,完全取決於使用的紋理,以及它的大小、顏色、尺寸等引數。

MLView 初始化之後會使用自帶的圖片建立一個預設的紋理,這個紋理就是一個簡單的不透明的圓點,所以只能畫最簡單的線條。如果想要畫出上圖那樣的效果,就需要使用相對複雜一點的紋理了。MaLiang 的示例專案裡面提供了好幾個設定好的紋理,用他們可以模擬出鉛筆、水筆以及毛筆的特效,上面的文字就是使用毛筆特效寫出來的。

快照

Canvas 提供了一個簡單的快照功能:

open func snapshot() -> UIImage?
複製程式碼

呼叫該方法會對畫布生成一個當前內容的快照並以 Image 的形式返回,快照的實現邏輯很簡單,你也可以自己實現更加複雜的快照邏輯。

2. Brush

直接使用紋理還是比較繁瑣的,另外與紋理相關的還有顏色、線條的粗細以及其他一些引數,所以這裡提供了一個 Brush 類來處理所有的這些資料。

Brush 的屬性在改變後會立刻影響接下來的繪製效果。

  • opacity 透明度

上面提到,塗鴉的本質是把紋理疊加到畫筆的過程,所以想要做出深淺不一的筆跡,紋理就需要具有透明度,可以通過opacity 屬性來調節。

  • pointSize 筆跡粗細

pointSize 直接影響筆跡的粗細,它是以 iOS 尺寸的標準單位 點(point) 來衡量的,所以這是一個自適應螢幕畫素密度的屬性。你不需要根據裝置型別來計算實際畫素,直接指定眼睛可見的大小就可以了。

  • pointStep 點距

同上,由於筆跡是通過疊加紋理實現的,因此除了透明度外,兩個紋理之間的距離也會影響到筆跡的深淺。另外如果把點距設定到大於筆跡的尺寸,甚至可以畫出類似虛線的效果。點距的單位也是 點(point)

  • forceSensitive 壓力敏感度

之所以說 pointSize 是影響筆跡的粗細,而不是直接確定,是因為有壓力感應的存在。筆跡的實際尺寸會隨著壓力的大小在 pointSize 指定的尺寸上下浮動,壓力越大,筆跡越粗。forceSensitive 影響筆跡對壓力浮動的劇烈程度,建議設定為 0 - 1 之間的某個值。如果設定過大,筆跡隨壓力的便會會太過劇烈而失真;如果將 forceSensitive 的值設定為 0,則對該畫筆關閉壓力感應效果,筆跡粗細不會隨著壓力而變化。

MaLiang 預設使用 iOS 裝置的壓力感應特性,另外在一些不支援壓力感應的裝置上使用模擬的壓力感應。模擬壓感依賴手勢移動的速度來判斷壓力的大小,速度越快壓力越小。

  • color 顏色

影響筆跡的顏色,實際畫出的顏色會計算進 opacity 的值,不過由於紋理之間會疊加,所以相互效果可以基本抵消。你一般不需要為顏色額外指定透明度的值。

  • texture 紋理

texture 是一個非公開屬性,實際使用時只需要使用紋理圖的 Image 初始化 Brush 物件就可以了,不需要關心 texture 的具體實現。

實際繪製時的顏色是設定的 color 與紋理的顏色混合之後的結果,所以需要保證紋理圖是白色的,才能確保繪製正確的顏色。這個問題可能會在未來改善。

texture 實際上是一個 MLTexture 型別的物件,MLTexture 內部分裝了紋理相關的 OpenGL 實現,包括建立紋理、切換畫筆時的紋理繫結等。

3. Document

Document 不是實現塗鴉的必備元件,它是為了提供一些更加深入的功能而設計的。Document 維護著持有它的畫布的所有筆跡資料,依賴這些資料,可以實現撤銷和重做功能。這兩個功能 MaLiang 已經預設實現。

通過 Document 持有的資料,你還可以輕鬆實現儲存塗鴉資料到檔案的邏輯。反過來也可以將儲存的資料重新還原成畫布影象,這樣可以實現跨裝置的資料同步功能。

Document 功能預設是沒有啟用的,需要手動通過程式碼開啟:

canvas.setupDocument()
複製程式碼

Document 在執行過程中需要使用一部分硬碟空間來存放臨時資料,所以如果裝置儲存空間不足時,上面的操作會丟擲一個異常,為了保證程式的健壯性,建議使用 do-catch 模式來捕獲可能的異常情況:

do {
    try canvas.setupDocument()
} catch {
    // do somthing when error occurs
}
複製程式碼

計劃實現的一些特性

計劃中 MaLiang 還存在一些尚未實現的特性,這些特性會在未來逐漸新增進來,當然,你也幫助我實現,然後給我提交 PR :)

  • [x] 撤銷 & 重做,目前已經實現

  • [x] 匯出圖片,已實現

  • [ ] 繪製文字到畫布中的指定位置

  • [ ] 繪製指定的圖片到畫布中的指定位置

  • [ ] 紋理旋轉,旋轉紋理可以實現一些更加特殊的筆跡效果

由來

MaLiang 起源於多年前的一個塗鴉專案,當時還是基於 Objective-C 和 OpenGL ES1 實現的,OpenGL ES1 對於抗鋸齒的支援不是很好,所以塗鴉的效果不怎麼敢恭維。並且當時由於太年輕,整個框架的設計和結構都比較凌亂。雖然最後順利上架了一段時間,不過由於各種各樣的原因,整個專案隨當時的公司一起無疾而終了。

去年開始重拾這個專案,打算基於 Swift 和 OpenGL ES3 完全重寫,同時將當時處理得不是很好的地方加以改進,另外擴充套件了一些自己近期想到的東西,最終誕生了這個庫。

Why Swift?

使用 Swift 直接和 OpenGL 打交道確實不是一件容易的事情,有人奉勸我使用 OC 或者 C 作為中間層來呼叫 OpenGL,再用 Swift 封裝上層邏輯,確實這樣可以以最低的成本實現需要的效果。

不過作為一個業餘專案,成本並不是我第一考慮的要素,而且這個庫雖然是基於 OpenGL 的,但是真正跟 OpenGL 打交道的,其實也就那幾百行程式碼。為了追求這一點點成本和便利性,犧牲整個專案結構的統一和整潔,在我這是無法接受的。

另外,引入 OC 程式碼意味著同時引入了 OC 的動態執行時環境,這對 Swift 的執行效率會有一定的影響。雖然作為一個 iOS 的專案,現在必然無法擺脫 OC 的動態執行時環境,我的這點偏執似乎也沒有什麼意義,不過誰知道以後會怎麼樣呢 :)

應用

說了半天,這個庫有什麼用?說實話我也不知道,或許可以用來做簽名?不過簽名其實用 CoreGraphics 就足夠了。或許可以用它來做一個畫畫的 App 來逗小孩玩,可能我真會這麼幹。。。

說到底,這主要是對當初懵懂時期經歷的一個紀念吧。感興趣的都可以拿去玩 :)

接下來可能會打算基於這個庫開發一款塗鴉的 App。當然了,多年前的那個專案是不會復活了,新的這個 App 會是一個融合了很多我自己想法的全新專案。當然了希望不要半途而廢 - -!

相關文章