(開源)給圖片編輯器添加了輔助線

前言

上篇我們介紹了做的圖片編輯器,大部分工具類的軟件都有輔助線,方便拖拽元素的時候對齊,能讓我們快速的做出漂亮的圖片。 這兩天給編輯器加上了輔助線, 輔助線實現過程稍微有些複雜,我們一步步說下實現過程。

演示

演示地址

實現流程

原理講解

  • 左側輔助線出現
    image.png
    我們以節點2爲移動的元素,通過上面的圖觀察我們可以看出,當左側輔助線出現的時候,節點1x座標和節點2x座標相等的時候輔助線就會出現,我們移動節點2的時候動態去判斷。

  • 右側輔助線出現
    image.png

我們以節點2爲移動的元素,通過上面的圖觀察我們可以看出,當右側輔助線出現的時候,節點1x+width(座標x+節點的寬度)和節點2x座標相等的時候輔助線就會出現,我們移動節點2的時候動態去判斷。

輔助線規則

  • 左側輔助線 x1(x) = x2(x)
  • 右側輔助線 x1(x+width) = x2(x)
  • 水平中間輔助線 x1(x+width/2) = x2(x+ width / 2)
  • 頂部輔助線 x1(y) = x2(y)
  • 底部輔助線 x1(y+height) = x(y)
  • 垂直中間輔助線 x1(y+height/2) = x2(+height/2)
    上面的公式我們已節點2拖動的元素節點1目標元素。當我們以節點1拖動元素節點2目標元素,公式會有變化,大家可以自行嘗試一下。留在評論區

代碼實現

上面我們分析出了一個節點的對比規則,畫布上可能會有很多節點,讓當前移動的節點去和剩下的元素去做比較。

找出畫布上的所有元素,記住位置

通過Knova的layer下的children獲取所有元素,並記錄位置,代碼如下

// 獲取單個節點的位置信息
export const getLocationItem = (shapeObject: Konva.Shape) => {
  const id = shapeObject.id();
  const width = shapeObject.width();
  const height = shapeObject.height();
  const x = shapeObject.x();
  const y = shapeObject.y();

  const locationItem: LocationItem = {
    id,
    w: width,
    h: height,
    x, // x座標
    y, // y座標
    l: x, // 左側方向                      
    r: x + width, // 右側方向
    t: y,  // 頂部方向
    b: y + height, // 底部方向
    lc: x + (width / 2), // 水平居中
    tc: y + (height / 2) // 垂直居中
  }
  return locationItem;
  // console.log('locationItem=>', locationItem);
}

// 設置所有節點的信息
export const setLocationItems = (layer: Konva.Layer) => {
  locationItems = [];
  layer.children?.forEach(item => {
    if (item.className !== 'Transformer') {
      locationItems.push(getLocationItem(item));
    }
  });
}

節點拖動,根據計算規則畫線

在拖動節點的時候,調用detectionToLine該方法。

/**
 * 拖動節點,shape代表當前拖動的節點
 */
export const detectionToLine = (layer: Konva.Layer, shape: Konva.Shape) => {
  const locationItem = getLocationItem(shape); // 當前節點的位置信息
  // 過濾當前節點,和剩下的節點做比較
  const compareLocations = locationItems.filter((item: LocationItem) => item.id !== locationItem.id);
  removeLines(layer); // 移除之前劃過的線
  compareLocations.forEach((item: LocationItem) => {
    if ((Math.abs(locationItem.x - item.x) <= threshold)) { // 處理左側方向
      shape.setPosition({ x: item.x, y: locationItem.y })
      addLine(layer, locationItem, item, DIRECTION.left)
    }
    if ((Math.abs(locationItem.x - item.r) <= threshold)) { // 處理右側
      shape.setPosition({ x: item.r, y: locationItem.y })
      addLine(layer, locationItem, item, DIRECTION.right);
    }

    if ((Math.abs(locationItem.lc - item.lc) <= threshold)) { // 處理水平居中
      shape.setPosition({ x: item.lc - (locationItem.w / 2), y: locationItem.y })
      addLine(layer, locationItem, item, DIRECTION.leftCenter);
    }

    // 拖動節點和目標節點互換的判斷條件
    if ((Math.abs(locationItem.r - item.x) <= threshold)) {
      shape.setPosition({ x: item.l - locationItem.w, y: locationItem.t })
      addLine(layer,item,locationItem, DIRECTION.right)
    }
    if ((Math.abs(locationItem.r - item.r) <= threshold)) { // 右側相等
      shape.setPosition({ x: item.r - locationItem.w, y: locationItem.t })
      addLine(layer,item,locationItem, DIRECTION.right)
    }


    if ((Math.abs(locationItem.y - item.y) <= threshold)) { // 處理垂直方向頂部
      shape.setPosition({ x: locationItem.x, y: item.y })
      addLine(layer, locationItem, item, DIRECTION.top);
    }

    if ((Math.abs(locationItem.y - item.b) <= threshold)) { // 處理底部
      shape.setPosition({ x: locationItem.x, y: item.b })
      addLine(layer, locationItem, item, DIRECTION.bottom);
    }

    if ((Math.abs(locationItem.tc - item.tc) <= threshold)) { // 處理垂直頂部居中
      shape.setPosition({ x: locationItem.x, y: item.tc - (locationItem.h /2 ) })
      addLine(layer, locationItem, item, DIRECTION.topCenter);
    }

     // 拖動節點和目標節點互換的判斷條件
    if ((Math.abs(locationItem.b - item.t) <= threshold)) { // 處理垂底部方向
      shape.setPosition({ x: locationItem.l, y: item.t - locationItem.h })
      addLine(layer,item,locationItem, DIRECTION.bottom)
    }

    if ((Math.abs(locationItem.b - item.b) <= threshold)) { // 右側相等
      shape.setPosition({ x: locationItem.l, y: item.b - locationItem.h })
      addLine(layer,item,locationItem, DIRECTION.bottom)
    }
  });
}

