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 @@
正在唤醒灵感...
⚠ 请允许摄像头权限以开启手势交互
+
@@ -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;