feat(gesture): 优化移动端手势识别与控制交互
- 调整移动端手势识别阈值,提升识别准确率 - 增加移动端专属手势指令:左右滑动控制前后移动 - 优化手势影响范围计算逻辑,增强触摸屏设备体验 - 完善摄像头权限检查机制,改善用户授权流程 - 支持通过点击提示文字唤起控制台界面 - 统一控制台显示/隐藏逻辑,提高代码可维护性 - 调整响应式布局参数,适配更多设备屏幕尺寸
This commit is contained in:
113
christmas.html
113
christmas.html
@@ -330,7 +330,7 @@
|
|||||||
.controls-hint {
|
.controls-hint {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 15%;
|
left: 15%;
|
||||||
bottom: 5%;
|
bottom: 3%;
|
||||||
color: rgba(252, 238, 167, 0.7);
|
color: rgba(252, 238, 167, 0.7);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
@@ -396,7 +396,7 @@
|
|||||||
<span id="audio-icon">🔇</span>
|
<span id="audio-icon">🔇</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 添加左侧提示文字 -->
|
<!-- 添加左侧提示文字 -->
|
||||||
<div class="controls-hint">按 H 键唤起操作台</div>
|
<div class="controls-hint" id="controls-hint">按 H 键/点击唤起操作台</div>
|
||||||
|
|
||||||
<div id="webcam-wrapper">
|
<div id="webcam-wrapper">
|
||||||
<video id="webcam" autoplay playsinline style="display:none;"></video>
|
<video id="webcam" autoplay playsinline style="display:none;"></video>
|
||||||
@@ -405,6 +405,7 @@
|
|||||||
|
|
||||||
<audio id="bg-music" loop preload="auto">
|
<audio id="bg-music" loop preload="auto">
|
||||||
<source src="data/christmas.mp3" type="audio/mpeg">
|
<source src="data/christmas.mp3" type="audio/mpeg">
|
||||||
|
您的浏览器不支持音频元素。
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@@ -584,7 +585,7 @@
|
|||||||
|
|
||||||
// 实时手势影响 - 只关注当前手势位置,不使用历史轨迹
|
// 实时手势影响 - 只关注当前手势位置,不使用历史轨迹
|
||||||
// 在触摸屏设备上增加手势灵敏度
|
// 在触摸屏设备上增加手势灵敏度
|
||||||
if (handState.detected && ('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
|
if (handState.detected) {
|
||||||
// 将2D手势坐标转换为3D空间中的影响点
|
// 将2D手势坐标转换为3D空间中的影响点
|
||||||
// 根据屏幕尺寸调整手势影响范围
|
// 根据屏幕尺寸调整手势影响范围
|
||||||
let handInfluenceRange = 15;
|
let handInfluenceRange = 15;
|
||||||
@@ -600,6 +601,11 @@
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 在触摸屏设备上增强手势影响
|
||||||
|
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
|
||||||
|
handInfluenceRange *= 1.5; // 触摸屏设备增强影响范围
|
||||||
|
}
|
||||||
|
|
||||||
// 计算粒子与手势影响点之间的距离
|
// 计算粒子与手势影响点之间的距离
|
||||||
const distance = this.mesh.position.distanceTo(handInfluencePoint);
|
const distance = this.mesh.position.distanceTo(handInfluencePoint);
|
||||||
// 根据屏幕尺寸调整最大影响距离
|
// 根据屏幕尺寸调整最大影响距离
|
||||||
@@ -648,8 +654,8 @@
|
|||||||
// 应用影响到目标位置
|
// 应用影响到目标位置
|
||||||
// 在触摸屏设备上增强手势影响
|
// 在触摸屏设备上增强手势影响
|
||||||
let influenceMultiplier = 2.0;
|
let influenceMultiplier = 2.0;
|
||||||
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
|
if (('ontouchstart' in window || navigator.maxTouchPoints > 0) && window.innerWidth <= 1024) {
|
||||||
influenceMultiplier = 2.5; // 触摸屏设备增强影响
|
influenceMultiplier = 3.0; // 触摸屏设备增强影响
|
||||||
}
|
}
|
||||||
|
|
||||||
target.add(
|
target.add(
|
||||||
@@ -938,6 +944,27 @@
|
|||||||
numHands: 1
|
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) {
|
if (navigator.mediaDevices?.getUserMedia) {
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({video: true});
|
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;
|
let lastVideoTime = -1;
|
||||||
async function predictWebcam() {
|
async function predictWebcam() {
|
||||||
if (video.currentTime !== lastVideoTime) {
|
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;
|
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; // 稍微放宽条件
|
let pinchThreshold = 0.07;
|
||||||
const isRingPinching = thumbRingDist < 0.07; // 稍微放宽条件
|
let fistThreshold = 0.25;
|
||||||
const isPinkyPinching = thumbPinkyDist < 0.07; // 稍微放宽条件
|
|
||||||
|
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) {
|
if (isIndexPinching && !isMiddlePinching && !isRingPinching && !isPinkyPinching) {
|
||||||
@@ -1023,6 +1087,14 @@
|
|||||||
} else if (!isFist && tips.some(t => t.y > wrist.y + 0.1)) {
|
} else if (!isFist && tips.some(t => t.y > wrist.y + 0.1)) {
|
||||||
// 有些指尖在手腕下方(手向下)-> 左旋转
|
// 有些指尖在手腕下方(手向下)-> 左旋转
|
||||||
newGestureCommand = 'LEFT_ROTATE';
|
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 键控制控制台的显示/隐藏 (标题保持不变)
|
// H 键控制控制台的显示/隐藏 (标题保持不变)
|
||||||
const controlsContainer = document.getElementById('controls-container');
|
const controlsContainer = document.getElementById('controls-container');
|
||||||
|
const controlsHint = document.getElementById('controls-hint');
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (e.key.toLowerCase() === 'h') {
|
if (e.key.toLowerCase() === 'h') {
|
||||||
if (controlsContainer) {
|
toggleControls();
|
||||||
controlsContainer.classList.toggle('ui-hidden');
|
|
||||||
STATE.uiVisible = !controlsContainer.classList.contains('ui-hidden');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 为提示文字添加点击事件
|
||||||
|
if (controlsHint) {
|
||||||
|
controlsHint.addEventListener('click', toggleControls);
|
||||||
|
}
|
||||||
|
|
||||||
// 页面加载完成后,默认隐藏控制面板
|
// 页面加载完成后,默认隐藏控制面板
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
if (controlsContainer) {
|
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() {
|
function animate() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
const dt = Math.min(clock.getDelta(), 0.1);
|
const dt = Math.min(clock.getDelta(), 0.1);
|
||||||
|
|||||||
Reference in New Issue
Block a user