最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

JS前端canvas交互實(shí)現(xiàn)拖拽旋轉(zhuǎn)及縮放示例

 更新時間:2022年08月03日 09:01:29   作者:尤水就下  
這篇文章主要為大家介紹了JS前端canvas交互實(shí)現(xiàn)拖拽旋轉(zhuǎn)及縮放示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

正文

到目前為止,我們已經(jīng)能夠?qū)ξ矬w進(jìn)行點(diǎn)選和框選的操作了,但是這還不夠,因?yàn)椴]有什么實(shí)際性的改變,并且畫布看起來也有點(diǎn)呆板,所以這個章節(jié)的主要目的就是讓畫布中的物體活起來,其實(shí)就是增加一些常見的交互而已啦??,比如拖拽、旋轉(zhuǎn)和縮放。這是這個系列最重要的章節(jié)之一,希望能夠?qū)δ阌兴鶐椭?/p>

拖拽

先來說說拖拽平移的實(shí)現(xiàn)吧,因?yàn)樗顬楹唵??。我們知道每個物體都是有 top 和 left 值來表示物體位置的,所以平移的時候只需要簡單的更新下物體的 top 和 left 值即可,然后每次移動都會觸發(fā) renderAll 方法進(jìn)行重新渲染,于是就自然而然的在新的位置繪制物體了。

這個就是典型的數(shù)據(jù)與視圖分離,這個章節(jié)包括接下來的章節(jié)我們一般都不需要去修改物體的 render 方法了,但凡畫布上有物體在動(物體狀態(tài)改變了),我們都只需要更新物體的數(shù)據(jù)就行,而不用去關(guān)心如何繪制,反正值改了會自然而然的反應(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)時記錄的一些初始信息,大概長下面這個樣子,看看就行,有個印象即可????:

em...,沒錯,拖拽平移的部分就那么短,畢竟確實(shí)簡單。

旋轉(zhuǎn)

再來說下旋轉(zhuǎn)吧,旋轉(zhuǎn)也比較簡單。我們知道每個物體都是有一個 angle 變量來表示物體旋轉(zhuǎn)角度的,當(dāng)對物體進(jìn)行旋轉(zhuǎn)操作的時候,我們可以先計(jì)算出拖拽旋轉(zhuǎn)的角度 deltaAngle,于是新的 angle = 舊的 angle + deltaAngle,然后重新賦值 angle 變量即可,同樣的這個過程中也不會涉及修改物體的 _render 方法,只不過比平移稍微麻煩點(diǎn)的就是這個變換的角度該怎么計(jì)算呢?

其實(shí)旋轉(zhuǎn)的過程本質(zhì)就是鼠標(biāo)點(diǎn)的旋轉(zhuǎn),也就是說我們只要計(jì)算出當(dāng)前鼠標(biāo)點(diǎn)和初始鼠標(biāo)點(diǎn)之間的角度就行。就像下面這張圖一樣:

我們先來看看一個點(diǎn)的情況下,怎么算這個點(diǎn)的朝向,一般我們算的是該點(diǎn)與原點(diǎn)的連線和 x 軸正方向之間的逆時針方向的夾角,如下圖所示:

通常我們會用 radian = Math.atan2(y, x) 來計(jì)算弧度,注意是弧度(radian)不是角度(angle),所以再提醒下,canvas 中用的都是弧度,但是角度方便我們理解,所以時不時需要轉(zhuǎn)換;

另外要注意我們用的是 Math.atan2 而不是 Math.atan,雖然它們大同小異,但是我們不能根據(jù) atan 的值來確定唯一的方向,比如點(diǎn)(1, 1)和點(diǎn)(-1, -1),它們的 atan 值都一樣,但是方向確相反,所以有了 atan2,atan2 的取值范圍在 [-Math.PI, Math.PI] 之間,并且四個象限的取值各不相同,所以一般都是用它來計(jì)算。

知道了這些計(jì)算就簡單了,原點(diǎn)就是物體的中心點(diǎn),鼠標(biāo)按下的點(diǎn)可以與物體中心點(diǎn)相連形成一個起始角度,鼠標(biāo)拖拽時的點(diǎn)也可以與物體中心點(diǎn)相連形成一個最終角度,用最終角度-起始角度就能得到要變換的角度了。

切記,通常情況下我們對什么物體進(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;
}

縮放

再來就是縮放啦,這個又比上面的旋轉(zhuǎn)稍微麻煩些,這里我們以右邊中間的縮放控制點(diǎn)為例子,其他控制點(diǎn)是一個意思(復(fù)制改改就行),先看看效果????:

大家仔細(xì)看上圖中右邊中間紅色的那個控制點(diǎn),它的縮放結(jié)果其實(shí)是就沿著 x 軸拉伸,本能的想法是什么呢?就是計(jì)算出水平方向的拖拽距離 dx,然后去改變物體的寬度,就像這樣 object.width += dx,但是如果 width 變成了負(fù)數(shù)怎么辦,是不是也要處理一下,簡單點(diǎn)的做法就是我們可以限制個最小值,如果是右邊的控制點(diǎn)拉到最左邊了,就不允許再拉了。

