国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

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

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

編輯器 github 地址:

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

線上體驗:

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

圖形的屬性

圖形有幾個重要的基礎(chǔ)屬性,會經(jīng)常被用到,我們在實現(xiàn)縮放圖形前需要理清一下它們。

  • x / y
  • width / height
  • rotation

位置和大小

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

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

width 和 height 為圖形的寬高,這個沒什么好說的。

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

旋轉(zhuǎn)

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

因為弧度是數(shù)學(xué)計算中的常客,各種 API 都是要求提供弧度的,比如內(nèi)置的 Math.sin() 方法。

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

當(dāng)然 UI 層還是要展示角度,因為是面向用戶的,對于數(shù)據(jù)和 UI 不統(tǒng)一的問題,在 UI 層做一個轉(zhuǎn)換即可。

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

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

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

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

(因為弧度不直觀,后面會用角度來描述,但數(shù)據(jù)層依舊還是用的弧度)

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

通常這些編輯器自己決定就好。像我的項目,向上表示 0 度,順時針方向為旋轉(zhuǎn)方向,方向取值為 [0, 360)。

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

圖片

縮放實現(xiàn)思路

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

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

交互邏輯:

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

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

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

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

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

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

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

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

更新 width 和 height

首先是更新矩形寬高。

因為有一個旋轉(zhuǎn),所以算法不會這么直觀。

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

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

看看代碼

class Graph {
  // ...

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

    // 計算反向旋轉(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') {
      // 參照點為左上角(x 和 y)
      // 新的寬高自然就是光標(biāo)位置減去 x、y
      width = posX - oldBox.x;
      height = poxY - oldBox.y;
    }
    // 其他控制點的邏輯暫且省略...
    
    // 2. 計算 x 和 y
    // ...
  }
}

看看只更新寬高的效果。

可以看到是有問題的,因為修改寬高后,矩形的中心點也發(fā)生了變化,導(dǎo)致縮放中心錯誤。所以我們要修正一下 x 和 y。

修正 x 和 y

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

重點就一句話:縮放前的參考點和縮放后的參考點的位置要保持一致。這個參考點其實就是圖形縮放過程中的縮放中心。

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

class Graph {
  // ...

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

    // 2. 計算 x 和 y

    // 設(shè)置參照點,不同縮放類型的參照點不同
    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;
    }
    // 其他縮放類型暫且省略

    // 縮放前的參考點位置
    const { x: prevRotatedOriginX, y: prevRotatedOriginY } = transformRotate(
      prevOriginX,
      prevOriginY,
      oldBox.rotation || 0,
      cx,
      cy
    );
    // 縮放后的參考點位置
    const { x: rotatedOriginX, y: rotatedOriginY } = transformRotate(
      originX,
      originY,
      oldBox.rotation || 0,
      oldBox.x + width / 2, // 旋轉(zhuǎn)中心是新的
      oldBox.y + height / 2
    );
    // 計算新舊兩個參考點的差值,對 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ù),這里要做一個標(biāo)準(zhǔn)化,然后賦值給圖形屬性即可。

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

其他縮放控制點

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

不同類型下 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;
}

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

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;
} else if (type === 'ne') { // 右上縮放點,參考點為左下角
  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;
}

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

鎖定縮放比

按住 shift 可以鎖定縮放比。

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

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

class Graph {
  // ...

  movePoint(type, newPos, oldBox, keepRatio = false) {
    // 1. 計算 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. 計算 x 和 y
    // ...
  }
}

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

優(yōu)化點

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

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

另外就是選中多個圖形,然后縮放的場景我沒實現(xiàn)。這種場景下,通常是要鎖定寬高比的。

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

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

按住 Alt 實現(xiàn)圖形中心縮放也沒做,這個比較簡單,有空再做。

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

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

2023-09-26 07:39:21

2024-01-03 08:43:17

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

2023-07-07 13:56:01

圖形編輯器畫布縮放

2023-08-31 11:32:57

圖形編輯器contain

2023-09-07 08:24:35

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

2023-02-01 09:21:59

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

2023-04-07 08:02:30

圖形編輯器對齊功能

2023-09-11 09:02:31

圖形編輯器模塊間的通信

2024-01-08 08:30:05

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

2023-04-10 08:45:44

圖形編輯器排列移動功能

2023-07-31 08:46:07

圖形編輯器圖形自動對齊

2023-10-10 16:04:30

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

2023-10-08 08:11:40

圖形編輯器快捷鍵操作

2023-08-28 08:10:50

Hex圖形編輯器

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

圖形編輯器撤銷重做功能
點贊
收藏

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

