成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

[源碼閱讀]基于Canvas+貝塞爾曲線算法的平滑手寫(xiě)板

Darkgel / 2040人閱讀

摘要:對(duì)于能畫(huà)出貝塞爾曲線的,對(duì)已經(jīng)求出的實(shí)例,執(zhí)行,否則執(zhí)行畫(huà)點(diǎn)的方法獲取配置中的,執(zhí)行畫(huà)點(diǎn)??偨Y(jié)閱讀一遍后,這個(gè)庫(kù)說(shuō)白就是基礎(chǔ)的事件操作貝塞爾曲線算法,但是,它內(nèi)部的代碼格式非常清晰,細(xì)粒度代碼復(fù)用使得維護(hù)起來(lái)非常方便。

signature_pad一個(gè)基于Canvas的平滑手寫(xiě)畫(huà)板工具
介紹

實(shí)現(xiàn)手寫(xiě)有多種方式。

一種比較容易做出的是對(duì)鼠標(biāo)移動(dòng)軌跡畫(huà)點(diǎn),再將兩點(diǎn)之間以直線相連,最后再進(jìn)行平滑處理,這種方案不需要什么算法支持,但同樣,它面對(duì)一個(gè)性能和美觀的抉擇,打的點(diǎn)多,密集,性能相對(duì)較低,但更加美觀,視覺(jué)上更平滑;

此處用的另一種方案,畫(huà)貝塞爾曲線。

由于canvas沒(méi)有默認(rèn)的畫(huà)出貝塞爾曲線方法(感謝@madRain評(píng)論中更正)由于canvas并沒(méi)有提供根據(jù)初始和結(jié)束點(diǎn)計(jì)算出貝塞爾曲線控制點(diǎn)的API,因此這里使用了貝塞爾曲線的一系列算法,包括求控制點(diǎn)求長(zhǎng)度,計(jì)算當(dāng)前點(diǎn)的大小,最后用canvas畫(huà)出每一個(gè)確定位置的點(diǎn)。

補(bǔ)充:個(gè)人認(rèn)為,之所以不使用canvas提供的貝塞爾曲線API,是因?yàn)榭梢詫?shí)時(shí)控制線條粗細(xì)(點(diǎn)的大小),在斜街的時(shí)候達(dá)到平滑的效果。
參數(shù)及配置介紹

提供的可配置參數(shù)如下

export interface IOptions {
  // 點(diǎn)的大小(不是線條)
  dotSize?: number | (() => number);
  // 最粗的線條寬度
  minWidth?: number;
  // 最細(xì)的線條寬度
  maxWidth?: number;
  // 最小間隔距離(這個(gè)距離用貝塞爾曲線填充)
  minDistance?: number;
  // 背景色
  backgroundColor?: string;
  // 筆顏色
  penColor?: string;
  // 節(jié)流的間隔
  throttle?: number;
  // 當(dāng)前畫(huà)筆速度的計(jì)算率,默認(rèn)0.7,意思就是 當(dāng)前速度=當(dāng)前實(shí)際速度*0.7+上一次速度*0.3
  velocityFilterWeight?: number;
  // 初始回調(diào)
  onBegin?: (event: MouseEvent | Touch) => void;
  // 結(jié)束回調(diào)
  onEnd?: (event: MouseEvent | Touch) => void;
}

這里要注意的是并沒(méi)有線條粗細(xì)這個(gè)選項(xiàng),因?yàn)檫@里面的粗細(xì)不等線條都是通過(guò)一個(gè)個(gè)大小不同的點(diǎn)構(gòu)造而成;

throttle這個(gè)配置可以參考loadsh或者underscore_.throttle,功能一致,就是為了提高性能。

注冊(cè)事件

constructor內(nèi)部,除了配置傳入的參數(shù)外,就是注冊(cè)事件。

這里優(yōu)先使用了PointerEvent觸點(diǎn)事件,PointerEvent可以說(shuō)是觸摸以及點(diǎn)擊事件的統(tǒng)一,如果設(shè)備支持,不需要再分別為mousetouch寫(xiě)兩套事件了。

狀態(tài)數(shù)據(jù)儲(chǔ)存

狀態(tài)開(kāi)關(guān):

this._mouseButtonDown

當(dāng)執(zhí)行move事件時(shí),會(huì)檢查此狀態(tài),只有在true的情況下才會(huì)執(zhí)行。

數(shù)據(jù)儲(chǔ)存分為2種格式:

pointGroup

這是當(dāng)前筆畫(huà)的點(diǎn)的一個(gè)集合,內(nèi)部?jī)?chǔ)存了當(dāng)前筆畫(huà)的顏色color和所有的點(diǎn)points

this._data

這是一個(gè)儲(chǔ)存所有筆畫(huà)的棧,格式為[pointGroup, pointGroup, ..., pointGroup],當(dāng)需要執(zhí)行undo的時(shí)候,只需要?jiǎng)h除this._data中的最后一條數(shù)據(jù)。

