JS前端使用canvas搞一個(gè)手勢(shì)識(shí)別
前言
最近在看一些關(guān)于圖形學(xué)的東西,寫了個(gè)一筆畫手勢(shì)識(shí)別的小 demo,效果大概是下面這個(gè)樣子:
如果你是初次看過(guò)肯定會(huì)覺(jué)得很有意思??。哈哈,話不多說(shuō),讓我們直接開擼吧。
這里可以先花幾秒鐘想一下你會(huì)怎么做???帶著問(wèn)題往下看能夠記得更牢固,比如你可能最關(guān)心的就是怎么識(shí)別怎么對(duì)比。這里先提前貼上項(xiàng)目 demo 地址,有需要的自取。另外這里并不會(huì)涉及什么人工智能、AI識(shí)別、深度學(xué)習(xí)啥的,所以請(qǐng)放心食用??。
具體步驟
發(fā)車?yán)?????????
第一步:手勢(shì)繪制
既然要識(shí)別那肯定得先有手勢(shì)啦,所以第一步要做的就是手勢(shì)繪制,這一步相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,學(xué)習(xí)過(guò) canvas 的同學(xué)應(yīng)該有看過(guò)畫板的實(shí)現(xiàn),這個(gè)也是一樣的,監(jiān)聽 canvas 上的鼠標(biāo)事件,然后在移動(dòng)的時(shí)候?qū)⑹髽?biāo)坐標(biāo)點(diǎn)用線段相連即可,不同的是我們?cè)诶L制過(guò)程中還順便把每個(gè)坐標(biāo)點(diǎn)都畫了一下,核心代碼如下(可跳過(guò)):
handleMousemove(e: MouseEvent) { if (!this.isMove) return; const curPoint = this.getCanvasPos(e); const lastPoint = this.inputPoints[this.inputPoints.length - 1]; // 畫線段 CanvasUtils.drawLine(this.ctx2d, lastPoint[0], lastPoint[1], curPoint[0], curPoint[1], 'blue', 3); // 畫坐標(biāo)點(diǎn) CanvasUtils.drawCircle(this.ctx2d, curPoint[0], curPoint[1], 5); // 如果覺(jué)得原始點(diǎn)的數(shù)量太多,可以節(jié)流 this.inputPoints.push(curPoint); }
畫完之后大概是下面這個(gè)樣子:
從上圖可以看到繪制出來(lái)的紅點(diǎn)并不均勻,因?yàn)橐还P畫過(guò)程中的手速不一樣,疏密程度也就不一樣,所以為了避免這個(gè)因素的影響,我們需要重新取個(gè)樣。
第二步:重新取樣
不同場(chǎng)景的取樣方式也有所不同。這里我們簡(jiǎn)單的選擇等分線條取樣即可,也就是先計(jì)算出整個(gè)手勢(shì)的長(zhǎng)度(所有線段長(zhǎng)度相加),然后 n 等分取點(diǎn)(隨便幾等分,看效果調(diào)節(jié),不用糾結(jié))。
注意我們并沒(méi)有改變?cè)甲鴺?biāo)點(diǎn)的信息,手勢(shì)的繪制還是要按照原來(lái)的點(diǎn)繪制,所以需要加一個(gè)變量來(lái)存儲(chǔ)新采樣的點(diǎn)(后面的計(jì)算全都是用新的取樣點(diǎn)來(lái)計(jì)算)。這個(gè)計(jì)算還是有點(diǎn)小麻煩的,所以我準(zhǔn)備了一張圖方便大家理解????:
然后就是具體的代碼實(shí)現(xiàn)(大概懂了可跳過(guò)):
export type Point = [number, number]; static resample(inputPoints: Point[], sampleCount: number): Point[] { const len = GeoUtils.getLength(inputPoints); const unit = len / (sampleCount - 1); const outputPoints: Point[] = [[...inputPoints[0]]]; let curLen = 0; let prevPoint = inputPoints[0]; for (let i = 1; i < inputPoints.length; i++) { const curPoint = inputPoints[i]; let dx = curPoint[0] - prevPoint[0]; let dy = curPoint[1] - prevPoint[1]; let tempLen = GeoUtils.getLength([prevPoint, curPoint]); while (curLen + tempLen >= unit) { const ds = unit - curLen; const ratio = ds / tempLen; const newPoint: Point = [prevPoint[0] + dx * ratio, prevPoint[1] + dy * ratio]; outputPoints.push(newPoint); curLen = 0; prevPoint = newPoint; dx = curPoint[0] - prevPoint[0]; dy = curPoint[1] - prevPoint[1]; tempLen = GeoUtils.getLength([prevPoint, curPoint]); } prevPoint = curPoint; curLen += tempLen; } while (outputPoints.length < sampleCount) { outputPoints.push([...prevPoint]); } return outputPoints; }
重新采樣之后大概是下面這個(gè)效果:
要注意如果你采用了 n 等分,那么所有的手勢(shì)都應(yīng)該是 n 等分的,不能改變,否則難以比較。另外我們順便把手勢(shì)的中心點(diǎn)算了出來(lái)(就是簡(jiǎn)單的把每個(gè)采樣點(diǎn)坐標(biāo)相加取平均值),并且將手勢(shì)的起始點(diǎn)(最后一個(gè)點(diǎn)也行)與中心點(diǎn)相連,這個(gè)你可以粗淺的認(rèn)為它表示的是這個(gè)手勢(shì)的大致方向,不理解可以先跳過(guò),后續(xù)會(huì)講到。
第二步:平移
其實(shí)你要比較任何東西,都是要量化成數(shù)字來(lái)比較的,而不是通過(guò)感覺(jué)。 不能說(shuō)我覺(jué)的兩個(gè)手勢(shì)長(zhǎng)得像它就像,那只是人工沒(méi)有智能,所以我們要怎么解決這個(gè)問(wèn)題呢?我們需要定一個(gè)標(biāo)準(zhǔn),讓所有手勢(shì)都在同一個(gè)模子下進(jìn)行比較(就好像你要找個(gè)對(duì)象,不得有個(gè)衡量標(biāo)準(zhǔn)嗎),比如都變成同樣的大小、同樣的方向。
不然你想想如果我豎著寫了一個(gè)很大的3和橫著寫了一個(gè)很小的3,它們要怎么比較。所以接下來(lái)我們要做的就是把手勢(shì)標(biāo)準(zhǔn)化(其實(shí)每幅示例圖中的虛線框就是我們的架子),為后續(xù)的比較打好基礎(chǔ),為此就需要經(jīng)歷平移、旋轉(zhuǎn)、縮放這幾個(gè)步驟。
關(guān)于平移,剛才我們已經(jīng)計(jì)算過(guò)手勢(shì)的中心點(diǎn),現(xiàn)在只需要把它移動(dòng)到畫布中心即可,簡(jiǎn)單算下平移距離,然后對(duì)所有新的采樣點(diǎn)做平移操作即可,示例代碼如下:
// 對(duì)每個(gè)坐標(biāo)點(diǎn)進(jìn)行平移 static translate(points: Point[], dx: number, dy: number) { points.forEach((p) => { p[0] += dx; p[1] += dy; }); }
效果如下:
要注意我們?cè)诶L制的時(shí)候需要將畫布左上角的原點(diǎn)移到到畫布中間,這樣做能夠極大的方便計(jì)算,包括接下來(lái)的旋轉(zhuǎn)和縮放也是在平移坐標(biāo)系的基礎(chǔ)上。
第三步:旋轉(zhuǎn)
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)除了中間的虛線框,我們還把整個(gè)畫布八等分了,這是為什么呢?其實(shí)上文中有提到,是因?yàn)槭謩?shì)具有方向性,比如 丨和 /,這兩種手勢(shì)本應(yīng)該很相近,但是方向不同,所以就需要進(jìn)行一定的旋轉(zhuǎn)。
而這里的八條等分線就是我們要靠近的方向(幾等分也是你自己隨意取的),于是乎我們可以簡(jiǎn)單地算下手勢(shì)方向(圖中的綠線)離哪條等分線近就往哪邊旋轉(zhuǎn),然后把所有的點(diǎn)都進(jìn)行旋轉(zhuǎn)變換即可,代碼如下(可跳過(guò)):
// 計(jì)算需要旋轉(zhuǎn)到最近輔助線的弧度,center 為中心點(diǎn),startPoint 為手勢(shì)起始點(diǎn),sublineCount 為坐標(biāo)等分?jǐn)?shù)量 static computeRadianToSubline(center: Point, startPoint: Point, sublineCount: number): number { const dy = startPoint[1] - center[1]; const dx = startPoint[0] - center[0]; let radian = Math.atan2(dy, dx); if (radian < 0) radian += TWO_PI; const unitRadian = TWO_PI / sublineCount; const targetRadian = Math.round(radian / unitRadian) * unitRadian; radian -= targetRadian; return radian; } // 對(duì)每個(gè)坐標(biāo)點(diǎn)進(jìn)行旋轉(zhuǎn) static rotate(points: Point[], radian: number) { const sin = Math.sin(radian); const cos = Math.cos(radian); points.forEach((p) => { let [x, y] = p; p[0] = cos * x - sin * y; p[1] = sin * x + cos * y; }); }
很多同學(xué)可能會(huì)覺(jué)得旋轉(zhuǎn)比平移難,其實(shí)很簡(jiǎn)單的,你只需要知道一個(gè)點(diǎn)是怎么旋轉(zhuǎn)的就行了(線段的旋轉(zhuǎn)就是兩個(gè)端點(diǎn)的旋轉(zhuǎn),多邊形的旋轉(zhuǎn)就是多個(gè)頂點(diǎn)的旋轉(zhuǎn)),這里我畫了張推導(dǎo)圖方便大家理解(不感興趣也可以跳過(guò)):
然后看下這步的效果圖:
第四步:縮放
我們每次繪制的手勢(shì)是有大有小的,所以這里需要統(tǒng)一成一個(gè)大小,也就是做個(gè)縮放。
比如我們要把一個(gè) 600*600
的手勢(shì)放進(jìn)一個(gè) 100*100
的容器中(也就是圖中的虛線框),那就要縮小 6 倍。
那具體要怎么求呢?首先我們要求出手勢(shì)的包圍盒大小,這里采用AABB模型(還有OBB、球模型等)。
那什么是 AABB 包圍盒呢,這個(gè)賊簡(jiǎn)單,就是找出所有采樣點(diǎn)的最大最小 x、y 值即可,就像下面這樣:
現(xiàn)在只要用容器長(zhǎng)度除以 AABB 的最長(zhǎng)邊,得到的就是縮放倍數(shù)。然后同樣的,遍歷所有點(diǎn)進(jìn)行縮放操作,具體代碼如下:
// 再次提醒下因?yàn)槲覀円呀?jīng)把坐標(biāo)系移到了畫布中央,畫布中心和手勢(shì)中心是重合的,所以直接乘以縮放倍速就可以了 static scale(points: Point[], scale: number) { points.forEach((p) => { let [x, y] = p; p[0] = x * scale; p[1] = y * scale; }); }
效果圖如下:
注意不是說(shuō)縮放之后的圖形一定要在虛線框里面,而是縮放之后的圖形大小和虛線框差不多。
第五步:手勢(shì)錄入
這個(gè)就是簡(jiǎn)單的保存數(shù)據(jù),一共可分為兩步:
- 縮略圖:動(dòng)態(tài)地創(chuàng)建一個(gè) canvas 來(lái)繪制手勢(shì),再通過(guò) drawImage 繪制到畫布上,這個(gè)其實(shí)和第一步是一樣的,只不過(guò)圖變小了。用原始點(diǎn)或采樣點(diǎn)畫都可以(原始點(diǎn)比較精確),畢竟是縮略圖,看不出來(lái)太大差別。
- 保存數(shù)據(jù):采樣坐標(biāo)點(diǎn)肯定是要保存的,畢竟我們辛辛苦苦標(biāo)準(zhǔn)化了這么久,其它的想保存啥就保存啥。
第六步:比較(重點(diǎn))
假設(shè)我們已經(jīng)有了兩個(gè)標(biāo)準(zhǔn)化后的手勢(shì),那怎樣才能知道他們相似呢?如果你沒(méi)看過(guò)相關(guān)知識(shí),大概率是不懂的,我。也是??。。。同樣的,這里也可以停下來(lái)思考幾秒種??。。。 ok,其實(shí)手勢(shì)相似與否可以轉(zhuǎn)成兩組采樣點(diǎn)是否足夠靠近的問(wèn)題,一種直觀的解法就是計(jì)算兩組采樣點(diǎn)之間的距離,看是否小于某個(gè)閾值,類似下面這樣:
不懂的話想成一個(gè)采樣點(diǎn)就好理解了(就變成了求兩點(diǎn)距離??),具體代碼如下:
static squaredEuclideanDistance(points1, points2) { let squaredDistance = 0; const count = points1.length; for (let i = 0; i < count; i++) { const p1 = points1[i]; const p2 = points2[i]; const dx = p1[0] - p2[0]; const dy = p1[1] - p2[1]; squaredDistance += dx * dx + dy * dy; } return squaredDistance; }
其實(shí)上面這種方法有個(gè)高大上的名字,叫歐氏距離(好了好了,別裝了??)。
但是對(duì)于我們這種場(chǎng)景有個(gè)更好的相似度算法(算法?溜了溜了?。?,所以接下來(lái)我們來(lái)介紹一個(gè)余弦相似度的概念(不難的,我都畫了圖的,包看包會(huì)):
如果上圖采用的是歐氏距離比較,顯然 AC 距離更近更相似。
如果用余弦值比較,那顯然是 AB 更相似。
這是因?yàn)闅W氏距離得到的是絕對(duì)差異,余弦相似度比較的是相對(duì)差異(仔細(xì)品品??)。
那為什么夾角的大小可以判定兩點(diǎn)的相似度呢?
其實(shí)這個(gè)方法主要判定的是兩點(diǎn)方向的相似度,你可以看到即便向量B很長(zhǎng),但是不影響它的方向朝向,所以B的目標(biāo)朝向和A更相近,這個(gè)用力學(xué)的知識(shí)會(huì)比較好理解一點(diǎn),看下下面這張圖:
夾角越小,發(fā)力的方向才越一致,我們才能拉動(dòng)一個(gè)物體(我們就是有相同目標(biāo)的一類人,也就是相似)。那這么多個(gè)點(diǎn)我們?cè)趺此阌嘞蚁嗨贫饶兀?/p>
回頭看看剛才求夾角的公式,既然只和方向相關(guān),而和向量A、向量B長(zhǎng)度無(wú)關(guān),那么我們一般可以把A、B變成單位向量(就是向量除以它們自身的長(zhǎng)度),這樣A、B的模長(zhǎng)就為1,于是余弦值就可以變成這樣:
是不是突然簡(jiǎn)單了不少,接下來(lái)我們就想辦法把手勢(shì)變成向量就行(就是變成一個(gè)很長(zhǎng)的數(shù)組),這里看圖理解會(huì)方便些:
我們可以把轉(zhuǎn)變后的一維數(shù)組叫做這個(gè)手勢(shì)的特征,并當(dāng)做數(shù)據(jù)保存下來(lái),下次比較的時(shí)候直接把這個(gè)數(shù)組拿出來(lái)算余弦值即可。
// 計(jì)算余弦相似度 static calcCosDistance(vector1: number[], vector2: number[]): number { let similarity = 0; vector1.forEach((v1, i) => { const v2 = vector2[i]; similarity += v1 * v2; }); return similarity; // 相似度介于 -1~1 }
余弦相似度在很多場(chǎng)合都是有用到的,比如文章相似度中詞向量的應(yīng)用(扯遠(yuǎn)了),所以這里簡(jiǎn)要回顧一下它的具體思路:
- 想辦法把原始數(shù)據(jù)轉(zhuǎn)換成長(zhǎng)度相同的一維數(shù)組
[a, b, c, ..., n]
,(雖然是一維數(shù)組,但是是 n 維向量,不理解沒(méi)關(guān)系)。 - 遍歷現(xiàn)有數(shù)據(jù),分別求出對(duì)應(yīng)的余弦值,找出相似度值最高的那一個(gè)。
注意事項(xiàng)
- 手勢(shì)具有方向性:我們可以識(shí)別
|
和/
,因?yàn)樗麄兘?jīng)過(guò)旋轉(zhuǎn)都靠近y
軸,但是|
和一
就不行了,一個(gè)是 y 軸一個(gè)是 x 軸。所以如果我們要想把|
和一
識(shí)別成一個(gè)東西可以這樣子搞,把|
多旋轉(zhuǎn)幾個(gè)角度,在每個(gè)角度都判斷一下是否相似。 - 手勢(shì)的寬高比會(huì)影響結(jié)果:比如你畫一個(gè)正方形和一個(gè)長(zhǎng)長(zhǎng)扁扁的矩形是不相似的。
- 采樣點(diǎn)的數(shù)量:過(guò)多過(guò)少都不行,過(guò)多效率低,對(duì)圖形一致性要求也高,反之同理。
- 手勢(shì)的復(fù)雜度:圖形的識(shí)別率和圖形的復(fù)雜性沒(méi)有太大關(guān)系。簡(jiǎn)單的圖形由于特征不明顯,容易出錯(cuò),比如多邊型和圓。復(fù)雜的圖形,采樣點(diǎn)就容易被稀釋,得到的特征比較粗。
- 應(yīng)用場(chǎng)景:大家可以自己想想這個(gè)東西除了用在手勢(shì)還能用在那里?這里舉個(gè)例子,比如數(shù)學(xué)老師在遠(yuǎn)程上課、寫板書的時(shí)候,經(jīng)常需要徒手畫圓或者畫正方形,這里我們就可以幫其自動(dòng)校正,如果畫的像一個(gè)圓就自動(dòng)重新生成一個(gè)正圓,也許描述的比較蒼白,所以大家可以自行腦補(bǔ)一下畫面??。
比較的基本套路(可跳過(guò))
這里簡(jiǎn)單補(bǔ)充下比較兩個(gè)東西是否相似的一般套路,也就兩大步:
特征提?。ň褪翘幚頂?shù)據(jù)的過(guò)程):
不管是什么東西,都有對(duì)應(yīng)的原始數(shù)據(jù),我們要做的就是將其(經(jīng)過(guò)層層處理)轉(zhuǎn)換成同一個(gè)框架維度下(也就是標(biāo)準(zhǔn)化),通常就是將原始數(shù)據(jù)轉(zhuǎn)換成長(zhǎng)度相同的一維數(shù)組(再次強(qiáng)調(diào)雖然是一維數(shù)組,但其實(shí)是 n 維向量)。
算法識(shí)別(就是比較數(shù)據(jù)的過(guò)程):
- 通過(guò)某種算法(比如上面提到的歐氏距離和余弦相似度)進(jìn)行逐一對(duì)比。
- 類似的還有網(wǎng)格識(shí)別(先把圖片馬賽克化,像素粒度就變粗了,然后根據(jù)像素顏色差值進(jìn)行比較,這個(gè)方法是適用于以縮略圖找原圖)、方向識(shí)別(比如只要手勢(shì)順序是先向右再向下再向左再向上就認(rèn)為是矩形)等。
- 顯然不同的特征和算法就造成了結(jié)果的千差萬(wàn)別(效率啊、準(zhǔn)確率等,還有薪資待遇???),優(yōu)化的手段也是百花齊放,所以也就沒(méi)有通用的算法,只有適合的算法,因地制宜。
我們以一個(gè)極其簡(jiǎn)單的推薦算法為例,推薦算法的問(wèn)題在某種程度上可以轉(zhuǎn)換成兩個(gè)人的喜好相似程度:
喜好 | 干飯 | 摸魚 | 睡覺(jué) | 就是玩 | ... |
---|---|---|---|---|---|
甲(咸魚) | ? | ? | ? | ? | ... |
乙(翻身) | ? | ? | ? | ? | ... |
我 | ? | ? | ? | ? | ... |
這個(gè)和我們的手勢(shì)識(shí)別不能說(shuō)是很像,只能說(shuō)是一模一樣,在已有的手勢(shì)中(甲乙)找一個(gè)和我(喜好)相似度較高的,每一行其實(shí)就是一系列采樣點(diǎn),最終可以簡(jiǎn)單的推斷出我(可能)是條咸魚??,還喜歡摸魚。
又比如你打算買一臺(tái)電腦,那大概率是先看下周圍的人用什么,然后你就買什么,從眾本質(zhì)上也是一種相似(大眾的選擇就是方向),近朱者赤近墨者黑嘛。如果你說(shuō)很獨(dú)立自主,自己想買啥買啥,那也是對(duì)的,畢竟這玩意怎么搞都搞不到 100%。
關(guān)于多筆畫(可跳過(guò))
我們本文學(xué)的是單筆畫,現(xiàn)在你可以稍微想下,如果是多筆畫應(yīng)該怎么搞?這里還是可以短暫的思考幾秒種??。。。
- 對(duì)于簡(jiǎn)單的一筆畫來(lái)說(shuō)上面的識(shí)別效果是很不錯(cuò)的,不論是效率和準(zhǔn)確率,但如果是多筆畫,那就復(fù)雜起來(lái)了,比如漢字的識(shí)別(想想就頭大??)。
- 這里就介紹一個(gè)簡(jiǎn)單的識(shí)別方法,就是把多筆畫拆成單筆畫,通過(guò)本文的學(xué)習(xí)你可以求出每個(gè)單筆畫的相似度,然后簡(jiǎn)單求和就可以得到整個(gè)字體的相似度,最后取相似度最高的即可(就這??)。
舉個(gè)具象點(diǎn)的例子,比如十
這個(gè)字(這里僅僅是例子哈,不完全是這樣):
提取每個(gè)漢字的筆畫特征,一般可以采集起始點(diǎn)、終點(diǎn)和中間的轉(zhuǎn)折點(diǎn)。數(shù)據(jù)大概長(zhǎng)下面這個(gè)樣子:
- 處理數(shù)據(jù)(標(biāo)準(zhǔn)化的過(guò)程,比如把每個(gè)字移到畫布中心,縮放成一樣的大?。?/li>
- 比較數(shù)據(jù)(選個(gè)算法,這里就是先判斷下筆畫數(shù),再簡(jiǎn)單的將單筆相似度相加求和) 這就完了?當(dāng)然還差得遠(yuǎn)呢,問(wèn)題一抓一大把。比如:
- 由于存在連筆的情況,一筆可能寫成兩筆,所以我們應(yīng)該允許筆畫的誤差在 2 左右,但是在最終排序時(shí),筆畫數(shù)越接近的,優(yōu)先級(jí)越高。
- 每一筆當(dāng)中至少包含起點(diǎn)和終點(diǎn),中間可能有幾個(gè)拐點(diǎn),如果比較的時(shí)候單筆的坐標(biāo)點(diǎn)數(shù)量不同該怎么處理?一種方式是進(jìn)行插值計(jì)算,另一種方式是取最初的采樣點(diǎn)信息。
采用上述的方式如果我寫了個(gè)丁
字是不是好像也能識(shí)別出來(lái),大體都是一橫一豎,有沒(méi)有什么辦法可以避免呢?當(dāng)然是有的,現(xiàn)在我們每一筆保存的不再是點(diǎn)的坐標(biāo),而是該點(diǎn)與前一個(gè)點(diǎn)連線的角度,如果是每一筆的起始點(diǎn),就拿上一筆的終點(diǎn)作為前一個(gè)點(diǎn),說(shuō)起來(lái)比較抽象,所以我又畫了張圖????(很簡(jiǎn)單的一張圖,不要被嚇到??):
大家想想如果是十
字,在上圖的第二個(gè)角度(綠2)中是不是就可以明顯區(qū)分開了。另外我們只保存了兩兩點(diǎn)之間的角度,還省了不少空間呢。
看起來(lái)好像沒(méi)問(wèn)題了?不,還是差得遠(yuǎn)呢。你想想要是筆畫順序不對(duì)咋整。還是以十
為例,我先寫豎再寫橫咋整。啊這。。。其實(shí)還有其他識(shí)別方法,比如把文字按坐標(biāo)軸切分成四塊,分四段校驗(yàn),這就不深入了,點(diǎn)到即止(畢竟就懂點(diǎn)皮毛)。
小結(jié)
以上就是手勢(shì)識(shí)別的大致思路,雖然看起來(lái)是挺高大上的一個(gè)東西,但是讀完之后應(yīng)該覺(jué)得。不。。算難吧。。。有些東西不是你不會(huì)只是你不知道也沒(méi)去嘗試下,嘿嘿。最后,再次送上項(xiàng)目地址傳送門,順便附上我 canvas 專欄的另外兩篇實(shí)戰(zhàn)文章:
html2canvas 用著有問(wèn)題?手寫一個(gè)就知道為啥了??
??用 canvas 來(lái)畫個(gè)函數(shù)曲線吧!縱享絲滑
更多關(guān)于JS前端canvas手勢(shì)識(shí)別的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序使用navigateTo數(shù)據(jù)傳遞的實(shí)例
這篇文章主要介紹了微信小程序使用navigateTo數(shù)據(jù)傳遞的實(shí)例的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下2017-09-09