国产精品欧美激情在线播放| 亚洲国产成人一区二区三区| 51午夜精品国产| 三年中文高清在线观看第6集 | 天堂中文在线播放| 激情综合五月天| 欧美成人合集magnet| 天堂av在线播放| 久久亚洲精华国产精华液| 国产日韩精品电影| 中文字幕在线三区| 天天综合天天做天天综合| 人人妻人人澡人人爽欧美一区| 午夜先锋成人动漫在线| 日韩一本二本av| 色乱码一区二区三区在线| 日韩一区二区免费看| 欧美第一黄网免费网站| 午夜影院免费在线| 亚洲综合精品久久| 男女h黄动漫啪啪无遮挡软件| 成人无号精品一区二区三区| 一区二区三区四区精品| 九色在线视频| 一区二区三区四区视频精品免费| 杨幂一区欧美专区| 亚洲在线观看| 国产欧美久久一区二区| 少妇高潮一区二区三区99| 69p69国产精品| av播放在线| 日本黄色一区二区| 97在线影院| 粉嫩老牛aⅴ一区二区三区| av免费网站观看| 国产福利一区二区三区视频| 亚洲国产精品www| 日韩有码一区二区三区| 欧洲亚洲一区二区| 日本午夜精品视频在线观看| 欧美精品一区二区三区四区五区| 高清成人在线| 日韩成人一级大片| 国产精品三区四区| 一本色道久久综合亚洲精品不卡| 成人观看高清在线观看免费| 国产一区二区精品久| 欧美一级视频在线观看| 亚洲欧洲av| 鬼打鬼之黄金道士1992林正英| 成人精品天堂一区二区三区| 亚洲一区二区久久久久久| 黄色成人91| 国产大片精品免费永久看nba| 精品视频高潮| 国产高清不卡av| 99精品国产在热久久婷婷| 一区二区免费电影| 99久久免费精品高清特色大片| 老司机福利在线视频| 亚洲欧美日韩一区| 免费av一级电影| 91黄色激情网站| 亚洲www色| 伊人久久久久久久久久| 青青久久av| 日韩经典在线视频| 久久久久久久久一| 男人天堂资源在线| 久久精品国产欧美激情| 欧美精品亚洲二区| 日本在线观看大片免费视频| 91av在线播放视频| 91精品国产高清一区二区三区蜜臀| 日韩电影中文字幕在线| 成人久久网站| 国产美女高潮久久白浆| 久久亚洲精品伦理| 国产夫妻视频| 亚洲毛片在线看| 国内精品久久久久国产盗摄免费观看完整版| 18禁网站免费无遮挡无码中文| 欧美视频在线免费| 日韩中出av| 亚洲精品成人久久久998| 亚洲高清免费在线| 在线视频成人| 艳色歌舞团一区二区三区| 一区av在线播放| av不卡一区二区| 日韩免费视频播放| 国产视频欧美视频| 日韩极品在线观看| 国产黄网站在线观看| 91网在线免费观看| 欧美日韩一区二区免费在线观看 | 国产成人综合自拍| 91精品国产91久久久久游泳池| 亚洲老头同性xxxxx| 久久69精品久久久久久久电影好 | 日韩中文在线中文网三级| 亚洲免费综合| 欧美无乱码久久久免费午夜一区| 欧美激情欧美激情| 日本在线不卡一区| 国内在线免费视频| 欧美一二三四五区| 亚洲国产精品久久| 精品系列免费在线观看| 亚洲优女在线| 国产在线青青草| 国产精品男人的天堂| 欧美影视一区二区三区| 日韩在线一区二区| 国产ktv在线视频| 男人天堂网视频| 91精品国产综合久久男男| 午夜国产不卡在线观看视频| 亚洲啪啪91| 热久久久久久| 性色av一区| 久久国产精品网| 国产精品入口福利| 日韩三级高清在线| 国产亚洲一区二区三区| 亚洲视频日本| 99久热在线精品视频观看| 中文字幕欧美一区二区| 激情五月五月婷婷| 成人国产精品久久久| 亚洲欧美综合v| 欧美三级乱人伦电影| 国产精品人人做人人爽人人添| 亚洲黄色大片| 国产99亚洲| 伊人久久国产| 91高清在线| av在线www| 欧美 日韩 国产 高清| 欧美精品一区二区视频| 欧美极品美女电影一区| 欧美精品一区男女天堂| 一区二区三区日韩欧美精品| 国产成人在线免费| 免费看日本一区二区| 亚洲伊人精品酒店| 欧美寡妇性猛交xxx免费| 又黄又爽在线观看| 国产精品拍拍拍| 欧美变态另类刺激| 黄色网络在线观看| 日韩中文一区| 久久久久久九九| 国产欧美日韩一区| 国产精品久久久久久久久粉嫩av| 欧美日韩国产成人在线观看| 亚洲男人av电影| 亚洲免费精彩视频| 亚洲成人激情在线观看| 日韩欧美国产三级| 欧美日本一道本| 欧美日本在线观看| 91精品在线观看入口| 欧美日韩午夜精品| 欧美一级搡bbbb搡bbbb| 亚洲第一区第二区| 亚洲女人天堂av| 超碰97人人做人人爱少妇| 精品国内亚洲在观看18黄| 久久av在线看| 欧美激情综合色综合啪啪五月| 国产精品久久久久久久午夜 | 欧美狂野另类xxxxoooo| 一区二区三区精品视频在线| 亚洲激情六月丁香| 欧美在线观看视频一区二区三区| 亚洲欧美成人一区二区在线电影| 欧美日本一区| 噜噜爱69成人精品| 黄色成人av网| 日本精品久久久久久久| 日韩中文字幕a| 欧美一区二区麻豆红桃视频| 婷婷夜色潮精品综合在线| 亚洲自拍小视频| 在线观看免费黄视频| 免费人成在线不卡| 日韩最新免费不卡| 免费看成年人视频在线观看| 日韩午夜免费视频| 久久精品国产亚洲7777| 在线播放你懂得| 琪琪一区二区三区| 97超视频免费观看| 成人在线影视| 日本一区二区成人| 色综合久久久久久久久五月| 亚洲日本视频在线| 国产精品一区在线观看你懂的| 一区二区三区美女xx视频|