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