事件流程及方法 mouseDown事件

當(dāng)鼠標(biāo)(觸點(diǎn))按下時(shí),改變狀態(tài)this._mouseButtonDown = true,調(diào)用onBegin回調(diào),創(chuàng)建當(dāng)前筆畫(huà)的一個(gè)新的集合,然后對(duì)當(dāng)前點(diǎn)執(zhí)行更新。

mouseMove事件

首先檢查this._mouseButtonDown狀態(tài),對(duì)當(dāng)前點(diǎn)執(zhí)行更新。

mouseUp事件

改變狀態(tài)this._mouseButtonDown = false;,調(diào)用onEnd回調(diào),對(duì)當(dāng)前點(diǎn)執(zhí)行更新。

可以看到,上面的每一個(gè)事件內(nèi)部都調(diào)用對(duì)當(dāng)前點(diǎn)執(zhí)行更新的方法。

_strokeUpdate——點(diǎn)的更新方法
private _strokeUpdate(event: MouseEvent | Touch): void {
    // 獲取當(dāng)前觸點(diǎn)的位置
    const x = event.clientX;
    const y = event.clientY;

    // 創(chuàng)建點(diǎn)
    const point = this._createPoint(x, y);
    // 調(diào)出最后一個(gè)點(diǎn)集
    const lastPointGroup = this._data[this._data.length - 1];
    // 獲取最后一個(gè)點(diǎn)集的點(diǎn)的數(shù)組
    const lastPoints = lastPointGroup.points;
    // 如果存在上一個(gè)點(diǎn),獲取上一個(gè)點(diǎn)
    const lastPoint =
      lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
    // 判斷上一個(gè)點(diǎn)到當(dāng)前點(diǎn)是否太近(也就是小于配置的最小間隔距離)
    const isLastPointTooClose = lastPoint
      ? point.distanceTo(lastPoint) <= this.minDistance
      : false;
    // 調(diào)出點(diǎn)集的顏色
    const color = lastPointGroup.color;

    // Skip this point if it"s too close to the previous one
    // 存在上一個(gè)點(diǎn)但是太近,跳過(guò),其余的執(zhí)行
    if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
      // 向上一次的點(diǎn)數(shù)組中添加當(dāng)前點(diǎn),并且生成一個(gè)新的貝塞爾曲線實(shí)例
      // 包括4個(gè)點(diǎn) (初始點(diǎn),2個(gè)控制點(diǎn),結(jié)束點(diǎn))
      // 初始寬度,最終寬度
      const curve = this._addPoint(point);

      // 如果不存在lastPoint,即當(dāng)前點(diǎn)是第一個(gè)點(diǎn)
      if (!lastPoint) {
        // 畫(huà)一個(gè)點(diǎn)
        this._drawDot({ color, point });
      // 如果存在lastPoint 并且能形成一個(gè)貝塞爾曲線實(shí)例(3個(gè)點(diǎn)以上)
      } else if (curve) {
        // 畫(huà)出參數(shù)中curve實(shí)例中兩點(diǎn)之間的曲線
        this._drawCurve({ color, curve });
      }
      // 添加到當(dāng)前筆畫(huà)的點(diǎn)數(shù)組
      lastPoints.push({
        time: point.time,
        x: point.x,
        y: point.y,
      });
    }
  }

這個(gè)方法前面就是一系列判斷

判斷是否是第一個(gè)點(diǎn)

判斷是否能加入點(diǎn)的集合(滿足點(diǎn)的最小間隔)

判斷是否能畫(huà)出貝塞爾曲線(滿足至少3個(gè)點(diǎn))

對(duì)于能畫(huà)出貝塞爾曲線的點(diǎn),執(zhí)行算法,求出Besier實(shí)例,包括4個(gè)點(diǎn)初始點(diǎn),結(jié)束點(diǎn)控制點(diǎn)1,控制點(diǎn)2以及當(dāng)前曲線中線條的的初始寬度結(jié)束寬度。

具體如何算的,請(qǐng)參考源碼src/bezier.ts和這篇文章。

對(duì)于能畫(huà)出貝塞爾曲線的,對(duì)已經(jīng)求出的Bezier實(shí)例,執(zhí)行this._drawCurve,否則執(zhí)行this._drawDot

this._drawDot——畫(huà)點(diǎn)的方法

獲取配置中的dotSize,執(zhí)行canvas畫(huà)點(diǎn)。

this.__drawCurve——畫(huà)線的方法

求出當(dāng)前Bezier實(shí)例初始點(diǎn)結(jié)束點(diǎn)之間的距離,這個(gè)距離不是直線距離,而是貝塞爾曲線距離。

對(duì)這個(gè)距離進(jìn)行擴(kuò)展,例如,計(jì)算得到距離為50,那就擴(kuò)展為100個(gè)點(diǎn),即我需要在50這個(gè)距離內(nèi)畫(huà)出100個(gè)點(diǎn);

