feat(gesture): 优化移动端手势识别与控制交互

- 调整移动端手势识别阈值,提升识别准确率
- 增加移动端专属手势指令:左右滑动控制前后移动
- 优化手势影响范围计算逻辑,增强触摸屏设备体验
- 完善摄像头权限检查机制,改善用户授权流程
- 支持通过点击提示文字唤起控制台界面
- 统一控制台显示/隐藏逻辑,提高代码可维护性
- 调整响应式布局参数,适配更多设备屏幕尺寸
This commit is contained in:
hehh
2025-12-12 20:15:56 +08:00
parent ad744a0690
commit b424f898db

View File

@@ -330,7 +330,7 @@
.controls-hint {
position: fixed;
left: 15%;
bottom: 5%;
bottom: 3%;
color: rgba(252, 238, 167, 0.7);
font-size: 12px;
z-index: 11;
@@ -396,7 +396,7 @@
<span id="audio-icon">🔇</span>
</div>
<!-- 添加左侧提示文字 -->
<div class="controls-hint">按 H 键唤起操作台</div>
<div class="controls-hint" id="controls-hint">按 H 键/点击唤起操作台</div>
<div id="webcam-wrapper">
<video id="webcam" autoplay playsinline style="display:none;"></video>
@@ -405,6 +405,7 @@
<audio id="bg-music" loop preload="auto">
<source src="data/christmas.mp3" type="audio/mpeg">
您的浏览器不支持音频元素。
</audio>
<script type="module">
@@ -584,7 +585,7 @@
// 实时手势影响 - 只关注当前手势位置,不使用历史轨迹
// 在触摸屏设备上增加手势灵敏度
if (handState.detected && ('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
if (handState.detected) {
// 将2D手势坐标转换为3D空间中的影响点
// 根据屏幕尺寸调整手势影响范围
let handInfluenceRange = 15;
@@ -600,6 +601,11 @@
0
);
// 在触摸屏设备上增强手势影响
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
handInfluenceRange *= 1.5; // 触摸屏设备增强影响范围
}
// 计算粒子与手势影响点之间的距离
const distance = this.mesh.position.distanceTo(handInfluencePoint);
// 根据屏幕尺寸调整最大影响距离
@@ -648,8 +654,8 @@
// 应用影响到目标位置
// 在触摸屏设备上增强手势影响
let influenceMultiplier = 2.0;
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
influenceMultiplier = 2.5; // 触摸屏设备增强影响
if (('ontouchstart' in window || navigator.maxTouchPoints > 0) && window.innerWidth <= 1024) {
influenceMultiplier = 3.0; // 触摸屏设备增强影响
}
target.add(
@@ -938,6 +944,27 @@
numHands: 1
});
// 检查摄像头权限状态
try {
const permissionStatus = await navigator.permissions.query({name: 'camera'});
if (permissionStatus.state === 'granted') {
// 如果已经授予权限,直接启动摄像头
startWebcam();
} else if (permissionStatus.state === 'prompt') {
// 如果需要用户授权,显示提示让用户主动点击启动
showCameraPermissionPrompt();
} else {
// 权限被拒绝,显示提示
console.warn("摄像头权限已被拒绝");
}
} catch (error) {
// 浏览器不支持权限查询API直接尝试启动摄像头
startWebcam();
}
}
// 启动摄像头
async function startWebcam() {
if (navigator.mediaDevices?.getUserMedia) {
try {
const stream = await navigator.mediaDevices.getUserMedia({video: true});
@@ -949,6 +976,34 @@
}
}
// 显示摄像头权限提示
function showCameraPermissionPrompt() {
// 创建提示元素
const promptDiv = document.createElement('div');
promptDiv.id = 'camera-prompt';
promptDiv.innerHTML = `
<div style="position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8); color: white; padding: 15px; border-radius: 10px;
text-align: center; z-index: 1000; backdrop-filter: blur(5px);">
<p>需要摄像头权限以启用手势控制功能</p>
<button id="enable-camera" style="background: #4CAF50; color: white; border: none;
padding: 10px 20px; border-radius: 5px; cursor: pointer;
margin-top: 10px;">启用摄像头</button>
</div>
`;
document.body.appendChild(promptDiv);
// 添加点击事件
document.getElementById('enable-camera').addEventListener('click', async () => {
try {
await startWebcam();
promptDiv.remove();
} catch (error) {
console.error("启动摄像头失败:", error);
}
});
}
let lastVideoTime = -1;
async function predictWebcam() {
if (video.currentTime !== lastVideoTime) {
@@ -1002,13 +1057,22 @@
let avgTipDist = tips.reduce((sum, t) => sum + Math.hypot(t.x - wrist.x, t.y - wrist.y), 0) / 4;
// 更精确的手势识别逻辑(调整阈值以便更容易识别)
const isIndexPinching = thumbIndexDist < 0.07; // 稍微放宽条件
const isMiddlePinching = thumbMiddleDist < 0.07; // 稍微放宽条件
const isRingPinching = thumbRingDist < 0.07; // 稍微放宽条件
const isPinkyPinching = thumbPinkyDist < 0.07; // 稍微放宽条件
// 根据屏幕尺寸调整手势识别阈值,在移动设备上适当放宽条件
let pinchThreshold = 0.07;
let fistThreshold = 0.25;
if (window.innerWidth <= 1024) { // 移动端和小屏幕设备
pinchThreshold = 0.09; // 放宽捏合条件
fistThreshold = 0.30; // 放宽握拳条件
}
const isIndexPinching = thumbIndexDist < pinchThreshold;
const isMiddlePinching = thumbMiddleDist < pinchThreshold;
const isRingPinching = thumbRingDist < pinchThreshold;
const isPinkyPinching = thumbPinkyDist < pinchThreshold;
// 握拳检测(所有指尖都靠近手腕)
const isFist = avgTipDist < 0.25;
const isFist = avgTipDist < fistThreshold;
// 手势指令识别
if (isIndexPinching && !isMiddlePinching && !isRingPinching && !isPinkyPinching) {
@@ -1023,6 +1087,14 @@
} else if (!isFist && tips.some(t => t.y > wrist.y + 0.1)) {
// 有些指尖在手腕下方(手向下)-> 左旋转
newGestureCommand = 'LEFT_ROTATE';
// 为移动设备增加简单的手势识别
} else if (window.innerWidth <= 1024 && !isFist && tips.some(t => t.x < wrist.x - 0.1)) {
// 在移动设备上,手指在手腕左侧 -> 向前移动
newGestureCommand = 'MOVE_FORWARD';
} else if (window.innerWidth <= 1024 && !isFist && tips.some(t => t.x > wrist.x + 0.1)) {
// 在移动设备上,手指在手腕右侧 -> 向后移动
newGestureCommand = 'MOVE_BACKWARD';
}
// 模式切换逻辑(修复冲突问题)
@@ -1104,15 +1176,19 @@
// H 键控制控制台的显示/隐藏 (标题保持不变)
const controlsContainer = document.getElementById('controls-container');
const controlsHint = document.getElementById('controls-hint');
window.addEventListener('keydown', (e) => {
if (e.key.toLowerCase() === 'h') {
if (controlsContainer) {
controlsContainer.classList.toggle('ui-hidden');
STATE.uiVisible = !controlsContainer.classList.contains('ui-hidden');
}
toggleControls();
}
});
// 为提示文字添加点击事件
if (controlsHint) {
controlsHint.addEventListener('click', toggleControls);
}
// 页面加载完成后,默认隐藏控制面板
window.addEventListener('load', () => {
if (controlsContainer) {
@@ -1122,6 +1198,15 @@
});
}
// 控制操作台显示/隐藏的函数
function toggleControls() {
const controlsContainer = document.getElementById('controls-container');
if (controlsContainer) {
controlsContainer.classList.toggle('ui-hidden');
STATE.uiVisible = !controlsContainer.classList.contains('ui-hidden');
}
}
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);