signature_pad一個基於Canvas的平滑手寫畫板工具
實現手寫有多種方式。html
一種比較容易作出的是對鼠標移動軌跡畫點,再將兩點之間以直線
相連,最後再進行平滑處理,這種方案不須要什麼算法支持,但一樣,它面對一個性能和美觀的抉擇,打的點多,密集,性能相對較低,但更加美觀,視覺上更平滑;git
此處用的另外一種方案,畫貝塞爾曲線。github
因爲(感謝@madRain評論中更正)因爲canvas
沒有默認的畫出貝塞爾曲線方法canvas
並無提供根據初始和結束點計算出貝塞爾曲線控制點的API,所以這裏使用了貝塞爾曲線的一系列算法,包括求控制點
,求長度
,計算當前點的大小
,最後用canvas
畫出每個肯定位置的點。算法
補充:我的認爲,之因此不使用
canvas
提供的貝塞爾曲線API,是由於能夠實時控制線條粗細(點的大小),在斜街的時候達到平滑的效果。
提供的可配置參數以下canvas
export interface IOptions { // 點的大小(不是線條) dotSize?: number | (() => number); // 最粗的線條寬度 minWidth?: number; // 最細的線條寬度 maxWidth?: number; // 最小間隔距離(這個距離用貝塞爾曲線填充) minDistance?: number; // 背景色 backgroundColor?: string; // 筆顏色 penColor?: string; // 節流的間隔 throttle?: number; // 當前畫筆速度的計算率,默認0.7,意思就是 當前速度=當前實際速度*0.7+上一次速度*0.3 velocityFilterWeight?: number; // 初始回調 onBegin?: (event: MouseEvent | Touch) => void; // 結束回調 onEnd?: (event: MouseEvent | Touch) => void; }
這裏要注意的是並無線條粗細
這個選項,由於這裏面的粗細不等線條都是經過一個個大小不一樣的點構造而成;segmentfault
throttle
這個配置能夠參考loadsh
或者underscore
的_.throttle
,功能一致,就是爲了提升性能。數組
在constructor
內部,除了配置傳入的參數外,就是註冊事件。數據結構
這裏優先使用了PointerEvent觸點事件,PointerEvent
能夠說是觸摸以及點擊事件的統一,若是設備支持,不須要再分別爲mouse
和touch
寫兩套事件了。工具
狀態開關:性能
this._mouseButtonDown
當執行move
事件時,會檢查此狀態,只有在true
的狀況下才會執行。
數據儲存分爲2種格式:
pointGroup
這是當前筆畫的點的一個集合,內部儲存了當前筆畫的顏色color
和全部的點points<Array>
。
this._data
這是一個儲存全部筆畫的棧,格式爲[pointGroup, pointGroup, ..., pointGroup]
,當須要執行undo
的時候,只須要刪除this._data
中的最後一條數據。
mouseDown
事件當鼠標(觸點)按下時,改變狀態this._mouseButtonDown = true
,調用onBegin
回調,建立當前筆畫的一個新的集合,而後對當前點執行更新。
mouseMove
事件首先檢查this._mouseButtonDown
狀態,對當前點執行更新。
mouseUp
事件改變狀態this._mouseButtonDown = false;
,調用onEnd
回調,對當前點執行更新。
能夠看到,上面的每個事件內部都調用對當前點執行更新的方法。
_strokeUpdate
——點的更新方法private _strokeUpdate(event: MouseEvent | Touch): void { // 獲取當前觸點的位置 const x = event.clientX; const y = event.clientY; // 建立點 const point = this._createPoint(x, y); // 調出最後一個點集 const lastPointGroup = this._data[this._data.length - 1]; // 獲取最後一個點集的點的數組 const lastPoints = lastPointGroup.points; // 若是存在上一個點,獲取上一個點 const lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1]; // 判斷上一個點到當前點是否太近(也就是小於配置的最小間隔距離) const isLastPointTooClose = lastPoint ? point.distanceTo(lastPoint) <= this.minDistance : false; // 調出點集的顏色 const color = lastPointGroup.color; // Skip this point if it's too close to the previous one // 存在上一個點可是太近,跳過,其他的執行 if (!lastPoint || !(lastPoint && isLastPointTooClose)) { // 向上一次的點數組中添加當前點,而且生成一個新的貝塞爾曲線實例 // 包括4個點 (初始點,2個控制點,結束點) // 初始寬度,最終寬度 const curve = this._addPoint(point); // 若是不存在lastPoint,即當前點是第一個點 if (!lastPoint) { // 畫一個點 this._drawDot({ color, point }); // 若是存在lastPoint 而且能造成一個貝塞爾曲線實例(3個點以上) } else if (curve) { // 畫出參數中curve實例中兩點之間的曲線 this._drawCurve({ color, curve }); } // 添加到當前筆畫的點數組 lastPoints.push({ time: point.time, x: point.x, y: point.y, }); } }
這個方法前面就是一系列判斷
對於能畫出貝塞爾曲線的點,執行算法,求出Besier
實例,包括4個點初始點
,結束點
,控制點1
,控制點2
以及當前曲線中線條的的初始寬度
和結束寬度
。
具體如何算的,請參考源碼src/bezier.ts
和這篇文章。
對於能畫出貝塞爾曲線的,對已經求出的Bezier
實例,執行this._drawCurve
,不然執行this._drawDot
this._drawDot
——畫點的方法獲取配置中的dotSize
,執行canvas
畫點。
this.__drawCurve
——畫線的方法Bezier
實例初始點
和結束點
之間的距離,這個距離不是直線距離,而是貝塞爾曲線距離。50
,那就擴展爲100
個點,即我須要在50
這個距離內畫出100
個點;這麼作能夠保證在正常或者稍微快速的書寫中,不出現斷層。
canvas
畫點。以上就是整個基本流程。
閱讀一遍後,這個庫說白就是基礎的事件操做+貝塞爾曲線算法,可是,它內部的代碼格式很是清晰,細粒度+代碼複用
使得維護起來很是方便。
同時能夠對貝塞爾曲線有一個更深層的瞭解(算法仍是無法手撕囧),但起碼有一個比較完整的思路;
一些能夠借鑑的東西:
throttle
的寫法(參考源碼src/throttle.ts)undo
的方案
貝塞爾曲線算法資料:
源碼閱讀專欄對一些中小型熱門項目進行源碼閱讀和分析,對其總體作出導圖,以便快速瞭解內部關係及執行順序。
當前源碼(帶註釋),以及更多源碼閱讀內容: https://github.com/stonehank/sourcecode-analysis,歡迎fork
,求