From a2b627b4ed1287eb9003cfe03964eebcaec646df Mon Sep 17 00:00:00 2001 From: hehh Date: Thu, 4 Dec 2025 18:52:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(me):=20=E6=B7=BB=E5=8A=A0=E6=97=A0?= =?UTF-8?q?=E6=91=84=E5=83=8F=E5=A4=B4=E6=9D=83=E9=99=90=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E8=BF=9B=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增直接进入按钮和倒计时提示界面 - 实现5秒后自动进入档案馆逻辑 - 添加摄像头权限状态管理和重试机制 - 支持点击按钮立即跳过摄像头授权 - 更新多语言字典中的相关提示文本 - 调整粒子爆炸效果参数提升视觉体验 - 优化手势识别逻辑仅在摄像头启用时运行 - 添加重新申请摄像头权限的交互入口 --- me.html | 256 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 193 insertions(+), 63 deletions(-) diff --git a/me.html b/me.html index 42a67e3..84bd5b6 100644 --- a/me.html +++ b/me.html @@ -104,6 +104,15 @@ transform: translateY(0); } + .direct-enter-container { + text-align: center; + } + + .direct-enter-btn:hover { + background: var(--accent-color); + color: var(--loader-bg); + } + /* === 2. 叙事文本层 (DOM覆盖) === */ #narrative-layer { position: absolute; @@ -358,6 +367,10 @@
正在唤醒灵感...
⚠ 请允许摄像头权限以开启手势交互
+
+
3秒后自动进入
+ +
@@ -412,12 +425,15 @@ startScreen: document.getElementById('start-screen'), loaderMsg: document.getElementById('loader-msg'), permHint: document.getElementById('perm-guide'), - + directEnterContainer: document.getElementById('direct-enter-container'), + directEnterHint: document.getElementById('direct-enter-hint'), + directEnterBtn: document.getElementById('direct-enter-btn'), + // 叙事层相关元素 narrativeLayer: document.getElementById('narrative-layer'), nTitle: document.getElementById('n-title'), nSub: document.getElementById('n-sub'), - + // UI层相关元素 uiLayer: document.getElementById('ui-layer'), sysStatus: document.getElementById('sys-status'), @@ -426,7 +442,7 @@ mainHint: document.getElementById('main-hint'), subHint: document.getElementById('sub-hint'), exitBtn: document.getElementById('exit-btn'), - + // 其他元素 canvasContainer: document.getElementById('canvas-container'), inputVideo: document.getElementById('input-video') @@ -473,6 +489,14 @@ lang: getStoredLanguage() }; + // 摄像头权限状态管理 + const CAMERA_STATE = { + enabled: false, // 摄像头是否已启用 + permissionDenied: false, // 用户是否拒绝了权限 + autoEnterTimeout: null, // 自动进入计时器 + retryTimeout: null // 重试计时器 + }; + // 应用主题 document.documentElement.setAttribute('data-theme', ENV.theme); safeUpdateText(DOM_CACHE.themeDisplay, `THEME: ${ENV.lang.toUpperCase() === 'en' ? ENV.theme.toUpperCase() : ENV.theme === 'day' ? '白天' : '黑夜'}`); @@ -491,7 +515,12 @@ hints: { main: "双手合十 · 解锁档案", sub: "单手·引力牵引|双手·力场排斥", - unlocking: "正在识别..." + unlocking: "正在识别...", + directEnterHint: "5秒后自动进入", + directEnterBtn: "立即进入", + cameraDisabledMainHint: "点击进入档案馆", + cameraDisabledSubHint: "[ 再次申请摄像头权限 ]", + cameraRetryTimeout: "摄像头授权超时,5秒后取消" }, slides: [ {t: "初心", s: "在这喧嚣世界中,依然相信纯粹的力量"}, @@ -509,7 +538,12 @@ hints: { main: "Click or Namaste", sub: "One Hand Drag · Two Hands Repel", - unlocking: "Identifying..." + unlocking: "Identifying...", + directEnterHint: "Auto enter in 5 seconds", + directEnterBtn: "Enter Now", + cameraDisabledMainHint: "Click to Enter Archive", + cameraDisabledSubHint: "[ Request Camera Access Again ]", + cameraRetryTimeout: "Camera authorization timeout, canceling in 5 seconds" }, slides: [ {t: "Honesty", s: "In a noisy world, still believe in the power of simplicity"}, @@ -524,6 +558,16 @@ const CONTENT = DICTIONARY[ENV.lang]; + // 设置直接进入按钮的文本 + safeUpdateText(DOM_CACHE.directEnterBtn, CONTENT.hints.directEnterBtn); + safeUpdateText(DOM_CACHE.directEnterHint, CONTENT.hints.directEnterHint); + + // 直接进入按钮事件处理 + DOM_CACHE.directEnterBtn.addEventListener('click', () => { + clearTimeout(CAMERA_STATE.autoEnterTimeout); + enterWithoutCamera(); + }); + // 循环播放加载文案 let loadIdx = 0; const maxIdx = CONTENT.load.length - 1; @@ -536,9 +580,19 @@ loadIdx++; }, 600); - // 权限超时提示 + // 权限超时提示和自动进入功能 setTimeout(() => { - if (!APP_STATE.isLoaded) safeClass(DOM_CACHE.permHint, 'add', 'show'); + if (!APP_STATE.isLoaded) { + safeClass(DOM_CACHE.permHint, 'add', 'show'); + + // 显示直接进入选项 + DOM_CACHE.directEnterContainer.style.opacity = '1'; + + // 3秒后自动进入 + CAMERA_STATE.autoEnterTimeout = setTimeout(() => { + enterWithoutCamera(); + }, 5000); + } }, 5000); safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main); @@ -808,9 +862,41 @@ */ let narrativeTimer = null; + // 无摄像头进入档案馆 + function enterWithoutCamera() { + if (APP_STATE.mode === 'UNLOCKED') return; + APP_STATE.mode = 'UNLOCKED'; + CAMERA_STATE.enabled = false; + + // 隐藏加载屏幕 + const loader = DOM_CACHE.startScreen; + loader.style.opacity = 0; + DOM_CACHE.uiLayer.style.opacity = 1; + setTimeout(() => loader.style.display = 'none', 1000); + + // 更新UI提示 + safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.cameraDisabledMainHint); + safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.cameraDisabledSubHint); + + // 显示退出按钮 + safeClass(DOM_CACHE.exitBtn, 'add', 'visible'); + + // 粒子爆炸效果 + explode(300); + + startNarrative(); + } + window.enterArchive = function () { if (APP_STATE.mode === 'UNLOCKED') return; APP_STATE.mode = 'UNLOCKED'; + CAMERA_STATE.enabled = true; + + // 隐藏加载屏幕 + const loader = DOM_CACHE.startScreen; + loader.style.opacity = 0; + DOM_CACHE.uiLayer.style.opacity = 1; + setTimeout(() => loader.style.display = 'none', 1000); // UI safeClass(DOM_CACHE.mainHint, 'add', 'hidden'); // 隐藏主提示 (CSS需支持或直接display) @@ -819,7 +905,7 @@ safeClass(DOM_CACHE.exitBtn, 'add', 'visible'); // 粒子爆炸效果 - explode(100); + explode(300); startNarrative(); } @@ -828,13 +914,23 @@ APP_STATE.mode = 'LOCKED'; APP_STATE.exitCooldownUntil = Date.now() + 2000; - DOM_CACHE.mainHint.style.display = 'block'; - DOM_CACHE.subHint.style.display = 'block'; + // 根据摄像头状态更新UI + if (CAMERA_STATE.enabled) { + DOM_CACHE.mainHint.style.display = 'block'; + DOM_CACHE.subHint.style.display = 'block'; + safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main); + safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.sub); + } else { + DOM_CACHE.mainHint.style.display = 'block'; + DOM_CACHE.subHint.style.display = 'block'; + safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.cameraDisabledMainHint); + safeUpdateText(DOM_CACHE.subHint, CONTENT.hints.cameraDisabledSubHint); + } + safeClass(DOM_CACHE.exitBtn, 'remove', 'visible'); safeClass(DOM_CACHE.narrativeLayer, 'remove', 'show-text'); APP_STATE.unlockProgress = 0; - safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main); clearTimeout(narrativeTimer); explode(50); } @@ -897,68 +993,98 @@ // Loader logic if (!APP_STATE.isLoaded) { APP_STATE.isLoaded = true; - const loader = DOM_CACHE.startScreen; - loader.style.opacity = 0; - DOM_CACHE.uiLayer.style.opacity = 1; - setTimeout(() => loader.style.display = 'none', 1000); + CAMERA_STATE.enabled = true; + + // 清除自动进入定时器 + clearTimeout(CAMERA_STATE.autoEnterTimeout); } - const landmarks = results.multiHandLandmarks; - const sysStatus = DOM_CACHE.sysStatus; + // 只有在摄像头启用时才处理手势 + if (CAMERA_STATE.enabled) { + const landmarks = results.multiHandLandmarks; + const sysStatus = DOM_CACHE.sysStatus; - if (landmarks && landmarks.length > 0) { - APP_STATE.handCount = landmarks.length; - safeUpdateText(sysStatus, `LINKED (${APP_STATE.handCount})`); - if (sysStatus) sysStatus.style.color = ENV.theme === 'day' ? '#0044cc' : '#00ff00'; + if (landmarks && landmarks.length > 0) { + APP_STATE.handCount = landmarks.length; + safeUpdateText(sysStatus, `LINKED (${APP_STATE.handCount})`); + if (sysStatus) sysStatus.style.color = ENV.theme === 'day' ? '#0044cc' : '#00ff00'; - // 坐标处理 (镜像) - const process = (lm) => ({ - x: ((1.0 - lm.x) * 2 - 1) * 800, - y: -(lm.y * 2 - 1 - 0.2) * 600 - }); + // 坐标处理 (镜像) + const process = (lm) => ({ + x: ((1.0 - lm.x) * 2 - 1) * 800, + y: -(lm.y * 2 - 1 - 0.2) * 600 + }); - const p1 = process(landmarks[0][9]); - APP_STATE.handL.x += (p1.x - APP_STATE.handL.x) * 0.25; - APP_STATE.handL.y += (p1.y - APP_STATE.handL.y) * 0.25; + const p1 = process(landmarks[0][9]); + APP_STATE.handL.x += (p1.x - APP_STATE.handL.x) * 0.25; + APP_STATE.handL.y += (p1.y - APP_STATE.handL.y) * 0.25; - if (landmarks.length > 1) { - const p2 = process(landmarks[1][9]); - APP_STATE.handR.x += (p2.x - APP_STATE.handR.x) * 0.25; - APP_STATE.handR.y += (p2.y - APP_STATE.handR.y) * 0.25; - } - - // 合十检测 - if (landmarks.length === 2 && APP_STATE.mode === 'LOCKED' && Date.now() > APP_STATE.exitCooldownUntil) { - const w1 = landmarks[0][0], w2 = landmarks[1][0]; - const i1 = landmarks[0][8], i2 = landmarks[1][8]; - const m1 = landmarks[0][12], m2 = landmarks[1][12]; - const dW = Math.hypot(w1.x - w2.x, w1.y - w2.y); - const dI = Math.hypot(i1.x - i2.x, i1.y - i2.y); - const dM = Math.hypot(m1.x - m2.x, m1.y - m2.y); - const okW = dW < 0.28; - const okI = dI < 0.20; - const okM = dM < 0.20; - const ok = ((okW ? 1 : 0) + (okI ? 1 : 0) + (okM ? 1 : 0)) >= 2; - if (ok) { - APP_STATE.namasteStableFrames++; - } else { - APP_STATE.namasteStableFrames = Math.max(0, APP_STATE.namasteStableFrames - 1); + if (landmarks.length > 1) { + const p2 = process(landmarks[1][9]); + APP_STATE.handR.x += (p2.x - APP_STATE.handR.x) * 0.25; + APP_STATE.handR.y += (p2.y - APP_STATE.handR.y) * 0.25; } - const targetFrames = 20; - const pct = Math.min(100, Math.round(APP_STATE.namasteStableFrames / targetFrames * 100)); - if (pct > 0) safeUpdateText(DOM_CACHE.mainHint, `${CONTENT.hints.unlocking} ${pct}%`); - if (APP_STATE.namasteStableFrames >= targetFrames) enterArchive(); - if (!ok && APP_STATE.namasteStableFrames === 0) safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main); + + // 合十检测 + if (landmarks.length === 2 && APP_STATE.mode === 'LOCKED' && Date.now() > APP_STATE.exitCooldownUntil) { + const w1 = landmarks[0][0], w2 = landmarks[1][0]; + const i1 = landmarks[0][8], i2 = landmarks[1][8]; + const m1 = landmarks[0][12], m2 = landmarks[1][12]; + const dW = Math.hypot(w1.x - w2.x, w1.y - w2.y); + const dI = Math.hypot(i1.x - i2.x, i1.y - i2.y); + const dM = Math.hypot(m1.x - m2.x, m1.y - m2.y); + const okW = dW < 0.28; + const okI = dI < 0.20; + const okM = dM < 0.20; + const ok = ((okW ? 1 : 0) + (okI ? 1 : 0) + (okM ? 1 : 0)) >= 2; + if (ok) { + APP_STATE.namasteStableFrames++; + } else { + APP_STATE.namasteStableFrames = Math.max(0, APP_STATE.namasteStableFrames - 1); + } + const targetFrames = 20; + const pct = Math.min(100, Math.round(APP_STATE.namasteStableFrames / targetFrames * 100)); + if (pct > 0) safeUpdateText(DOM_CACHE.mainHint, `${CONTENT.hints.unlocking} ${pct}%`); + if (APP_STATE.namasteStableFrames >= targetFrames) enterArchive(); + if (!ok && APP_STATE.namasteStableFrames === 0) safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main); + } + } else { + APP_STATE.handCount = 0; + safeUpdateText(sysStatus, 'SEARCHING...'); + if (sysStatus) sysStatus.style.color = 'inherit'; + APP_STATE.unlockProgress = 0; + safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main); } - } else { - APP_STATE.handCount = 0; - safeUpdateText(sysStatus, 'SEARCHING...'); - if (sysStatus) sysStatus.style.color = 'inherit'; - APP_STATE.unlockProgress = 0; - safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main); } }); + DOM_CACHE.subHint.addEventListener('click', () => { + // 只有在无摄像头模式下才允许重新申请权限 + if (!CAMERA_STATE.enabled && APP_STATE.mode === 'UNLOCKED') { + requestCameraAccess(); + } + }); + + // 请求摄像头权限 + function requestCameraAccess() { + // 更新提示文本 + safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.cameraRetryTimeout); + + // 尝试启动摄像头 + cameraUtils.start().then(() => { + // 成功启动,隐藏提示 + safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main); + }).catch(err => { + console.log("Camera access error:", err); + }); + + // 5秒后取消 + CAMERA_STATE.retryTimeout = setTimeout(() => { + cameraUtils.stop(); + safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.cameraDisabledMainHint); + }, 5000); + } + const videoElement = DOM_CACHE.inputVideo; const cameraUtils = new Camera(videoElement, { onFrame: async () => { @@ -966,7 +1092,11 @@ }, width: 640, height: 480 }); - cameraUtils.start(); + + // 初始启动摄像头 + cameraUtils.start().catch(err => { + console.log("Initial camera access denied:", err); + }); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight;