畫布就是一切(二) — 實現元素拖拉拽


在《畫布就是一切(一) — 基礎入門》中,我們介紹了利用畫布進行UI編程的基本模式,分析了如何實現鼠標懸浮在元素上,元素變色的功能。在本文中,我們依然利用畫布編程的基本模式進行編程,但這一次我們將會提升一定的難度,實現元素拖拉拽的效果。

使用過流程圖或是圖形繪制軟件的同學都見到過這樣的場景對於矩形拖拉拽的場景:

010-rect-drag

本文將以上述的場景為需求,結合畫布編程的基本模式來復現一個類似的效果。本文的代碼已經提交至GitHub倉庫,在倉庫根目錄/02_drag目錄中。

canvas-is-everything/02_drag at main · w4ngzhen/canvas-is-everything (github.com)

狀態

我們首先分析這個場景下的狀態有哪些。鼠標在矩形元素上按下后,鼠標可以拖動矩形元素,鼠標松開后,矩形不再跟隨鼠標移動。那么對於UI來說,最基本的就是矩形的位置和大小,同時我們還需要一個狀態來表示矩形元素是否被選中:

  • 矩形位置position
  • 矩形大小size
  • 矩形是否被選中selected

輸入與更新

在這個場景中,更新點主要在於當鼠標點擊在元素上時,矩形selected會修改為true;當鼠標移動的時候,只要有元素被選中且鼠標的左鍵處於點擊的狀態,那么就會修改矩形元素的position。而造成更新的原因就是鼠標的行為輸入(點擊以及移動)。

渲染

實際上,在該場景下,渲染是最簡單的部分,根據上一篇文章的介紹,我們只需要canvas的context不斷的畫矩形即可。

流程梳理

讓我們再次對流程進行梳理。初始情況下,鼠標在畫布上移動進而產生移動事件。我們引入一個輔助變量lastMousePosition(默認值為null),來表示上一次鼠標移動事件的所在位置。在鼠標移動事件觸發中,我們得到此刻鼠標的位置,並與上一次鼠標位置做向量差,進而得到位移差offset。對於offset我們將其應用在矩形的移動上。此外,當鼠標按下的時候,我們判斷是否選中矩形,進而將矩形的selected置為true或false。當鼠標抬起的時候,我們直接設置矩形selected為false即可。

基礎拖拽代碼編寫與分析

1)工具方法

定義常用的工具方法:

  • 獲取鼠標在canvas上的位置。

  • 檢查某個點是否位於某個矩形中。

// 1 定義常用工具方法
const utils = {

  /**
   * 工具方法:獲取鼠標在畫布上的position
   */
  getMousePositionInCanvas: (event, canvasEle) => {
    // 移動事件對象,從中解構clientX和clientY
    let {clientX, clientY} = event;
    // 解構canvas的boundingClientRect中的left和top
    let {left, top} = canvasEle.getBoundingClientRect();
    // 計算得到鼠標在canvas上的坐標
    return {
      x: clientX - left,
      y: clientY - top
    };
  },

  /**
   * 工具方法:檢查點point是否在矩形內
   */
  isPointInRect: (rect, point) => {
    let {x: rectX, y: rectY, width, height} = rect;
    let {x: pX, y: pY} = point;
    return (rectX <= pX && pX <= rectX + width) && (rectY <= pY && pY <= rectY + height);
  },

};

2)狀態定義

// 2 定義狀態
let rect = {
  x: 10,
  y: 10,
  width: 80,
  height: 60,
  selected: false
};

根據前文,在矩形一般的屬性上位置和大小上,我們還新增了屬性selected,用於表示矩形是否被選中。

3)獲取Canvas元素對象

// 3 獲取canvas元素,准備在步驟
let canvasEle = document.querySelector('#myCanvas');

調用API,獲取Canvas元素對象,用於后續的事件監聽。

4)鼠標按下事件

// 4 鼠標按下事件
canvasEle.addEventListener('mousedown', event => {
  // 獲取鼠標按下時位置
  let {x, y} = utils.getMousePositionInCanvas(event, canvasEle);
  // 矩形是否被選中取決於點擊時候的鼠標是否在矩形內部
  rect.selected = utils.isPointInRect(rect, {x, y});
});

獲取當前鼠標按下的位置,並通過工具函數來判斷是否需要將矩形選中(selected置為true/false)。

5)鼠標移動處理

// 5 鼠標移動處理
// 5.1 定義輔助變量,記錄每一次移動的位置
let mousePosition = null;
canvasEle.addEventListener('mousemove', event => {

  // 5.2 記錄上一次的鼠標位置
  let lastMousePosition = mousePosition;

  // 5.3 更新當前鼠標位置
  mousePosition = utils.getMousePositionInCanvas(event, canvasEle);

  // 5.4 判斷是否鼠標左鍵點擊且有矩形被選中
  // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
  let buttons = event.buttons;
  if (!(buttons === 1 && rect.selected)) {
    // 不滿足則不處理
    return;
  }

  // 5.5 獲取鼠標偏移
  let offset;
  if (lastMousePosition === null) {
    // 首次記錄,偏移dx和dy為0
    offset = {
      dx: 0,
      dy: 0
    };
  } else {
    // 曾經已經記錄了位置,則偏移則為當前位置和上一次位置做向量差
    offset = {
      dx: mousePosition.x - lastMousePosition.x,
      dy: mousePosition.y - lastMousePosition.y
    };
  }

  // 5.6 改動rect位置
  rect.x = rect.x + offset.dx;
  rect.y = rect.y + offset.dy;

});

這一部分的代碼略長。但是邏輯並不難理解。

