摘要:對(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è)備支持,不需要再分別為mouse和touch寫(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
摘要:,算法就是這樣,那我們基于該算法再對(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...
摘要:,算法就是這樣,那我們基于該算法再對(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...
摘要:寫(xiě)在最前本次分享一下在作者上一次失利即拿到畢業(yè)證第二天突然收到阿里社招面試通知失敗之后,通過(guò)分析自己的定位與實(shí)際情況,做出的未來(lái)一到兩年的規(guī)劃。在博客有一定曝光度的積累中,陸續(xù)收到了一些面試邀請(qǐng),基本上是阿里的但是我知道我菜。。 寫(xiě)在最前 本次分享一下在作者上一次失利即拿到畢業(yè)證第二天突然收到阿里社招面試通知失敗之后,通過(guò)分析自己的定位與實(shí)際情況,做出的未來(lái)一到兩年的規(guī)劃。以及本次社招...
閱讀 2975·2021-09-23 11:32
閱讀 2938·2021-09-22 15:12
閱讀 1719·2019-08-30 14:07
閱讀 3461·2019-08-29 16:59
閱讀 1651·2019-08-29 11:11
閱讀 2314·2019-08-26 13:50
閱讀 2436·2019-08-26 13:49
閱讀 2630·2019-08-26 11:49