如何实现蓝色小圆可拖动,并且边界限制在灰色大圆内?如下所示
需求源自 业务上遇到一个组件需求,设计师设计了一个“脸型整合器”根据可拖动小圆的位置与其它脸型的位置关系计算融合比例
如图
我们先把具体的人脸功能去掉再分析
中间的蓝色小圆可由鼠标拖动,但拖动需要限制在大的圆形内
以下代码全部为 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>
至于其它计算距离,计算反比例的脸形整合业务代码,就不放了
脸形容器的位置还是用圆形公式算出来
各个脸形容器与小圆点距离,距离还是用距离公式得出
知道各个距离了就可以根据业务需要算比例了