亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

 更新時(shí)間: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)呆板,所以這個(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)

    這篇文章主要介紹了微信小程序 轉(zhuǎn)發(fā)功能的實(shí)現(xiàn)的相關(guān)資料,這里提供實(shí)現(xiàn)方法及實(shí)例幫助大家學(xué)習(xí)理解,需要的朋友可以參考下
    2017-08-08
  • 用JS創(chuàng)建一個(gè)錄屏功能

    用JS創(chuàng)建一個(gè)錄屏功能

    這篇文章主要介紹了利用JS創(chuàng)建一個(gè)錄屏功能,創(chuàng)建這個(gè)功能錢我們首先創(chuàng)建一個(gè)HTML文件,包含記錄按鈕和一個(gè)播放標(biāo)簽,下面來看看創(chuàng)建的詳細(xì)過程
    2021-11-11
  • 微信小程序 <swiper-item>標(biāo)簽傳入數(shù)據(jù)

    微信小程序 <swiper-item>標(biāo)簽傳入數(shù)據(jù)

    這篇文章主要介紹了微信小程序 <swiper-item>標(biāo)簽傳入數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下
    2017-05-05
  • JavaScript面試數(shù)組index和對象key問題詳解

    JavaScript面試數(shù)組index和對象key問題詳解

    這篇文章主要為大家介紹了JavaScript面試數(shù)組index和對象key問題詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • JavaScript數(shù)組去重方案

    JavaScript數(shù)組去重方案

    這篇文章主要介紹了JS數(shù)組方案,先總結(jié)一下我們數(shù)組的方法:pop、push、shift、unshift、slice、splice、sort、reverse、concat、join、indexOf、lastIndexOf、map、forEach,下面文章將詳細(xì)對他們做個(gè)詳細(xì)介紹,需要的朋友可以參考一下
    2021-09-09
  • apply?call?bind方法原理及使用場景示例詳解

    apply?call?bind方法原理及使用場景示例詳解

    這篇文章主要為大家介紹了apply&call&bind方法原理及使用場景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-08-08
  • js中ES6繼承和ES5繼承之間的差別

    js中ES6繼承和ES5繼承之間的差別

    這篇文章主要介紹了ES6繼承和ES5繼承,以及兩個(gè)繼承之間的差別,文中運(yùn)用示例以及代碼講解的非常清晰,感興趣的小伙伴可以參考一下
    2021-08-08
  • Tree Shaking實(shí)現(xiàn)方法指南

    Tree Shaking實(shí)現(xiàn)方法指南

    這篇文章主要為大家介紹了Tree Shaking實(shí)現(xiàn)方法指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • 微信小程序 LOL 英雄介紹開發(fā)實(shí)例

    微信小程序 LOL 英雄介紹開發(fā)實(shí)例

    這篇文章主要介紹了微信小程序 LOL 英雄介紹開發(fā)的相關(guān)資料,需要的朋友可以參考下
    2016-09-09
  • JavaScript阻止事件冒泡的方法

    JavaScript阻止事件冒泡的方法

    這篇文章主要介紹了基于JavaScript阻止事件冒泡,事件冒泡?開始時(shí)由最具體的元素接收,然后逐級向上傳播到到?DOM?最頂層節(jié)點(diǎn)。更多詳細(xì)內(nèi)容請需要的小伙伴參考下面文章的具體內(nèi)容希望對你有所幫助
    2021-12-12

最新評論