達到閾值,添加輔助線

我們可以看到在對比的時候是這樣的代碼

Math.abs(locationItem.b - item.b) <= threshold)

這塊主要是用來判斷兩個節點之間的距離小於設定的閾值,觸發添加輔助線。

還有一段設置當前節點位置的代碼,如下

 shape.setPosition({ x: locationItem.l, y: item.t - locationItem.h })

這塊的主要作用是輔助線出現的是,節點移動的位置不超過閾值,節點不會動。

添加輔助線

添加輔助線會傳入拖動的元素和目標元素,以及哪個方向要出現輔助線

 addLine(layer, locationItem, item, DIRECTION.left)

根據拖動的元素和目標元素以及方向計算出輔助線出現的位置

/**
 *
 * @param sourceItem 拖動的圖形
 * @param targetItem 目標圖形
 * @param targetItem 方向
 */
const getPoints = (sourceItem: LocationItem, targetItem: LocationItem, direction: DIRECTION) => {

  let minItem: LocationItem, maxItem: LocationItem;
  let points: any = [];

  let po = {
    [DIRECTION.left]: [
      [targetItem.l, sourceItem.b, targetItem.l, targetItem.t],
      [targetItem.l, targetItem.b, targetItem.l, sourceItem.t]
    ],
    [DIRECTION.right]: [
      [targetItem.r, sourceItem.b, targetItem.r, targetItem.t],
      [targetItem.r, targetItem.b, targetItem.r, sourceItem.t]
    ],
    [DIRECTION.leftCenter]: [
      [targetItem.lc, sourceItem.b, targetItem.lc, targetItem.t],
      [targetItem.lc, targetItem.b, targetItem.lc, sourceItem.t]
    ],
    [DIRECTION.top]: [
      [sourceItem.r, targetItem.t, targetItem.l, targetItem.t],
      [targetItem.r, targetItem.t, sourceItem.l, targetItem.t]
    ],
    [DIRECTION.bottom]: [
      [sourceItem.r, targetItem.b, targetItem.l, targetItem.b],
      [targetItem.r, targetItem.b, sourceItem.l, targetItem.b]
    ],
    [DIRECTION.topCenter]: [
      [sourceItem.r, targetItem.tc, targetItem.l, targetItem.tc],
      [targetItem.r, targetItem.tc, sourceItem.l, targetItem.tc]
    ]
  }

  switch (direction) {
    case DIRECTION.left:
      return sourceItem.y < targetItem.y ? po[DIRECTION.left][0] : po[DIRECTION.left][1];

    case DIRECTION.right:
      // 目標圖形是否在上邊
      return sourceItem.y < targetItem.y ? po[DIRECTION.right][0] : po[DIRECTION.right][1];

    case DIRECTION.leftCenter:
      return sourceItem.y < targetItem.y ? po[DIRECTION.leftCenter][0] : po[DIRECTION.leftCenter][1];

    case DIRECTION.top:
      return sourceItem.x < targetItem.x ? po[DIRECTION.top][0] : po[DIRECTION.top][1];

    case DIRECTION.bottom:
      return sourceItem.x < targetItem.x ? po[DIRECTION.bottom][0] : po[DIRECTION.bottom][1];

    case DIRECTION.topCenter:
      return sourceItem.x < targetItem.x ? po[DIRECTION.topCenter][0] : po[DIRECTION.topCenter][1];
    default:
      break;
  }
  return points;
}

添加輔助線方法,比較簡單

export const addLine = (layer: Konva.Layer, sourceItem: LocationItem, targetItem: LocationItem, direction: DIRECTION) => {
// 計算出輔助線的位置新新
  const points = getPoints(sourceItem, targetItem, direction);
  var greenLine = new Konva.Line({
    points: points,
    stroke: 'green',
    strokeWidth: 1,
    lineJoin: 'round',
    dash: [10, 10]
  })
  // greenLine.direction = direction

  lines.push(greenLine);
  layer.add(greenLine);
  layer.draw();
}

地址

參考

交流溝通

建立了一個微信交流羣,如需溝通討論,請加入。

image.png

二維碼過期,請添加微信號q1454763497,備註image editor,我會拉你進羣

總結

複製先的實現還是稍微有點複雜,主要是弄明白原理和計算公式,也就簡單了。大家可以把公式補全,留在評論區,鍛鍊下自己的分析能力。部分代碼上面已經描述出來,如需要查看更詳細的內容,請移步fast-image-editor
大家覺得有幫忙,請在github幫忙star一下。

歷史文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章