自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

圖形編輯器開(kāi)發(fā):實(shí)現(xiàn)縮放圖形

開(kāi)發(fā) 前端
旋轉(zhuǎn)度數(shù)通常要配合一個(gè)變換中心(Origin),這個(gè)可以作為一個(gè)屬性讓用戶設(shè)置。但我更建議將 x、y、Width、Height 形成的 矩形的中點(diǎn) 作為旋轉(zhuǎn)中心,這樣更簡(jiǎn)單一些,減少用戶的心智負(fù)擔(dān),也防止出現(xiàn)用戶設(shè)置一些奇怪 Origin 的場(chǎng)景。

編輯器 github 地址:

https://github.com/F-star/suika

線上體驗(yàn):

https://blog.fstars.wang/app/suika/

圖形的屬性

圖形有幾個(gè)重要的基礎(chǔ)屬性,會(huì)經(jīng)常被用到,我們?cè)趯?shí)現(xiàn)縮放圖形前需要理清一下它們。

  • x / y
  • width / height
  • rotation

位置和大小

x 和 y 為圖形的左上角位置,注意是旋轉(zhuǎn)前的。

x、y 旋轉(zhuǎn)后我們叫做 rotatedX、rotatedY,屬性面板中會(huì)用到。

width 和 height 為圖形的寬高,這個(gè)沒(méi)什么好說(shuō)的。

另外,有些圖形有些特殊,它的 x、y、width、height 是要通過(guò)其他屬性計(jì)算出來(lái)的,比如貝塞爾曲線。

旋轉(zhuǎn)

rotation 為圖形的旋轉(zhuǎn)度數(shù),通常使用 弧度單位。

因?yàn)榛《仁菙?shù)學(xué)計(jì)算中的???,各種 API 都是要求提供弧度的,比如內(nèi)置的 Math.sin() 方法。

你存角度自然也是可以,但不推薦,但計(jì)算時(shí)多了一層多余的單位轉(zhuǎn)換,且丟失一些微小的精度。

當(dāng)然 UI 層還是要展示角度,因?yàn)槭敲嫦蛴脩舻?,?duì)于數(shù)據(jù)和 UI 不統(tǒng)一的問(wèn)題,在 UI 層做一個(gè)轉(zhuǎn)換即可。

旋轉(zhuǎn)度數(shù)通常要配合一個(gè)變換中心(origin),這個(gè)可以作為一個(gè)屬性讓用戶設(shè)置。

但我更建議將 x、y、width、height 形成的 矩形的中點(diǎn) 作為旋轉(zhuǎn)中心,這樣更簡(jiǎn)單一些,減少用戶的心智負(fù)擔(dān),也防止出現(xiàn)用戶設(shè)置一些奇怪 origin 的場(chǎng)景。

下圖中,紅色矩形是藍(lán)色矩陣順時(shí)針旋轉(zhuǎn) 45 度得到。

旋轉(zhuǎn)度數(shù)還要考慮 旋轉(zhuǎn)方向、基準(zhǔn)角度、取值范圍 問(wèn)題。

(因?yàn)榛《炔恢庇^,后面會(huì)用角度來(lái)描述,但數(shù)據(jù)層依舊還是用的弧度)

  • 旋轉(zhuǎn)方向:設(shè)置旋轉(zhuǎn)后,圖形是會(huì)往順時(shí)針?lè)较蜻€是逆時(shí)針?lè)较蛐D(zhuǎn)。
  • 基準(zhǔn)角度:朝向哪里是 0 度。
  • 取值范圍:通常為 [0, 360) 和 (-180, 180]。二者其實(shí)等價(jià),只是顯示有區(qū)別,后者其實(shí)只是前者減去 180 度。

通常這些編輯器自己決定就好。像我的項(xiàng)目,向上表示 0 度,順時(shí)針?lè)较驗(yàn)樾D(zhuǎn)方向,方向取值為 [0, 360)。

一些編輯器是支持用戶自己設(shè)置的,比如 AutoCAD 可通過(guò)圖形單位命令,設(shè)置旋轉(zhuǎn)方向和基準(zhǔn)角度。

圖片

縮放實(shí)現(xiàn)思路

進(jìn)入正題,對(duì)圖形進(jìn)行縮放。

接下來(lái)會(huì)以通過(guò)右下角(也叫東南 se 方向) 縮放控制點(diǎn)縮放為例進(jìn)行講解。

交互邏輯:

選擇工具下,當(dāng)光標(biāo)落在右下角的縮放控制點(diǎn)上時(shí),光標(biāo)會(huì)變成縮放樣式(這個(gè)不是本文核心,不講)。

此時(shí)按下鼠標(biāo),然后進(jìn)行拖拽,即可對(duì)圖形以左上角為縮放中心,進(jìn)行縮放。

實(shí)現(xiàn)思路:更新 width 和 height,然后確定參照點(diǎn),修正 x  和 y。

按下鼠標(biāo)時(shí),我們要把當(dāng)前圖形的 x、y、width、height、rotation 記錄下來(lái)。之后的縮放是基于這個(gè)初始狀態(tài)進(jìn)行的。

const mousedown = (e) => {
  // ...
  
  // 縮放前圖形的屬性,之后我們會(huì)直接更新圖形屬性,導(dǎo)致原來(lái)的屬性丟失,所以要記錄下這個(gè)快照。
  prevElement = {
    x: item.x,
    y: item.y,
    width: item.width,
    height: item.height,
    rotation: item.rotation ?? 0,
  }
}

拖拽時(shí),調(diào)用我們將要實(shí)現(xiàn)的 movePoint 方法,去更新這個(gè)圖形。

const drag = (e) = {
  // ...
  
  selectElement.movePoint(
    'se', // 縮放控制點(diǎn)類型:右下(或東南)
    lastPoint, // 當(dāng)前光標(biāo)位置(基于場(chǎng)景坐標(biāo)系)
    prevElement, // 縮放前的屬性快照
  );
}

下面就是核心方法 movePoint 的實(shí)現(xiàn)邏輯了。

更新 width 和 height

首先是更新矩形寬高。

因?yàn)橛幸粋€(gè)旋轉(zhuǎn),所以算法不會(huì)這么直觀。

我們要意識(shí)到這里有一個(gè)變換。看到的圖形,是做過(guò)變換(基于矩形中心旋轉(zhuǎn))之后的,但我們需要修改的 width、height、x、y 則是旋轉(zhuǎn)前的。

所以我們需要把光標(biāo)位置給旋轉(zhuǎn)回來(lái),然后再減去 x 和 y 去得到真正的 width 和 height。

看看代碼

class Graph {
  // ...

  // 根據(jù)縮放點(diǎn)更新圖形
  movePoint(type, newPos, oldBox) {
    // 1. 計(jì)算 width 和 height
    // 計(jì)算縮放中心(也就是矩形的中點(diǎn))
    const cx = oldBox.x + oldBox.width / 2;
    const cy = oldBox.y + oldBox.height / 2;

    // 計(jì)算反向旋轉(zhuǎn)的光標(biāo)位置
    const { x: posX, y: poxY } = transformRotate(
      newPos.x,
      newPos.y,
      -(oldBox.rotation || 0), // 注意這里是負(fù)數(shù)
      cx,
      cy
    );
    
    let width = 0;
    let height = 0;
    if (type === 'se') {
      // 參照點(diǎn)為左上角(x 和 y)
      // 新的寬高自然就是光標(biāo)位置減去 x、y
      width = posX - oldBox.x;
      height = poxY - oldBox.y;
    }
    // 其他控制點(diǎn)的邏輯暫且省略...
    
    // 2. 計(jì)算 x 和 y
    // ...
  }
}

看看只更新寬高的效果。

可以看到是有問(wèn)題的,因?yàn)樾薷膶捀吆螅匦蔚闹行狞c(diǎn)也發(fā)生了變化,導(dǎo)致縮放中心錯(cuò)誤。所以我們要修正一下 x 和 y。

修正 x 和 y

接著我們就要修正 x 和 y 的值。

重點(diǎn)就一句話:縮放前的參考點(diǎn)和縮放后的參考點(diǎn)的位置要保持一致。這個(gè)參考點(diǎn)其實(shí)就是圖形縮放過(guò)程中的縮放中心。

對(duì)于右下角縮放控制點(diǎn),它的縮放中心就是左上角,即 x 和 y 經(jīng)過(guò)旋轉(zhuǎn)的位置。

class Graph {
  // ...

  movePoint(type, newPos, oldBox) {
    // 1. 計(jì)算 width 和 height
    // ...
    

    // 2. 計(jì)算 x 和 y

    // 設(shè)置參照點(diǎn),不同縮放類型的參照點(diǎn)不同
    let prevOriginX = 0;
    let prevOriginY = 0;
    let originX = 0;
    let originY = 0;
    if (type === "se") {
      prevOriginX = oldBox.x;
      prevOriginY = oldBox.y;
      originX = oldBox.x;
      originY = oldBox.y;
    }
    // 其他縮放類型暫且省略

    // 縮放前的參考點(diǎn)位置
    const { x: prevRotatedOriginX, y: prevRotatedOriginY } = transformRotate(
      prevOriginX,
      prevOriginY,
      oldBox.rotation || 0,
      cx,
      cy
    );
    // 縮放后的參考點(diǎn)位置
    const { x: rotatedOriginX, y: rotatedOriginY } = transformRotate(
      originX,
      originY,
      oldBox.rotation || 0,
      oldBox.x + width / 2, // 旋轉(zhuǎn)中心是新的
      oldBox.y + height / 2
    );
    // 計(jì)算新舊兩個(gè)參考點(diǎn)的差值,對(duì) x、y 進(jìn)行補(bǔ)正
    const dx = rotatedOriginX - prevRotatedOriginX;
    const dy = rotatedOriginY - prevRotatedOriginY;
    const x = oldBox.x - dx;
    const y = oldBox.y - dy;
  }
}

width 和 height 可能為負(fù)數(shù),這里要做一個(gè)標(biāo)準(zhǔn)化,然后賦值給圖形屬性即可。

this.setAttrs(
  normalizeRect({
    x,
    y,
    width,
    height,
  }),
);

其他縮放控制點(diǎn)

對(duì)于其他類型縮放控制點(diǎn),比如左上、右上、左下縮放控制點(diǎn),它們的大框架是一樣的,只是 width 和 height 計(jì)算方式不同,以及參考點(diǎn)不同。

不同類型下 width 和 height 的設(shè)置:

let width = 0;
let height = 0;
if (type === 'se') { // 右下
  width = posX - oldBox.x;
  height = poxY - oldBox.y;
} else if (type === 'ne') { // 右上
  width = posX - oldBox.x;
  height = oldBox.y + oldBox.height - poxY;
} else if (type === 'nw') {
  width = oldBox.x + oldBox.width - posX;
  height = oldBox.y + oldBox.height - poxY;
} else if (type === 'sw') {
  width = oldBox.x + oldBox.width - posX;
  height = poxY - oldBox.y;
}

新舊參考點(diǎn)設(shè)置:

let prevOriginX = 0;
let prevOriginY = 0;
let originX = 0;
let originY = 0;
if (type === 'se') {
  prevOriginX = oldBox.x; // 右下縮放點(diǎn),參考點(diǎn)為左上角
  prevOriginY = oldBox.y;
  originX = oldBox.x;
  originY = oldBox.y;
} else if (type === 'ne') { // 右上縮放點(diǎn),參考點(diǎn)為左下角
  prevOriginX = oldBox.x;
  prevOriginY = oldBox.y + oldBox.height;
  originX = oldBox.x;
  originY = oldBox.y + height;
} else if (type === 'nw') {
  prevOriginX = oldBox.x + oldBox.width;
  prevOriginY = oldBox.y + oldBox.height;
  originX = oldBox.x + width;
  originY = oldBox.y + height;
} else if (type === 'sw') {
  prevOriginX = oldBox.x + oldBox.width;
  prevOriginY = oldBox.y;
  originX = oldBox.x + width;
  originY = oldBox.y;
}

