編輯器 github 地址:
https://github.com/F-star/suika
線上體驗:
https://blog.fstars.wang/app/suika/
圖形有幾個重要的基礎屬性,會經常被用到,我們在實現縮放圖形前需要理清一下它們。
x 和 y 為圖形的左上角位置,注意是旋轉前的。
x、y 旋轉后我們叫做 rotatedX、rotatedY,屬性面板中會用到。
width 和 height 為圖形的寬高,這個沒什么好說的。
另外,有些圖形有些特殊,它的 x、y、width、height 是要通過其他屬性計算出來的,比如貝塞爾曲線。
rotation 為圖形的旋轉度數,通常使用 弧度單位。
因為弧度是數學計算中的常客,各種 API 都是要求提供弧度的,比如內置的 Math.sin() 方法。
你存角度自然也是可以,但不推薦,但計算時多了一層多余的單位轉換,且丟失一些微小的精度。
當然 UI 層還是要展示角度,因為是面向用戶的,對于數據和 UI 不統一的問題,在 UI 層做一個轉換即可。
旋轉度數通常要配合一個變換中心(origin),這個可以作為一個屬性讓用戶設置。
但我更建議將 x、y、width、height 形成的 矩形的中點 作為旋轉中心,這樣更簡單一些,減少用戶的心智負擔,也防止出現用戶設置一些奇怪 origin 的場景。
下圖中,紅色矩形是藍色矩陣順時針旋轉 45 度得到。
旋轉度數還要考慮 旋轉方向、基準角度、取值范圍 問題。
(因為弧度不直觀,后面會用角度來描述,但數據層依舊還是用的弧度)
通常這些編輯器自己決定就好。像我的項目,向上表示 0 度,順時針方向為旋轉方向,方向取值為 [0, 360)。
一些編輯器是支持用戶自己設置的,比如 AutoCAD 可通過圖形單位命令,設置旋轉方向和基準角度。
進入正題,對圖形進行縮放。
接下來會以通過右下角(也叫東南 se 方向) 縮放控制點縮放為例進行講解。
交互邏輯:
選擇工具下,當光標落在右下角的縮放控制點上時,光標會變成縮放樣式(這個不是本文核心,不講)。
此時按下鼠標,然后進行拖拽,即可對圖形以左上角為縮放中心,進行縮放。
實現思路:更新 width 和 height,然后確定參照點,修正 x 和 y。
按下鼠標時,我們要把當前圖形的 x、y、width、height、rotation 記錄下來。之后的縮放是基于這個初始狀態進行的。
const mousedown = (e) => { // ... // 縮放前圖形的屬性,之后我們會直接更新圖形屬性,導致原來的屬性丟失,所以要記錄下這個快照。 prevElement = { x: item.x, y: item.y, width: item.width, height: item.height, rotation: item.rotation ?? 0, }}
拖拽時,調用我們將要實現的 movePoint 方法,去更新這個圖形。
const drag = (e) = { // ... selectElement.movePoint( 'se', // 縮放控制點類型:右下(或東南) lastPoint, // 當前光標位置(基于場景坐標系) prevElement, // 縮放前的屬性快照 );}
下面就是核心方法 movePoint 的實現邏輯了。
首先是更新矩形寬高。
因為有一個旋轉,所以算法不會這么直觀。
我們要意識到這里有一個變換。看到的圖形,是做過變換(基于矩形中心旋轉)之后的,但我們需要修改的 width、height、x、y 則是旋轉前的。
所以我們需要把光標位置給旋轉回來,然后再減去 x 和 y 去得到真正的 width 和 height。
看看代碼
class Graph { // ... // 根據縮放點更新圖形 movePoint(type, newPos, oldBox) { // 1. 計算 width 和 height // 計算縮放中心(也就是矩形的中點) const cx = oldBox.x + oldBox.width / 2; const cy = oldBox.y + oldBox.height / 2; // 計算反向旋轉的光標位置 const { x: posX, y: poxY } = transformRotate( newPos.x, newPos.y, -(oldBox.rotation || 0), // 注意這里是負數 cx, cy ); let width = 0; let height = 0; if (type === 'se') { // 參照點為左上角(x 和 y) // 新的寬高自然就是光標位置減去 x、y width = posX - oldBox.x; height = poxY - oldBox.y; } // 其他控制點的邏輯暫且省略... // 2. 計算 x 和 y // ... }}
看看只更新寬高的效果。
可以看到是有問題的,因為修改寬高后,矩形的中心點也發生了變化,導致縮放中心錯誤。所以我們要修正一下 x 和 y。
接著我們就要修正 x 和 y 的值。
重點就一句話:縮放前的參考點和縮放后的參考點的位置要保持一致。這個參考點其實就是圖形縮放過程中的縮放中心。
對于右下角縮放控制點,它的縮放中心就是左上角,即 x 和 y 經過旋轉的位置。
class Graph { // ... movePoint(type, newPos, oldBox) { // 1. 計算 width 和 height // ... // 2. 計算 x 和 y // 設置參照點,不同縮放類型的參照點不同 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, // 旋轉中心是新的 oldBox.y + height / 2 ); // 計算新舊兩個參考點的差值,對 x、y 進行補正 const dx = rotatedOriginX - prevRotatedOriginX; const dy = rotatedOriginY - prevRotatedOriginY; const x = oldBox.x - dx; const y = oldBox.y - dy; }}
width 和 height 可能為負數,這里要做一個標準化,然后賦值給圖形屬性即可。
this.setAttrs( normalizeRect({ x, y, width, height, }),);
對于其他類型縮放控制點,比如左上、右上、左下縮放控制點,它們的大框架是一樣的,只是 width 和 height 計算方式不同,以及參考點不同。
不同類型下 width 和 height 的設置:
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;}
新舊參考點設置:
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;}
暫時沒實現正北、正南、正西、正東的邏輯,邏輯大差不差。
按住 shift 可以鎖定縮放比。
做法是對比新舊圖形寬高比,將 width 和 height 其中一個進行修正即可。注意正負號。
方法需要多傳一個 keepRatio 的參數:
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 // ... }}
貌似沒考慮除數 height 為 0 的情況..
本文的實現是考慮的是比較簡單的縮放圖形場景,一些更復雜的場景并未實現。
縮放還有另一種策略,就是會產生 反向顛倒 的縮放。要實現這個效果,需要引入縮放屬性,復雜度會提升很多。
另外就是選中多個圖形,然后縮放的場景我沒實現。這種場景下,通常是要鎖定寬高比的。
否則就會出現圖形的斜切效果,這個如果要實現,我們還要引入斜切屬性,復雜度再一次提升。
下面是 Figma 的效果,真是讓人頭扁。
按住 Alt 實現圖形中心縮放也沒做,這個比較簡單,有空再做。
讀者如果看懂我這篇文章,心里應該有思路的:width、height 的計算要加入圖形中點參數,參照點設置為圖形中點。
本文鏈接:http://www.tebozhan.com/showinfo-26-14348-0.html圖形編輯器開發:實現縮放圖形
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com