這么做可以保證在正?;蛘呱晕⒖焖俚臅?shū)寫(xiě)中,不出現(xiàn)斷層。

接著又是算法,目的是求出這個(gè)距離內(nèi)的每一個(gè)點(diǎn)的大小,這是一個(gè)變化值,是的粗細(xì)變化更加平滑。

最后同樣是canvas畫(huà)點(diǎn)。

以上就是整個(gè)基本流程。

總結(jié)

閱讀一遍后,這個(gè)庫(kù)說(shuō)白就是基礎(chǔ)的事件操作+貝塞爾曲線算法,但是,它內(nèi)部的代碼格式非常清晰,細(xì)粒度+代碼復(fù)用使得維護(hù)起來(lái)非常方便。

同時(shí)可以對(duì)貝塞爾曲線有一個(gè)更深層的了解(算法還是沒(méi)法手撕囧),但起碼有一個(gè)比較完整的思路;

一些可以借鑒的東西:

PointerEvent的優(yōu)勢(shì)

canvas+貝塞爾曲線

節(jié)流throttle的寫(xiě)法(參考源碼src/throttle.ts)

數(shù)據(jù)結(jié)構(gòu)及實(shí)現(xiàn)undo的方案

導(dǎo)圖

貝塞爾曲線算法資料:

https://medium.com/square-cor...

https://www.lemoda.net/maths/...

源碼閱讀專欄對(duì)一些中小型熱門(mén)項(xiàng)目進(jìn)行源碼閱讀和分析,對(duì)其整體做出導(dǎo)圖,以便快速了解內(nèi)部關(guān)系及執(zhí)行順序。
當(dāng)前源碼(帶注釋),以及更多源碼閱讀內(nèi)容:https://github.com/stonehank/sourcecode-analysis,歡迎fork,求

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/104818.html

相關(guān)文章

  • canvas進(jìn)階——如何畫(huà)出平滑曲線?

    摘要:,算法就是這樣,那我們基于該算法再對(duì)現(xiàn)有代碼進(jìn)行一次升級(jí)改造設(shè)置線條顏色在原有的基礎(chǔ)上,我們創(chuàng)建了一個(gè)變量用于保存之前事件中鼠標(biāo)經(jīng)過(guò)的點(diǎn),根據(jù)該算法可知要繪制二次貝塞爾曲線起碼需要個(gè)點(diǎn)以上,因此我們只有在中的點(diǎn)數(shù)大于時(shí)才開(kāi)始繪制。 背景概要 相信大家平時(shí)在學(xué)習(xí)canvas 或 項(xiàng)目開(kāi)發(fā)中使用canvas的時(shí)候應(yīng)該都遇到過(guò)這樣的需求:實(shí)現(xiàn)一個(gè)可以書(shū)寫(xiě)的畫(huà)板小工具。 嗯,相信這對(duì)canva...

    Cobub 評(píng)論0 收藏0
  • canvas進(jìn)階——如何畫(huà)出平滑曲線?

    摘要:,算法就是這樣,那我們基于該算法再對(duì)現(xiàn)有代碼進(jìn)行一次升級(jí)改造設(shè)置線條顏色在原有的基礎(chǔ)上,我們創(chuàng)建了一個(gè)變量用于保存之前事件中鼠標(biāo)經(jīng)過(guò)的點(diǎn),根據(jù)該算法可知要繪制二次貝塞爾曲線起碼需要個(gè)點(diǎn)以上,因此我們只有在中的點(diǎn)數(shù)大于時(shí)才開(kāi)始繪制。 背景概要 相信大家平時(shí)在學(xué)習(xí)canvas 或 項(xiàng)目開(kāi)發(fā)中使用canvas的時(shí)候應(yīng)該都遇到過(guò)這樣的需求:實(shí)現(xiàn)一個(gè)可以書(shū)寫(xiě)的畫(huà)板小工具。 嗯,相信這對(duì)canva...

    _ivan 評(píng)論0 收藏0
  • 記一次“失利后”經(jīng)過(guò)半年準(zhǔn)備通過(guò)阿里社招經(jīng)歷與感悟

    摘要:寫(xiě)在最前本次分享一下在作者上一次失利即拿到畢業(yè)證第二天突然收到阿里社招面試通知失敗之后,通過(guò)分析自己的定位與實(shí)際情況,做出的未來(lái)一到兩年的規(guī)劃。在博客有一定曝光度的積累中,陸續(xù)收到了一些面試邀請(qǐng),基本上是阿里的但是我知道我菜。。 寫(xiě)在最前 本次分享一下在作者上一次失利即拿到畢業(yè)證第二天突然收到阿里社招面試通知失敗之后,通過(guò)分析自己的定位與實(shí)際情況,做出的未來(lái)一到兩年的規(guī)劃。以及本次社招...

    malakashi 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<