暫時(shí)沒(méi)實(shí)現(xiàn)正北、正南、正西、正東的邏輯,邏輯大差不差。

鎖定縮放比

按住 shift 可以鎖定縮放比。

做法是對(duì)比新舊圖形寬高比,將 width 和 height 其中一個(gè)進(jìn)行修正即可。注意正負(fù)號(hào)。

方法需要多傳一個(gè) keepRatio 的參數(shù):

class Graph {
  // ...

  movePoint(type, newPos, oldBox, keepRatio = false) {
    // 1. 計(jì)算 width 和 height
    // ...
    
    if (keepRatio) {
      const ratio = oldBox.width / oldBox.height;
      const newRatio = Math.abs(width / height);
      if (newRatio > ratio) {
        height = (Math.sign(height) * Math.abs(width)) / ratio;
      } else {
        width = Math.sign(width) * Math.abs(height) * ratio;
      }
    }
    
    // 2. 計(jì)算 x 和 y
    // ...
  }
}

貌似沒(méi)考慮除數(shù) height 為 0 的情況..

優(yōu)化點(diǎn)

本文的實(shí)現(xiàn)是考慮的是比較簡(jiǎn)單的縮放圖形場(chǎng)景,一些更復(fù)雜的場(chǎng)景并未實(shí)現(xiàn)。

縮放還有另一種策略,就是會(huì)產(chǎn)生 反向顛倒 的縮放。要實(shí)現(xiàn)這個(gè)效果,需要引入縮放屬性,復(fù)雜度會(huì)提升很多。

另外就是選中多個(gè)圖形,然后縮放的場(chǎng)景我沒(méi)實(shí)現(xiàn)。這種場(chǎng)景下,通常是要鎖定寬高比的。

否則就會(huì)出現(xiàn)圖形的斜切效果,這個(gè)如果要實(shí)現(xiàn),我們還要引入斜切屬性,復(fù)雜度再一次提升。

下面是 Figma 的效果,真是讓人頭扁。

按住 Alt 實(shí)現(xiàn)圖形中心縮放也沒(méi)做,這個(gè)比較簡(jiǎn)單,有空再做。

讀者如果看懂我這篇文章,心里應(yīng)該有思路的:width、height 的計(jì)算要加入圖形中點(diǎn)參數(shù),參照點(diǎn)設(shè)置為圖形中點(diǎn)。

責(zé)任編輯:姜華 來(lái)源: 前端西瓜哥
相關(guān)推薦

2023-09-26 07:39:21

2023-07-07 13:56:01

圖形編輯器畫布縮放

2024-01-03 08:43:17

圖形編輯器旋轉(zhuǎn)控制點(diǎn)縮放控制點(diǎn)

2023-08-31 11:32:57

圖形編輯器contain

2023-09-07 08:24:35

圖形編輯器開(kāi)發(fā)繪制圖形工具

2023-02-01 09:21:59

圖形編輯器標(biāo)尺

2023-04-07 08:02:30

圖形編輯器對(duì)齊功能

2023-09-11 09:02:31

圖形編輯器模塊間的通信

2024-01-08 08:30:05

光標(biāo)圖形編輯器開(kāi)發(fā)游標(biāo)

2023-04-10 08:45:44

圖形編輯器排列移動(dòng)功能

2023-07-31 08:46:07

圖形編輯器圖形自動(dòng)對(duì)齊

2023-08-28 08:10:50

Hex圖形編輯器

2023-10-08 08:11:40

圖形編輯器快捷鍵操作

2023-10-10 16:04:30

圖形編輯器格式轉(zhuǎn)換

2023-01-18 08:30:40

圖形編輯器元素

2023-02-09 07:02:30

圖形編輯器修改圖形

2023-02-06 16:59:57

Canvas編輯器

2023-10-20 08:02:25

圖形編輯器前端

2023-02-02 14:07:00

圖形編輯器Canvas

2023-05-09 08:15:32

圖形編輯器撤銷重做功能
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)