JS前端canvas交互實現(xiàn)拖拽旋轉(zhuǎn)及縮放示例
正文
到目前為止,我們已經(jīng)能夠?qū)ξ矬w進行點選和框選的操作了,但是這還不夠,因為并沒有什么實際性的改變,并且畫布看起來也有點呆板,所以這個章節(jié)的主要目的就是讓畫布中的物體活起來,其實就是增加一些常見的交互而已啦??,比如拖拽、旋轉(zhuǎn)和縮放。這是這個系列最重要的章節(jié)之一,希望能夠?qū)δ阌兴鶐椭?/p>
拖拽
先來說說拖拽平移的實現(xiàn)吧,因為它最為簡單??。我們知道每個物體都是有 top 和 left 值來表示物體位置的,所以平移的時候只需要簡單的更新下物體的 top 和 left 值即可,然后每次移動都會觸發(fā) renderAll 方法進行重新渲染,于是就自然而然的在新的位置繪制物體了。
這個就是典型的數(shù)據(jù)與視圖分離,這個章節(jié)包括接下來的章節(jié)我們一般都不需要去修改物體的 render 方法了,但凡畫布上有物體在動(物體狀態(tài)改變了),我們都只需要更新物體的數(shù)據(jù)就行,而不用去關心如何繪制,反正值改了會自然而然的反應到畫布上,這點很重要。
然后簡單看下平移的代碼????:
/** 平移當前選中物體 */ _translateObject(x: number, y: number) { const target = this._currentTransform.target; target.set('left', x - this._currentTransform.offsetX); // offsetX 是畫布整體偏移 target.set('top', y - this._currentTransform.offsetY); // offsetY 是畫布整體偏移 }
是的,代碼就那么點,也不難理解,因為物體的繪制方法是固定的,我們所做的任何變換操作都僅僅是單純的修改數(shù)據(jù)而已。不過要提下上面代碼中的 _currentTransform
是什么東西,它就是一開始我們按下鼠標時記錄的一些初始信息,大概長下面這個樣子,看看就行,有個印象即可????:
em...,沒錯,拖拽平移的部分就那么短,畢竟確實簡單。
旋轉(zhuǎn)
再來說下旋轉(zhuǎn)吧,旋轉(zhuǎn)也比較簡單。我們知道每個物體都是有一個 angle 變量來表示物體旋轉(zhuǎn)角度的,當對物體進行旋轉(zhuǎn)操作的時候,我們可以先計算出拖拽旋轉(zhuǎn)的角度 deltaAngle,于是新的 angle = 舊的 angle + deltaAngle,然后重新賦值 angle 變量即可,同樣的這個過程中也不會涉及修改物體的 _render
方法,只不過比平移稍微麻煩點的就是這個變換的角度該怎么計算呢?
其實旋轉(zhuǎn)的過程本質(zhì)就是鼠標點的旋轉(zhuǎn),也就是說我們只要計算出當前鼠標點和初始鼠標點之間的角度就行。就像下面這張圖一樣:
我們先來看看一個點的情況下,怎么算這個點的朝向,一般我們算的是該點與原點的連線和 x 軸正方向之間的逆時針方向的夾角,如下圖所示:
通常我們會用 radian = Math.atan2(y, x) 來計算弧度,注意是弧度(radian)不是角度(angle),所以再提醒下,canvas 中用的都是弧度,但是角度方便我們理解,所以時不時需要轉(zhuǎn)換;
另外要注意我們用的是 Math.atan2 而不是 Math.atan,雖然它們大同小異,但是我們不能根據(jù) atan 的值來確定唯一的方向,比如點(1, 1)和點(-1, -1),它們的 atan 值都一樣,但是方向確相反,所以有了 atan2,atan2 的取值范圍在 [-Math.PI, Math.PI] 之間,并且四個象限的取值各不相同,所以一般都是用它來計算。
知道了這些計算就簡單了,原點就是物體的中心點,鼠標按下的點可以與物體中心點相連形成一個起始角度,鼠標拖拽時的點也可以與物體中心點相連形成一個最終角度,用最終角度-起始角度就能得到要變換的角度了。
切記,通常情況下我們對什么物體進行旋轉(zhuǎn),原點就是物體的中心點。下面是核心的代碼示例,代碼不多也好消化????:
/** 旋轉(zhuǎn)當前選中物體 */ _rotateObject(x: number, y: number) { const t = this._currentTransform; const o = this._offset; // 鼠標按下的點與物體中心點連線和 x 軸正方向形成的弧度 const lastRadian = Math.atan2(t.ey - o.top - t.top, t.ex - o.left - t.left); // 鼠標拖拽的終點與物體中心點連線和 x 軸正方向形成的弧度 const curRadian = Math.atan2(y - o.top - t.top, x - o.left - t.left); const deltaRadian = curRadian - lastRadian; let angle = Util.radiansToDegrees(t.theta + deltaRadian); // 新的角度 = 原來的角度 + 變換的角度 if (angle < 0) angle = 360 + angle; angle = angle % 360; t.target.angle = angle; }
縮放
再來就是縮放啦,這個又比上面的旋轉(zhuǎn)稍微麻煩些,這里我們以右邊中間的縮放控制點為例子,其他控制點是一個意思(復制改改就行),先看看效果????:
大家仔細看上圖中右邊中間紅色的那個控制點,它的縮放結(jié)果其實是就沿著 x 軸拉伸,本能的想法是什么呢?就是計算出水平方向的拖拽距離 dx,然后去改變物體的寬度,就像這樣 object.width += dx
,但是如果 width 變成了負數(shù)怎么辦,是不是也要處理一下,簡單點的做法就是我們可以限制個最小值,如果是右邊的控制點拉到最左邊了,就不允許再拉了。
不過,不知道你還記得我們早前說過的一個知識點么???就是我們一般不會去改變物體自身的大小,而是去修改物體的變換值,所以縮放的本質(zhì)也僅僅是改變物體的 scaleX 和 scaleY 值。還是以拖拽右邊中間控制點的拉伸為例子,這次我們算的是 scaleX,怎么算這個值會方便點呢?可以將拉伸的變換基點暫時變?yōu)樽筮呏虚g的控制點,也就是左邊的藍點(這個很重要),這樣計算當前寬度的時候就會比較方便了:
- 當前寬度 = 鼠標位置的 x - 左邊中間控制點的位置的 x
- scaleX = 當前寬度 / 自身寬度 記住,我們自身 width 的值并沒有改變,只是改變了 scaleX 值。同樣的它也有反向拉伸的問題,但我們可以變通處理一下,臨時變換下拉伸基點。什么意思呢???就是一旦變成反向拉伸,我們就立馬切換成按左邊中間控制點拖拽的邏輯執(zhí)行,也就是變成拖拽藍點,而紅點變成了參考基點,大家可以再好好看看上面那個動圖體會下。
- 當然這樣還不夠,拖拽縮放的時候還有個問題,就是 top 和 left 值也會隨之改變,所以算完 scaleX 之后還需要對這兩個值進行更新,大家注意看上面那個動圖中的黑點就能體會到了。然后再提醒兩個點:
- 就是縮放的時候中心點并不是在物體的中心,所以我們可以簡單的理解成單邊縮放;當然其實也可以沿著中心點縮放,只不過我們講解的是默認的形式;
- 如果是豎直拉伸,只要把 x 換成 y,把寬度換成高度即可,如果是右下角那個控制點就把 xy 的代碼都加上即可;
這里也簡單貼下核心代碼????:
/** * 縮放當前選中物體 * @param x 鼠標點 x * @param y 鼠標點 y * @param by 是否等比縮放,x | y | equally */ _scaleObject(x: number, y: number, by = 'equally') { let t = this._currentTransform, // 在鼠標按下的時候會記錄物體的狀態(tài) offset = this._offset, // 畫布偏移 target: FabricObject = t.target; // 縮放基點:比如拖拽右邊中間的控制點,其實我們參考的變換基點是左邊中間的控制點 let constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); // 以物體變換中心為原點的鼠標點坐標值 let localMouse = target.toLocalPoint(new Point(x - offset.left, y - offset.top), t.originX, t.originY); if (t.originX === 'right') { localMouse.x *= -1; } // 計算新的縮放值,以變換中心為原點,根據(jù)本地鼠標坐標點/原始寬度進行計算,重新設定物體縮放值 let newScaleX = target.scaleX; if (by === 'x') { newScaleX = localMouse.x / (target.width + target.padding); target.set('scaleX', newScaleX); } // 如果是反向拉伸 x if (newScaleX < 0) { if (t.originX === 'left') t.originX = 'right'; else if (t.originX === 'right') t.originX = 'left'; } // 縮放會改變物體位置,所以要重新設置 target.setPositionByOrigin(constraintPosition, t.originX, t.originY); }
這個變換看起來麻煩點,所以我單獨寫了個小 demo,有興趣的可以點擊這個鏈接單獨查看。建議大家多動手試試,記住,最核心的要點就是:
我們不改變物體自身的寬高大小,也不改變物體的渲染方法,而只是改變?nèi)N變換的值。
可能有的同學還會問到上面的變換操作在鼠標移動時會不停的調(diào)用 renderAll 這個渲染函數(shù),性能是不是一般啊,尤其是當物體一多就更不咋地了?
那肯定是這樣的,在前端,不管啥東西,只要東西多了就會垮掉,比如數(shù)據(jù)多了就得分頁,虛擬滾動;元素多了能不繪制就不繪制。
當然在 canvas 中也有它的解法,比如緩存、分層、上 webgl 等等,這個在后續(xù)的優(yōu)化章節(jié)中會專門講到,所以敬請期待吧。不過還是要說一下,性能這東西,我覺得吧,一個普通頁面一般是很少會遇到的,所以等遇到了再去考慮解決和優(yōu)化也不遲,不然就屬于過度優(yōu)化了(沒必要),不過在 canvas 中性能是個比較普遍的問題,你很容易寫出卡卡的 canvas,所以我們還是有必要講一講的??。
小結(jié)
本個章節(jié)我們主要講的是物體的一些變換操作,本來感覺應該是件很難的事情,但是歸功于我們之前做了很好的結(jié)構(gòu)劃分,也就是將數(shù)據(jù)和渲染層分離,所以這一趴其實我們最核心的就是只改變了數(shù)據(jù),其它什么都沒變,這種感覺就像什么。。。那是數(shù)據(jù)驅(qū)動視圖的味道,哈哈??。扯犢子了,這里就簡單總結(jié)下三種基本的操作吧:
- 拖拽,計算新的 top、left
- 旋轉(zhuǎn),計算新的 angle
- 縮放,計算新的 scaleX、scaleY
其實三種變換操作的本質(zhì)就是依托于鼠標坐標點的計算,啪??,沒了。
然后這里是簡版 fabric.js 的代碼鏈接,有興趣的可以看看。好啦,本次分享就到這里
canvas 中物體邊框和控制點的實現(xiàn)(四)??
實現(xiàn)一個輕量 fabric.js 系列三(物體基類)??
實現(xiàn)一個輕量 fabric.js 系列二(畫布初始化)??
實現(xiàn)一個輕量 fabric.js 系列一(摸透 canvas)??
更多關于JS前端canvas交互的資料請關注腳本之家其它相關文章!
相關文章
微信小程序 轉(zhuǎn)發(fā)功能的實現(xiàn)
這篇文章主要介紹了微信小程序 轉(zhuǎn)發(fā)功能的實現(xiàn)的相關資料,這里提供實現(xiàn)方法及實例幫助大家學習理解,需要的朋友可以參考下2017-08-08微信小程序 <swiper-item>標簽傳入數(shù)據(jù)
這篇文章主要介紹了微信小程序 <swiper-item>標簽傳入數(shù)據(jù)的相關資料,需要的朋友可以參考下2017-05-05JavaScript面試數(shù)組index和對象key問題詳解
這篇文章主要為大家介紹了JavaScript面試數(shù)組index和對象key問題詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12