天天看點

圖形編輯器開發:最基礎但卻複雜的選擇工具

作者:前端西瓜哥

大家好,我是前端西瓜哥。

對于一個圖形設計軟體,它最基礎的工具是什麼?選擇工具。

但這個選擇工具,卻是相當的複雜。這次我來和各位,細說細說選擇工具的一些彎彎道道。

我正在開發的圖形設計工具:

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

線上體驗:

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

單選

最基本的,要做到單個圖形的選中。

光标停留在圖形上方,按下滑鼠左鍵,這個圖形就被選中了。這就是一個簡單的選中了單個圖形的場景。

注意必須是 mousedown,不是 click。後面會說為什麼。

在代碼層,我們會使用 “圖形拾取” 算法确定光标落在哪個圖形的點選區域上,注意考慮隐藏、鎖定、組的情況。

如果你對圖形拾取的細節感興趣,可以看我的這篇文章:

《如何在 Canvas 上實作圖形拾取?》

隐藏和鎖定的圖形會被忽略,如果點的是組下的一個元素,要将整個組的所有元素都選中。

清空 被選中圖形集合(暫且叫做 selectSet),然後把這個圖形添加進去。

selectSet.clear()
selectSet.add(targetEl)
           

選中集合儲存的是被選中的圖形,可以儲存 id,也可以是圖形對象。

在渲染層,會對被選中的圖形進行輪廓高亮,讓使用者有感覺。

此外還會有一個 矩形選中框,上面還會有控制點,讓使用者可以縮放和旋轉圖形。

選中框是圖形的包圍盒,通常是 帶旋轉的 OBB 包圍盒。

如果點選到空白區域,要将 selectSet 清空。

圖形編輯器開發:最基礎但卻複雜的選擇工具

多選

有時候我們希望選中出多個圖形。

通常的做法是,按住 Shift 鍵,然後點選一個圖形。注意是在滑鼠按下時就按住

同時也要 支援取消選中:原來被選中的一個圖形,我按住 Shift 再

代碼的核心邏輯是:

如果這個圖形不在 selectSet 中,将其加入;如果這個圖形在 selectSet,将其移除。

if (event.shiftKey) {
  if (selectSet.has(targetEl)) {
    selectSet.delete(targetEl)
  } else {
    selectSet.add(targetEl)
  }
}
           

多個圖形被選中了,除了給它們高亮輪廓線,我們還需要用一個更大的矩形選中框包裹所有被選中圖形。

一個小點:如果是取消選中的邏輯,需要滑鼠釋放後才更新 selectSet。因為要防止和後面會說的按住 Shift 水準垂直拖拽沖突。
圖形編輯器開發:最基礎但卻複雜的選擇工具

框選

框選,提供了 一次性選中大量特定區域内圖形 的能力。

在空白區域按下滑鼠拖拽,然後釋放,可以構造出一個矩形,這個矩形我們稱為 “選區”。

圖形編輯器開發:最基礎但卻複雜的選擇工具

選區矩形會和圖形進行碰撞檢測判斷,決定将哪些圖形是被框選中的。

碰撞檢測有三種方案:

  1. 選區矩形和選中圖形的包圍盒屬于 包含(contain)關系;
  2. 選區矩形和選中圖形的包圍盒屬于 相交(intersect)關系;
  3. 不使用包圍盒,精準判斷是否有真正的 像素上的相交;

個人比較推薦相交的判斷方案,figma 也選擇了該方案。

如果你對碰撞檢測的細節感興趣,可以看我之前寫的文章:

《圖形編輯器——矩形選區是如何實作選中多個圖形的?》

《幾何算法:矩形碰撞和包含檢測算法》

框選可以和多選結合。即你可以按住 Shift 鍵,然後去框選。

它的效果是和按住 Shift 一個個去選中圖形的效果是一樣的。

核心代碼實作:

if (!event.shiftKey) {
  selectSet.clear();
}
for (const el of elementsInScence) {
  // 判斷是否碰撞,這個方法
  if (isRectIntersect(selectionBox, el)) {
    // 普通框選
    if (!event.shiftKey) {
      selectSet.add(el);
    }
    // 連續和框選的組合
    else {
      if (selectSet.has(el)) {
        selectSet.delete(el);
      } else {
        selectSet.add(el);
      }
    }
  }
}
           

移動

選擇工具,主要是用來選擇,選中後一個很普遍的操作是:移動選中元素。

是以這也是它有時候也被叫做 移動工具 的原因。

移動的互動過程:

  1. 光标停留在已經被選中的圖形上,按下滑鼠不放;
  2. 然後拖拽滑鼠,被選中圖形跟随光标移動;
  3. 釋放滑鼠,表示移動到目标位置,移動結束。
圖形編輯器開發:最基礎但卻複雜的選擇工具

代碼核心實作:

  1. 移動前此時記錄圖形的位置,和起始位置;
  2. 拖拽時計算相對位移,更新圖形的位置;
  3. 釋放時重置狀态,以及記錄到曆史記錄中。
// 圖形移動前位置
let elStartCoords = [];
// 滑鼠按下事件的光标位置,計算偏移量時作為基準
let startCoord = { x: undefined, y: undefined };
const onStart = (e) => {
  // 記錄初始坐标
  elStartCoords = elements.map((el) => ({ x: el.x, y: el.y }));
  startCoord.x = e.clientX;
  startCoord.y = e.clientY;
};
const onDrag = (e) => {
  // 計算偏移量,更新坐标
  const dx = e.clientX - startCoord.x;
  const dy = e.clientY - startCoord.y;
  elements.forEach((el, i) => {
    el.x = elStartCoords[i].x + dx;
    el.y = elStartCoords[i].y + dy;
  });
};
const onEnd = () => {
  // 重置狀态
  elStartCoords = [];
  startCoord = { x: undefined, y: undefined };
};
           

按住 Shift 鍵的垂直水準移動

假設我們做好了幾個對齊的圖形,當我們移動其中一個圖形的時候,希望能夠保持原來的對齊。

這時候,限制移動為水準或垂直方向就很有用。

通常通過在拖拽時按住 Shift 來開啟這個能力。

圖形編輯器開發:最基礎但卻複雜的選擇工具

要點:

  1. 拖拽的中途從沒按住 Shift 到按住,要立即響應,代碼實作上要補一個鍵盤事件監聽,而不是靠滑鼠移動事件,因為你不移動滑鼠,被選中元素就不會更新。
  2. 比較 dx 和 dy 的大小。dx 大,水準移動;dy 大,垂直移動。這樣圖形就能盡量靠近十字線(水準線+垂直線)

對齊到像素網格

對齊到網格,開啟後,讓圖形在移動的時候,讓圖檔盡量貼到網格線上。

圖形編輯器開發:最基礎但卻複雜的選擇工具

做法是将一個或多個圖形的包圍盒(AABB)的左上角坐标,進行取餘,得到一個落在網格線上的位置,用這位置去更新選中圖形。

擴充能力:控制點

選中圖形,是為了對它們進行操作。

這些 操作的實作,要通過控制點來落地。

常見的有:

  • 縮放控制點,在圖形選中框的 4 個角上;
  • 旋轉控制點,拖拽它設定圖形的旋轉,旋轉控制點;
  • 給圖形設定漸變填充色,需要指定兩種顔色的顔色和位置,需要的 漸變色控制點;

下面是 figma 的縮放和旋轉示範,我開發的編輯器還沒實作完整。

圖形編輯器開發:最基礎但卻複雜的選擇工具

此外,不同圖形繪制工具可能會有它們獨有的操作方式,這些都需要你根據圖形的特性去設計。

看看 Figma 對不同圖形的特殊控制點邏輯。

圖形編輯器開發:最基礎但卻複雜的選擇工具

是以選擇工具子產品在設計上,要提供 注冊各種類型圖形控制點邏輯 的能力。

在 “圖形拾取” 時,要把控制點也考慮進來,光标是否點在控制點上。

如果點在控制點上,拖拽邏輯就要走控制點的邏輯,不再走選擇工具的基礎邏輯。

其他

還有一些可考慮實作的增強能力:

  • 輕按兩下,進入編輯模式,進行一些更複雜的操作,比如可以變成貝塞爾曲線操作任意點。
  • 移動時,用線條顯示和其他圖形的點(比如中點、選中框角落的 4 個點)的距離,并在很接近時吸附過去。

結尾

總結一下,選擇工具,是一款圖形設計軟體最基礎的功能。

它的作用是選中的圖形,對它們進行操作,目的是 更新指定圖形屬性。

最基礎的操作是移動,接着是通過控制點實作的增強操作。

控制點操作的兩個基本能力是旋轉和縮放。然後我們會根據不同類型的圖形,去實作不同的控制點邏輯。

說是工具的一種,但它其實的定位更多是底層的基礎建設。

我是前端西瓜哥,歡迎關注我,學習更多圖形開發知識。

繼續閱讀