5.1 定義輔助變量mousePosition使用該變量記錄鼠標在每一次移動過程中的位置。

5.2 記錄臨時變量lastMousePosition將上一次事件記錄的mousePosition賦給該變量,用於后續進行偏移offset計算。

5.3 更新mousePosition

5.4 判斷是否鼠標左鍵點擊且有矩形被選中。在鼠標移動的過程中,我們是可以通過事件對象中的buttonbuttons屬性的數值來判斷當前鼠標的點擊情況(MDN)。當buttonsbutton為1的時候,表示移動的過程中鼠標左鍵是按下的狀態。通過判斷鼠標左鍵是否被按下來表示是否處於拖拽中,但是拖拽並不意味就選中了矩形在拖拽,還需要確定當前的矩形是否選中,所以需要(buttons === 1rect.selected === true)兩個條件共同決定。

5.5 獲取鼠標偏移。這一部分需要解釋一下什么是鼠標偏移(offset)。在鼠標移動的每時每刻都會有一個位置,我們利用mousePosition記錄了該位置。然后利用lastMousePositionmousePosition,我們將此刻的位置和上一次位置的x和y對應進行差(向量差),進而得到鼠標一小段的偏移量。但需要注意的是,如果是首次的移動事件,那么上一次的位置是lastMousePosition是null,那么我們認為這個偏移0。

020-mouse-offset-desc

5.6 改動矩形位置。將鼠標偏移值應用到矩形的位置上,讓矩形也位移對應的距離。

在鼠標移動的處理中,我們完成了由鼠標移動offset作為輸入,修改了被點中的矩形的位置。

6)鼠標按鍵抬起事件

// 6 鼠標抬起事件
canvasEle.addEventListener('mouseup', () => {
  // 鼠標抬起時,矩形就未被選中了
  rect.selected = false;
});

鼠標按鍵的抬起后,我們認為不再需要對矩形進行推拽,所以將矩形的selected置為false。

7)渲染處理

// 7 渲染
// 7.1 從Canvas元素上獲取context
let ctx = canvasEle.getContext('2d');
(function doRender() {
  requestAnimationFrame(() => {

    // 7.2 處理渲染
    (function render() {
      // 先清空畫布
      ctx.clearRect(0, 0, canvasEle.width, canvasEle.height);
      // 暫存當前ctx的狀態
      ctx.save();
      // 設置畫筆顏色:黑色
      ctx.strokeStyle = rect.selected ? '#F00' : '#000';
      // 矩形所在位置畫一個黑色框的矩形
      ctx.strokeRect(rect.x - 0.5, rect.y - 0.5, rect.width, rect.height);
      // 恢復ctx的狀態
      ctx.restore();
    })();

    // 7.3 遞歸調用
    doRender();

  });
})();

渲染部分的代碼,總的來說就是三個要點:

  1. 獲取Canvas元素的context對象。
  2. 使用requestAnimationFrameAPI並構造遞歸結構來讓瀏覽器調度渲染流程。
  3. 在渲染流程編寫畫布操作的代碼(清空、繪制)。

拖拽效果演示

至此,我們已經實現了元素拖動的樣例,效果如下:

030-drag-show-case

對於當前效果的完整代碼在項目根目錄/02_drag目錄中,對應git提交為:02_drag: 01_基礎效果

效果提升

對於上述效果,其實還是不完美的。因為當鼠標懸浮在矩形上的時候,並沒有任何UI上的信息,點擊的矩形進行拖拽的時候,鼠標指針也是普通的。於是我們優化代碼,將鼠標懸浮的呈現的效果以及拖拽時候的鼠標指針效果做出來。

我們設定,當鼠標懸浮在矩形上的時候,矩形會改變對應的顏色為帶有50%透明的紅色(rgba(255, 0, 0, 0.5),並且鼠標的指針修改為pointer。那么首先需要給矩形加上我們在第一章中提到的屬性hover

let rect = {
  x: 10,
  y: 10,
  width: 80,
  height: 60,
  selected: false,
  // hover效果
  hover: false,
};

在渲染中,我們不再像上一節中進行簡單的處理,而是需要對selected、hover以及一般狀態都進行考慮。

    // 7.2 處理渲染
    (function render() {
        
	  // ...

      // 被點擊選中:正紅色,指針為 'move'
      // 懸浮:帶50%透明的正紅色,指針為 'pointer'
      // 普通下為黑色,指針為 'default'
      if (rect.selected) {
        ctx.strokeStyle = '#FF0000';
        canvasEle.style.cursor = 'move';
      } else if (rect.hover) {
        ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)';
        canvasEle.style.cursor = 'pointer';
      } else {
        ctx.strokeStyle = '#000';
        canvasEle.style.cursor = 'default';
      }

	  // ...
        
    })();

接下來就是在鼠標移動事件中,修改hover:

canvasEle.addEventListener('mousemove', event => {

  // 5.2 記錄上一次的鼠標位置
  // ... ...

  // 5.3 更新當前鼠標位置
  mousePosition = utils.getMousePositionInCanvas(event, canvasEle);

  // 5.3.1 判斷鼠標是否懸浮在矩形
  rect.hover = utils.isPointInRect(rect, mousePosition);

  // 5.4 判斷是否鼠標左鍵點擊且有矩形被選中
  // ... ...

});

整體演示

至此,我們豐富了我們的拖拽樣例,結果如下:

040-drag-show-case-perfect

代碼倉庫與說明

本文所在的代碼倉庫地址為:

canvas-is-everything/02_drag at main · w4ngzhen/canvas-is-everything (github.com)

兩次提交:

  1. 02_drag: 01_基礎效果(優化前)
  2. 02_drag: 02_懸浮與點擊效果提升(優化后)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM