[源碼閱讀]基於Canvas+貝塞爾曲線算法的平滑手寫板

signature_pad一個基於Canvas的平滑手寫畫板工具

介紹

實現手寫有多種方式。html

一種比較容易作出的是對鼠標移動軌跡畫點,再將兩點之間以直線相連,最後再進行平滑處理,這種方案不須要什麼算法支持,但一樣,它面對一個性能和美觀的抉擇,打的點多,密集,性能相對較低,但更加美觀,視覺上更平滑;git

此處用的另外一種方案,畫貝塞爾曲線。github

因爲canvas沒有默認的畫出貝塞爾曲線方法(感謝@madRain評論中更正)因爲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能夠說是觸摸以及點擊事件的統一,若是設備支持,不須要再分別爲mousetouch寫兩套事件了。工具

狀態數據儲存

狀態開關:性能

  • this._mouseButtonDown

    當執行move事件時,會檢查此狀態,只有在true的狀況下才會執行。

數據儲存分爲2種格式:

  1. pointGroup

    這是當前筆畫的點的一個集合,內部儲存了當前筆畫的顏色color和全部的點points<Array>

  2. 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,
      });
    }
  }

這個方法前面就是一系列判斷

  • 判斷是不是第一個點
  • 判斷是否能加入點的集合(知足點的最小間隔)
  • 判斷是否能畫出貝塞爾曲線(知足至少3個點)

    對於能畫出貝塞爾曲線的點,執行算法,求出Besier實例,包括4個點初始點結束點控制點1控制點2以及當前曲線中線條的的初始寬度結束寬度

    具體如何算的,請參考源碼src/bezier.ts這篇文章

對於能畫出貝塞爾曲線的,對已經求出的Bezier實例,執行this._drawCurve,不然執行this._drawDot

this._drawDot——畫點的方法

獲取配置中的dotSize,執行canvas畫點。

this.__drawCurve——畫線的方法

  1. 求出當前Bezier實例初始點結束點之間的距離,這個距離不是直線距離,而是貝塞爾曲線距離。
  2. 對這個距離進行擴展,例如,計算獲得距離爲50,那就擴展爲100個點,即我須要在50這個距離內畫出100個點;

    這麼作能夠保證在正常或者稍微快速的書寫中,不出現斷層。

  3. 接着又是算法,目的是求出這個距離內的每個點的大小,這是一個變化值,是的粗細變化更加平滑。
  4. 最後一樣是canvas畫點。

以上就是整個基本流程。

總結

閱讀一遍後,這個庫說白就是基礎的事件操做+貝塞爾曲線算法,可是,它內部的代碼格式很是清晰,細粒度+代碼複用使得維護起來很是方便。

同時能夠對貝塞爾曲線有一個更深層的瞭解(算法仍是無法手撕囧),但起碼有一個比較完整的思路;

一些能夠借鑑的東西:

導圖

圖片描述

貝塞爾曲線算法資料:


源碼閱讀專欄對一些中小型熱門項目進行源碼閱讀和分析,對其總體作出導圖,以便快速瞭解內部關係及執行順序。
當前源碼(帶註釋),以及更多源碼閱讀內容: https://github.com/stonehank/sourcecode-analysis,歡迎 fork,求
相關文章
相關標籤/搜索