大家好,我是前端西瓜哥。
(資料圖片)
對于一個圖形設(shè)計(jì)軟件,它最基礎(chǔ)的工具是什么?選擇工具。
但這個選擇工具,卻是相當(dāng)?shù)膹?fù)雜。這次我來和各位,細(xì)說細(xì)說選擇工具的一些彎彎道道。
單選我正在開發(fā)的圖形設(shè)計(jì)工具:
https://github.com/F-star/suika
線上體驗(yàn):
https://blog.fstars.wang/app/suika/
最基本的,要做到單個圖形的選中。
光標(biāo)停留在圖形上方,按下鼠標(biāo)左鍵,這個圖形就被選中了。這就是一個簡單的選中了單個圖形的場景。
注意必須是 mousedown,不是 click。后面會說為什么。
在代碼層,我們會使用 “圖形拾取” 算法確定光標(biāo)落在哪個圖形的點(diǎn)擊區(qū)域上,注意考慮隱藏、鎖定、組的情況。
隱藏和鎖定的圖形會被忽略,如果點(diǎn)的是組下的一個元素,要將整個組的所有元素都選中。
清空被選中圖形集合(暫且叫做 selectSet),然后把這個圖形添加進(jìn)去。
selectSet.clear()selectSet.add(targetEl)
選中集合保存的是被選中的圖形,可以保存 id,也可以是圖形對象。
在渲染層,會對被選中的圖形進(jìn)行輪廓高亮,讓用戶有感知。
此外還會有一個矩形選中框,上面還會有控制點(diǎn),讓用戶可以縮放和旋轉(zhuǎn)圖形。
選中框是圖形的包圍盒,通常是帶旋轉(zhuǎn)的 OBB 包圍盒。
如果點(diǎn)擊到空白區(qū)域,要將 selectSet 清空。
多選有時候我們希望選中出多個圖形。
通常的做法是,按住 Shift 鍵,然后點(diǎn)擊一個圖形。注意是在鼠標(biāo)按下時就按住
同時也要支持取消選中:原來被選中的一個圖形,我按住 Shift 再
代碼的核心邏輯是:
如果這個圖形不在 selectSet 中,將其加入;如果這個圖形在 selectSet,將其移除。
if (event.shiftKey) { if (selectSet.has(targetEl)) { selectSet.delete(targetEl) } else { selectSet.add(targetEl) }}
多個圖形被選中了,除了給它們高亮輪廓線,我們還需要用一個更大的矩形選中框包裹所有被選中圖形。
框選一個小點(diǎn):如果是取消選中的邏輯,需要鼠標(biāo)釋放后才更新 selectSet。因?yàn)橐乐购秃竺鏁f的按住 Shift 水平垂直拖拽沖突。
框選,提供了一次性選中大量特定區(qū)域內(nèi)圖形的能力。
在空白區(qū)域按下鼠標(biāo)拖拽,然后釋放,可以構(gòu)造出一個矩形,這個矩形我們稱為 “選區(qū)”。
選區(qū)矩形會和圖形進(jìn)行碰撞檢測判斷,決定將哪些圖形是被框選中的。
碰撞檢測有三種方案:
選區(qū)矩形和選中圖形的包圍盒屬于包含(contain)關(guān)系;選區(qū)矩形和選中圖形的包圍盒屬于相交(intersect)關(guān)系;不使用包圍盒,精準(zhǔn)判斷是否有真正的像素上的相交;個人比較推薦相交的判斷方案,figma 也選擇了該方案。
框選可以和多選結(jié)合。即你可以按住 Shift 鍵,然后去框選。
它的效果是和按住 Shift 一個個去選中圖形的效果是一樣的。
核心代碼實(shí)現(xiàn):
if (!event.shiftKey) { selectSet.clear();}for (const el of elementsInScence) { // 判斷是否碰撞,這個方法 if (isRectIntersect(selectionBox, el)) { // 普通框選 if (!event.shiftKey) { selectSet.add(el); } // 連續(xù)和框選的組合 else { if (selectSet.has(el)) { selectSet.delete(el); } else { selectSet.add(el); } } }}
移動選擇工具,主要是用來選擇,選中后一個很普遍的操作是:移動選中元素。
所以這也是它有時候也被叫做移動工具的原因。
移動的交互過程:
光標(biāo)停留在已經(jīng)被選中的圖形上,按下鼠標(biāo)不放。然后拖拽鼠標(biāo),被選中圖形跟隨光標(biāo)移動。釋放鼠標(biāo),表示移動到目標(biāo)位置,移動結(jié)束。代碼核心實(shí)現(xiàn):
移動前此時記錄圖形的位置,和起始位置。拖拽時計(jì)算相對位移,更新圖形的位置。釋放時重置狀態(tài),以及記錄到歷史記錄中。// 圖形移動前位置let elStartCoords = [];// 鼠標(biāo)按下事件的光標(biāo)位置,計(jì)算偏移量時作為基準(zhǔn)let startCoord = { x: undefined, y: undefined };const onStart = (e) => { // 記錄初始坐標(biāo) elStartCoords = elements.map((el) => ({ x: el.x, y: el.y })); startCoord.x = e.clientX; startCoord.y = e.clientY;};const onDrag = (e) => { // 計(jì)算偏移量,更新坐標(biāo) 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 = () => { // 重置狀態(tài) elStartCoords = []; startCoord = { x: undefined, y: undefined };};
按住 Shift 鍵的垂直水平移動假設(shè)我們做好了幾個對齊的圖形,當(dāng)我們移動其中一個圖形的時候,希望能夠保持原來的對齊。
這時候,限制移動為水平或垂直方向就很有用。
通常通過在拖拽時按住 Shift來開啟這個能力。
要點(diǎn):
拖拽的中途從沒按住 Shift 到按住,要立即響應(yīng),代碼實(shí)現(xiàn)上要補(bǔ)一個鍵盤事件監(jiān)聽,而不是靠鼠標(biāo)移動事件,因?yàn)槟悴灰苿邮髽?biāo),被選中元素就不會更新。比較 dx 和 dy 的大小。dx 大,水平移動;dy 大,垂直移動。這樣圖形就能盡量靠近十字線(水平線+垂直線)對齊到像素網(wǎng)格對齊到網(wǎng)格,開啟后,讓圖形在移動的時候,讓圖片盡量貼到網(wǎng)格線上。
做法是將一個或多個圖形的包圍盒(AABB)的左上角坐標(biāo),進(jìn)行取余,得到一個落在網(wǎng)格線上的位置,用這位置去更新選中圖形。
擴(kuò)展能力:控制點(diǎn)選中圖形,是為了對它們進(jìn)行操作。
這些操作的實(shí)現(xiàn),要通過控制點(diǎn)來落地。
常見的有:
縮放控制點(diǎn),在圖形選中框的 4 個角上。旋轉(zhuǎn)控制點(diǎn),拖拽它設(shè)置圖形的旋轉(zhuǎn),旋轉(zhuǎn)控制點(diǎn)。給圖形設(shè)置漸變填充色,需要指定兩種顏色的顏色和位置,需要的漸變色控制點(diǎn)。下面是 figma 的縮放和旋轉(zhuǎn)演示,我開發(fā)的編輯器還沒實(shí)現(xiàn)完整。
此外,不同圖形繪制工具可能會有它們獨(dú)有的操作方式,這些都需要你根據(jù)圖形的特性去設(shè)計(jì)。
看看 Figma 對不同圖形的特殊控制點(diǎn)邏輯。
所以選擇工具模塊在設(shè)計(jì)上,要提供注冊各種類型圖形控制點(diǎn)邏輯的能力。
在 “圖形拾取” 時,要把控制點(diǎn)也考慮進(jìn)來,光標(biāo)是否點(diǎn)在控制點(diǎn)上。
如果點(diǎn)在控制點(diǎn)上,拖拽邏輯就要走控制點(diǎn)的邏輯,不再走選擇工具的基礎(chǔ)邏輯。
其他還有一些可考慮實(shí)現(xiàn)的增強(qiáng)能力:
雙擊,進(jìn)入編輯模式,進(jìn)行一些更復(fù)雜的操作,比如可以變成貝塞爾曲線操作任意點(diǎn)。移動時,用線條顯示和其他圖形的點(diǎn)(比如中點(diǎn)、選中框角落的 4 個點(diǎn))的距離,并在很接近時吸附過去。結(jié)尾總結(jié)一下,選擇工具,是一款圖形設(shè)計(jì)軟件最基礎(chǔ)的功能。
它的作用是選中的圖形,對它們進(jìn)行操作,目的是更新指定圖形屬性。
最基礎(chǔ)的操作是移動,接著是通過控制點(diǎn)實(shí)現(xiàn)的增強(qiáng)操作。
控制點(diǎn)操作的兩個基本能力是旋轉(zhuǎn)和縮放。然后我們會根據(jù)不同類型的圖形,去實(shí)現(xiàn)不同的控制點(diǎn)邏輯。
說是工具的一種,但它其實(shí)的定位更多是底層的基礎(chǔ)建設(shè)。
關(guān)鍵詞: