本文正在参加「金石计划」
虚拟摇杆组件的封装
最近面试了一家 web3d 元宇宙的公司,需要我做一个 虚拟摇杆,虽然没有去,但是闲下来还是做了下
思路
- 两个圆圈,一个大圆,一个小圆
- 大圆位置,鼠标点击固定,小圆位置,跟随鼠标移动
- 小圆永远在大圆内
嗯,就这么简单
实现
首先,画出两个圆,效果图
<template>
<div v-if="isShowVirtualRocker" id="virtual-rocker" ref="virtualRocker">
<div id="pointer-target" ref="pointerTarget"></div>
</div>
</template>
<script lang="ts" setup>
import { Ref, onMounted, onUnmounted, ref, reactive, nextTick } from 'vue';
// size 决定 虚拟摇杆大小
const props = defineProps({
size1: {
type: String,
default: "200px"
},
size2: {
type: String,
default: "40px"
}
})
let isShowVirtualRocker: Ref<boolean> = ref(false) // 控制虚拟摇杆的显示和隐藏
let virtualRocker: Ref<HTMLElement | null> = ref(null) // 大圆的 dom
let pointerTarget: Ref<HTMLElement | null> = ref(null); // 小圆的 dom
let startPosition = reactive({ x: 0, y: 0 }) // 初始位置
let endPosition = reactive({ x: 0, y: 0 }) // 移动位置
</script>
<style lang="scss" scoped>
#virtual-rocker {
width: v-bind(size1);
height: v-bind(size1);
background-color: #444444;
opacity: 0.5;
position: absolute;
border-radius: 50%;
z-index: 999;
#pointer-target {
width: v-bind(size2);
height: v-bind(size2);
background-color: #eeeeee;
opacity: 0.5;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
}
}
</style>
其次,添加监听事件 和 销毁事件
注意1,这里获取鼠标位置使用的是 clientX 和 clientY ,用 offsetX 和 offsetY 会存在一个原点闪烁的BUG,这个问题在我鼠标点击后,使用微信截图,此时鼠标放开,但是 mouseup 并未监听到,在之后的获取位置中,原点位置会在页面的左上角和父元素的左上角反复横跳,造成原点闪烁
注意2,vue中对虚拟dom的操作是异步操作,vue会创建一个队列,在确定没有响应式的数据更新后再执行渲染,来进行优化,所以在对 dom 元素设置可见后,直接获取是获取不到更新后的元素,需要在 nexttick 中获取,或者在settimeout 中获取
onMounted(() => {
// 事件监听
window.addEventListener("mousedown", onMousedown)
window.addEventListener("mousemove", onMousemove)
window.addEventListener("mouseup", onMouseup)
})
onUnmounted(() => {
// 事件监听移除
window.removeEventListener("mousedown", onMousedown)
window.removeEventListener("mousemove", onMousemove)
window.removeEventListener("mouseup", onMouseup)
})
mousedown
const onMousedown: (this: Window, ev: MouseEvent) => any = (event) => {
// 如果已经处于mousedown状态,不做处理
if (isShowVirtualRocker.value) return
const X = event.clientX
const Y = event.clientY
startPosition.x = X
startPosition.y = Y
isShowVirtualRocker.value = true
// vue 对虚拟dom的操作是异步操作,所以需要 异步 才可以获取到组件
nextTick(() => {
if (!virtualRocker.value) return
virtualRocker.value.style.left = X + "px"
virtualRocker.value.style.top = Y + "px"
virtualRocker.value.style.transform = "translate(-50%,-50%)"
});
}
mousemove
const onMousemove: (this: Window, ev: MouseEvent) => any = (event) => {
if (!virtualRocker.value || !pointerTarget.value) return
// 获取 当前鼠标 坐标
const X = event.clientX
const Y = event.clientY
endPosition.x = X
endPosition.y = Y
// 计算 当前鼠标坐标与 初始位置的距离
const distance = Math.sqrt(Math.pow((X - startPosition.x), 2) + Math.pow((Y - startPosition.y), 2))
// 鼠标移动到虚拟摇杆范围外,必须要控制 摇杆中心在 虚拟摇杆范围内
if (distance > parseInt(props.size1) / 2) {
// 利用三角函数计算 摇杆中心 在虚拟摇杆的中的位置
const tan = (Y - startPosition.y) / (X - startPosition.x)
const sin = Math.abs(Math.sin(Math.atan(tan)))
const cos = Math.abs(Math.cos(Math.atan(tan)))
const moveX = (X - startPosition.x) > 0 ? parseInt(props.size1) / 2 * cos : -parseInt(props.size1) / 2 * cos
const moveY = (Y - startPosition.y) > 0 ? parseInt(props.size1) / 2 * sin : -parseInt(props.size1) / 2 * sin
pointerTarget.value.style.left = moveX + parseInt(props.size1) / 2 + "px"
pointerTarget.value.style.top = moveY + parseInt(props.size1) / 2 + "px"
pointerTarget.value.style.transform = "translate(-50%,-50%)"
} else {
// 鼠标在虚拟摇杆范围内
pointerTarget.value.style.left = X - startPosition.x + parseInt(props.size1) / 2 + "px"
pointerTarget.value.style.top = Y - startPosition.y + parseInt(props.size1) / 2 + "px"
pointerTarget.value.style.transform = "translate(-50%,-50%)"
}
}
mouseup
const onMouseup: (this: Window, ev: MouseEvent) => any = (event) => {
isShowVirtualRocker.value = false
startPosition.x = 0
startPosition.y = 0
endPosition.x = 0
endPosition.y = 0
}
计算部分,简单的三角函数,鼠标超出边界,将摇杆中心固定在 摇杆大圆圈的边界
就实现了,简单的摇杆,WASD 控制的话,可以自行再添加 8 个相对坐标
原文链接:https://juejin.cn/post/7221551421832118328 作者:你也向往长安城吗