如何實現將拖動物體限制在某個圓形內--實現方式vue3.0

語言: CN / TW / HK

如何實現藍色小圓可拖動,並且邊界限制在灰色大圓內?如下所示

需求源自 業務上遇到一個元件需求,設計師設計了一個“臉型整合器”根據可拖動小圓的位置與其它臉型的位置關係計算融合比例

如圖

我們先把具體的人臉功能去掉再分析

中間的藍色小圓可由滑鼠拖動,但拖動需要限制在大的圓形內

以下程式碼全部為 vue3.0版本,如果你是vue2.0, react,或者原生js ,實現原理都一樣

第一步 先畫出UI

我們的藍色可拖動小圓點 pointer = ref(null) 為 小圓點dom的 引用

<template>
  <div class="face-blender-container">
    <div class="blender-circle">
      <div
        class="blend-pointer"
        ref="pointer"
        :style="{
          left: `${pointerPosition.x}px`,
          top: `${pointerPosition.y}px`,
        }"
      ></div>
    </div>
  </div>
</template>

<script>
import { onMounted, reactive, ref, toRefs } from "vue";

export default {
  setup() {
    const BLENDER_BORDER_WIDTH = 2; // 圓形混合器邊寬
    const BLENDER_RADIUS = 224 * 0.5 - BLENDER_BORDER_WIDTH; // 圓形混合器半徑
    // 圓形混合器中心點
    const center = {
      x: BLENDER_RADIUS,
      y: BLENDER_RADIUS,
    };
    const state = reactive({
      pointerPosition: { x: center.x, y: center.y },
    });
    const pointer = ref(null);
    

    return {
      ...toRefs(state),
      pointer,
    };
  },
};
</script>

<style lang="less" scoped>
@stageDiameter: 360px;
@blenderCircleDiameter: 224px;
@PointerDiameter: 20px;
.face-blender-container {
  position: relative;
  width: @stageDiameter;
  height: @stageDiameter;
}
.blender-circle {
  position: absolute;
  left: 50%;
  top: 50%;
  margin-left: @blenderCircleDiameter * -0.5;
  margin-top: @blenderCircleDiameter * -0.5;
  width: @blenderCircleDiameter;
  height: @blenderCircleDiameter;
  border-radius: @blenderCircleDiameter;
  background: rgba(255, 255, 255, 0.04);
  border: 2px solid rgba(255, 255, 255, 0.08);
}

.blend-pointer {
  position: absolute;
  left: 50%;
  top: 50%;
  width: @PointerDiameter;
  height: @PointerDiameter;
  margin-left: @PointerDiameter * -0.5;
  margin-top: @PointerDiameter * -0.5;
  border-radius: @PointerDiameter;
  background: #11bbf5;
  border: 2px solid #ffffff;
  z-index: 10;
}
</style>

ui 如圖(整體背景為黑色)

第二步 實現小圓點的無限制拖動

此時小圓點是可以拖到圓外的如圖

// 可拖動圓型指示器
    const initPointer = () => {
      const pointerDom = pointer.value;
      pointerDom.onmousedown = (e) => {
        // 滑鼠按下,計算當前元素距離可視區的距離
        const originX = e.clientX - pointerDom.offsetLeft - POINTER_RADIUS;
        const originY = e.clientY - pointerDom.offsetTop - POINTER_RADIUS;
        document.onmousemove = function (e) {
          // 通過事件委託,計算移動的距離
          const left = e.clientX - originX;
          const top = e.clientY - originY;
          
          state.pointerPosition.x = left
          state.pointerPosition.y = top
        };
        document.onmouseup = function (e) {
          document.onmousemove = null;
          document.onmouseup = null;
        };
      };
    };

    onMounted(() => {
      initPointer();
    });

注意是在onMouted 勾子內初始化的可拖動程式碼

第三步 實現小圓點的限制在大圓內拖動

由於是要限制在圓形內,與限制在方形內的通常計算方法不一樣

關鍵點是計算滑鼠 mousemove 時與大圓中心點的弧度 radian 與 距離 dist

弧度公式

dx = x2 - x1

dy = y2 - y1

radian = Math.atan2(dy, dx)

距離公式

dist = Math.sqrt(dx * dx + dy * dy)

當計算出了弧度與距離後,則要計算具體位置了

圓形位置公式

x = 半徑 * Math.cos(弧度) + 中心點x

y = 半徑 * Math.sin(弧度) + 中心點y

可以看出在圓形公式內控制或限制半徑,就限制了小圓的可拖動最大半徑範圍,所以需要判斷,當dist距離大於等於半徑時,計算圓形公式內的半徑設定為大圓半徑即可

具體程式碼