不過,不知道你還記得我們早前說過的一個知識點(diǎn)么???就是我們一般不會去改變物體自身的大小,而是去修改物體的變換值,所以縮放的本質(zhì)也僅僅是改變物體的 scaleX 和 scaleY 值。還是以拖拽右邊中間控制點(diǎn)的拉伸為例子,這次我們算的是 scaleX,怎么算這個值會方便點(diǎn)呢?可以將拉伸的變換基點(diǎn)暫時變?yōu)樽筮呏虚g的控制點(diǎn),也就是左邊的藍(lán)點(diǎn)(這個很重要),這樣計(jì)算當(dāng)前寬度的時候就會比較方便了:

  • 當(dāng)前寬度 = 鼠標(biāo)位置的 x - 左邊中間控制點(diǎn)的位置的 x
  • scaleX = 當(dāng)前寬度 / 自身寬度 記住,我們自身 width 的值并沒有改變,只是改變了 scaleX 值。同樣的它也有反向拉伸的問題,但我們可以變通處理一下,臨時變換下拉伸基點(diǎn)。什么意思呢???就是一旦變成反向拉伸,我們就立馬切換成按左邊中間控制點(diǎn)拖拽的邏輯執(zhí)行,也就是變成拖拽藍(lán)點(diǎn),而紅點(diǎn)變成了參考基點(diǎn),大家可以再好好看看上面那個動圖體會下。
  • 當(dāng)然這樣還不夠,拖拽縮放的時候還有個問題,就是 top 和 left 值也會隨之改變,所以算完 scaleX 之后還需要對這兩個值進(jìn)行更新,大家注意看上面那個動圖中的黑點(diǎn)就能體會到了。然后再提醒兩個點(diǎn):
  • 就是縮放的時候中心點(diǎn)并不是在物體的中心,所以我們可以簡單的理解成單邊縮放;當(dāng)然其實(shí)也可以沿著中心點(diǎn)縮放,只不過我們講解的是默認(rèn)的形式;
  • 如果是豎直拉伸,只要把 x 換成 y,把寬度換成高度即可,如果是右下角那個控制點(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)按下的時候會記錄物體的狀態(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';
    }
    // 縮放會改變物體位置,所以要重新設(shè)置
    target.setPositionByOrigin(constraintPosition, t.originX, t.originY);
}

這個變換看起來麻煩點(diǎn),所以我單獨(dú)寫了個小 demo,有興趣的可以點(diǎn)擊這個鏈接單獨(dú)查看。建議大家多動手試試,記住,最核心的要點(diǎn)就是:

我們不改變物體自身的寬高大小,也不改變物體的渲染方法,而只是改變?nèi)N變換的值。

可能有的同學(xué)還會問到上面的變換操作在鼠標(biāo)移動時會不停的調(diào)用 renderAll 這個渲染函數(shù),性能是不是一般啊,尤其是當(dāng)物體一多就更不咋地了?

那肯定是這樣的,在前端,不管啥東西,只要東西多了就會垮掉,比如數(shù)據(jù)多了就得分頁,虛擬滾動;元素多了能不繪制就不繪制。

當(dāng)然在 canvas 中也有它的解法,比如緩存、分層、上 webgl 等等,這個在后續(xù)的優(yōu)化章節(jié)中會專門講到,所以敬請期待吧。不過還是要說一下,性能這東西,我覺得吧,一個普通頁面一般是很少會遇到的,所以等遇到了再去考慮解決和優(yōu)化也不遲,不然就屬于過度優(yōu)化了(沒必要),不過在 canvas 中性能是個比較普遍的問題,你很容易寫出卡卡的 canvas,所以我們還是有必要講一講的??。

小結(jié)

本個章節(jié)我們主要講的是物體的一些變換操作,本來感覺應(yīng)該是件很難的事情,但是歸功于我們之前做了很好的結(jié)構(gòu)劃分,也就是將數(shù)據(jù)和渲染層分離,所以這一趴其實(shí)我們最核心的就是只改變了數(shù)據(jù),其它什么都沒變,這種感覺就像什么。。。那是數(shù)據(jù)驅(qū)動視圖的味道,哈哈??。扯犢子了,這里就簡單總結(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)一個輕量 fabric.js 系列三(物體基類)??

實(shí)現(xiàn)一個輕量 fabric.js 系列二(畫布初始化)??

實(shí)現(xiàn)一個輕量 fabric.js 系列一(摸透 canvas)??

更多關(guān)于JS前端canvas交互的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論

西昌市| 浑源县| 团风县| 若羌县| 镇安县| 汝城县| 象州县| 唐海县| 当阳市| 夏邑县| 怀来县| 临泉县| 灌阳县| 攀枝花市| 罗田县| 民勤县| 财经| 明溪县| 勐海县| 陇西县| 枝江市| 宣恩县| 繁峙县| 宜兰县| 镇坪县| 刚察县| 怀宁县| 吉水县| 颍上县| 财经| 和静县| 蒙山县| 黄骅市| 徐州市| 江川县| 隆子县| 卓资县| 道孚县| 柏乡县| 竹溪县| 米易县|