// 計算 x y
    const getPositionByRadian = (radian, radius) => {
      const x = radius * Math.cos(radian) + center.x;
      const y = radius * Math.sin(radian) + center.y;
      return { x, y };
    };
    // 可拖動圓形指示器
    const initPointer = () => {
      const pointerDom = pointer.value;
      pointerDom.onmousedown = (e) => {
        // 滑鼠按下,計算當前元素距離可視區的距離
        const originX = e.clientX - pointerDom.offsetLeft - POINTER_RADIUS;
        const originY = e.clientY - pointerDom.offsetTop - POINTER_RADIUS;
        document.onmousemove = function (e) {
          // 通過事件委託,計算移動的距離
          const left = e.clientX - originX;
          const top = e.clientY - originY;
          const dx = left - center.x;
          const dy = top - center.y;
          // 計算當前滑鼠與中心點的弧度
          const radian = Math.atan2(dy, dx);
          // 計算當前滑鼠與中心點距離
          const dist = Math.sqrt(dx * dx + dy * dy);
          const radius = dist >= BLENDER_RADIUS ? BLENDER_RADIUS : dist;
          // 根據半徑與弧度計算 x, y
          const { x, y } = getPositionByRadian(radian, radius);
          state.pointerPosition.x = x
          state.pointerPosition.y = y
        };
        document.onmouseup = function (e) {
          document.onmousemove = null;
          document.onmouseup = null;
        };
      };
    };

這樣就實現了最大可拖動範圍限制在大圓邊界

整體程式碼

<template>
  <div class="face-blender-container">
    <div class="blender-circle">
      <div
        class="blend-pointer"
        ref="pointer"
        :style="{
          left: `${pointerPosition.x}px`,
          top: `${pointerPosition.y}px`,
        }"
      ></div>
    </div>
  </div>
</template>

<script>
import { onMounted, reactive, ref, toRefs } from "vue";

export default {
  setup() {
    const BLENDER_BORDER_WIDTH = 2; // 圓形混合器邊寬
    const BLENDER_RADIUS = 224 * 0.5 - BLENDER_BORDER_WIDTH; // 圓形混合器半徑
    const POINTER_RADIUS = 20 * 0.5; // 可拖動指示器半徑
    // 圓形混合器中心點
    const center = {
      x: BLENDER_RADIUS,
      y: BLENDER_RADIUS,
    };
    const state = reactive({
      pointerPosition: { x: center.x, y: center.y },
    });
    const pointer = ref(null);
    // 計算 x y
    const getPositionByRadian = (radian, radius) => {
      const x = radius * Math.cos(radian) + center.x;
      const y = radius * Math.sin(radian) + center.y;
      return { x, y };
    };
    // 可拖動圓型指示器
    const initPointer = () => {
      const pointerDom = pointer.value;
      pointerDom.onmousedown = (e) => {
        // 滑鼠按下,計算當前元素距離可視區的距離
        const originX = e.clientX - pointerDom.offsetLeft - POINTER_RADIUS;
        const originY = e.clientY - pointerDom.offsetTop - POINTER_RADIUS;
        document.onmousemove = function (e) {
          // 通過事件委託,計算移動的距離
          const left = e.clientX - originX;
          const top = e.clientY - originY;
          const dx = left - center.x;
          const dy = top - center.y;
          // 計算當前滑鼠與中心點的弧度
          const radian = Math.atan2(dy, dx);
          // 計算當前滑鼠與中心點距離
          const dist = Math.sqrt(dx * dx + dy * dy);
          const radius = dist >= BLENDER_RADIUS ? BLENDER_RADIUS : dist;
          // 根據半徑與弧度計算 x, y
          const { x, y } = getPositionByRadian(radian, radius);
          state.pointerPosition.x = x
          state.pointerPosition.y = y
        };
        document.onmouseup = function (e) {
          document.onmousemove = null;
          document.onmouseup = null;
        };
      };
    };

    onMounted(() => {
      initPointer();
    });

    return {
      ...toRefs(state),
      pointer,
    };
  },
};
</script>

<style lang="less" scoped>
@stageDiameter: 360px;
@blenderCircleDiameter: 224px;
@faceCircleDiameter: 64px;
@PointerDiameter: 20px;
.face-blender-container {
  position: relative;
  width: @stageDiameter;
  height: @stageDiameter;
}
.blender-circle {
  position: absolute;
  left: 50%;
  top: 50%;
  margin-left: @blenderCircleDiameter * -0.5;
  margin-top: @blenderCircleDiameter * -0.5;
  width: @blenderCircleDiameter;
  height: @blenderCircleDiameter;
  border-radius: @blenderCircleDiameter;
  background: rgba(255, 255, 255, 0.04);
  border: 2px solid rgba(255, 255, 255, 0.08);
}

.blend-pointer {
  position: absolute;
  left: 50%;
  top: 50%;
  width: @PointerDiameter;
  height: @PointerDiameter;
  margin-left: @PointerDiameter * -0.5;
  margin-top: @PointerDiameter * -0.5;
  border-radius: @PointerDiameter;
  background: #11bbf5;
  border: 2px solid #ffffff;
  z-index: 10;
}
</style>

至於其它計算距離,計算反比例的臉形整合業務程式碼,就不放了

臉形容器的位置還是用圓形公式算出來

各個臉形容器與小圓點距離,距離還是用距離公式得出

知道各個距離了就可以根據業務需要算比例了

轉載入註明部落格園池中物 [email protected] sheldon.wang

github: https://github.com/willian12345