Files
photo-live/public/index.html
T
JianMiauandClaude Fable 5 ffe3dc366f 移除掃描框與載入張數顯示
摘要:
掃描頁不再顯示 MindAR 掃描框動畫,開始畫面也不再顯示「已載入 N 張動態相片」。

根本原因:
使用者不需要這兩個提示,畫面更乾淨。

影響:
掃描時只看到相機畫面;開始畫面狀態列留空(錯誤訊息仍會顯示)。

修法:
mindar-image 的 uiScanning 改為 no;載入完成後狀態文字改為空字串。版本標示 v6。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:05:51 +08:00

218 lines
7.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>動態相片</title>
<script src="/vendor/aframe.min.js"></script>
<script src="/vendor/mindar-image-aframe.prod.js"></script>
<style>
/* 背景只能放在 htmlroot canvas),不能放在 body
MindAR 的相機 video 是 body 下 z-index:-2 的元素,body 自己的背景會蓋住它 */
html { background: #000; }
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
#overlay {
position: fixed; inset: 0; z-index: 1000;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 16px; padding: 24px; text-align: center;
background: radial-gradient(circle at 50% 30%, #1c2333, #0b0e16 70%);
color: #e8ecf5; font-family: system-ui, -apple-system, "Noto Sans TC", sans-serif;
}
#overlay.hidden { display: none; }
#overlay h1 { margin: 0; font-size: 28px; font-weight: 700; letter-spacing: 2px; }
#status { margin: 0; font-size: 15px; color: #9aa4bd; min-height: 1.4em; }
#startBtn {
padding: 14px 48px; font-size: 18px; font-weight: 600; letter-spacing: 4px;
color: #0b0e16; background: #e8ecf5; border: none; border-radius: 999px; cursor: pointer;
}
#startBtn:active { transform: scale(0.97); }
.hint { font-size: 13px; color: #6b7590; max-width: 320px; line-height: 1.7; }
#stopBtn {
position: fixed; top: 16px; right: 16px; z-index: 1001;
width: 40px; height: 40px; border-radius: 50%; border: none; cursor: pointer;
background: rgba(0,0,0,.5); color: #fff; font-size: 18px; display: none;
}
a { color: #8fb4ff; }
</style>
</head>
<body>
<div id="overlay">
<h1>動態相片</h1>
<p id="status">載入中…</p>
<button id="startBtn" hidden>開始掃描</button>
<p class="hint">按下開始後,將相機對準已登錄的照片,<br>對應的影片就會覆蓋在照片上播放。</p>
<p class="hint" style="font-size:11px">v6</p>
</div>
<button id="stopBtn" title="停止"></button>
<script>
const overlay = document.getElementById('overlay');
const statusEl = document.getElementById('status');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
let sceneEl = null;
const videos = [];
async function init() {
let state;
try {
state = await (await fetch('/api/state')).json();
} catch (e) {
statusEl.textContent = '無法連線到伺服器';
return;
}
const adminUrl = `http://${location.hostname}:${state.ports.admin}/`;
if (!state.targets || !state.targets.targets.length) {
statusEl.innerHTML = `尚未建立任何動態相片。<br>請先到 <a href="${adminUrl}">管理頁</a> 新增照片與影片並編譯。`;
return;
}
if (!window.isSecureContext) {
statusEl.innerHTML = '瀏覽器要求安全連線才能開啟相機,<br>請改用 <b>https://</b> 連到本頁。';
return;
}
const pairById = Object.fromEntries(state.pairs.map(p => [p.id, p]));
const entries = state.targets.targets
.map((t, index) => ({ ...t, index, pair: pairById[t.pairId] }))
.filter(t => t.pair);
if (!entries.length) {
statusEl.innerHTML = `目標檔已過期,請到 <a href="${adminUrl}">管理頁</a> 重新編譯。`;
return;
}
buildScene(state.targets.compiledAt, entries);
statusEl.textContent = '';
startBtn.hidden = false;
}
function buildScene(version, entries) {
sceneEl = document.createElement('a-scene');
sceneEl.setAttribute('mindar-image',
`imageTargetSrc: /data/targets.mind?v=${version}; autoStart: false; maxTrack: 1; ` +
'filterMinCF: 0.0001; filterBeta: 0.001; uiLoading: no; uiScanning: no; uiError: yes;');
sceneEl.setAttribute('embedded', '');
sceneEl.setAttribute('color-space', 'sRGB');
sceneEl.setAttribute('renderer', 'colorManagement: true; physicallyCorrectLights: true');
sceneEl.setAttribute('vr-mode-ui', 'enabled: false');
sceneEl.setAttribute('device-orientation-permission-ui', 'enabled: false');
const assets = document.createElement('a-assets');
sceneEl.appendChild(assets);
const camera = document.createElement('a-camera');
camera.setAttribute('position', '0 0 0');
camera.setAttribute('look-controls', 'enabled: false');
sceneEl.appendChild(camera);
for (const t of entries) {
const video = document.createElement('video');
video.id = 'vid-' + t.index;
video.src = '/data/videos/' + encodeURIComponent(t.pair.video);
video.loop = true;
video.preload = 'auto';
video.crossOrigin = 'anonymous';
video.setAttribute('playsinline', '');
video.setAttribute('webkit-playsinline', '');
// 影片一律以靜音狀態建立,直到第一次掃到照片才解除靜音,
// 確保解鎖過程無論發生什麼載入競態都不可能出聲
video.muted = true;
video.defaultMuted = true;
video.setAttribute('muted', '');
assets.appendChild(video);
videos.push(video);
const anchor = document.createElement('a-entity');
anchor.setAttribute('mindar-image-target', 'targetIndex: ' + t.index);
// MindAR 的目標寬度固定為 1,用照片長寬比讓影片剛好蓋住整張照片
const plane = document.createElement('a-video');
plane.setAttribute('src', '#vid-' + t.index);
plane.setAttribute('width', '1');
plane.setAttribute('height', String(t.aspect || 1));
plane.setAttribute('position', '0 0 0');
anchor.appendChild(plane);
let isFound = false;
// 保險:照片不在畫面上時若影片意外開始播放,立刻壓回去
video.addEventListener('playing', () => {
if (!isFound) {
video.pause();
video.currentTime = 0;
}
});
anchor.addEventListener('targetFound', () => {
isFound = true;
video.muted = false; // 掃到照片這一刻才解除靜音
video.defaultMuted = false;
video.removeAttribute('muted');
video.play().catch(() => {
video.muted = true; // 有聲播放被瀏覽器擋下時退回靜音播放
video.play().catch(() => {});
});
});
anchor.addEventListener('targetLost', () => {
isFound = false;
video.pause();
});
sceneEl.appendChild(anchor);
}
document.body.appendChild(sceneEl);
}
startBtn.addEventListener('click', async () => {
startBtn.disabled = true;
statusEl.textContent = '啟動相機中…';
// 在使用者手勢中先 play/pause 一次,解鎖行動裝置的自動播放限制
// (解鎖許可綁定各個影片元素,之後掃到照片才能自動帶聲音播放)。
// 影片維持靜音,直到 targetFound 才解除,解鎖過程絕不出聲
for (const v of videos) {
// 重新靜音(之前掃到過照片的影片會處於未靜音狀態)
v.muted = true;
v.defaultMuted = true;
v.setAttribute('muted', '');
try {
await v.play();
v.pause();
v.currentTime = 0;
} catch {
// 靜音播放被擋下也無妨,掃到照片時會再嘗試播放
}
}
// 保險:AR 引擎就緒時把所有影片壓回暫停歸零,任何啟動期間的意外播放都會被停掉
sceneEl.addEventListener('arReady', () => {
for (const v of videos) {
if (!v.paused) v.pause();
v.currentTime = 0;
}
}, { once: true });
const start = () => {
const sys = sceneEl.systems['mindar-image-system'];
sys.start();
overlay.classList.add('hidden');
stopBtn.style.display = 'block';
};
if (sceneEl.hasLoaded) start();
else sceneEl.addEventListener('loaded', start, { once: true });
});
stopBtn.addEventListener('click', () => {
try { sceneEl.systems['mindar-image-system'].stop(); } catch {}
for (const v of videos) v.pause();
stopBtn.style.display = 'none';
overlay.classList.remove('hidden');
startBtn.disabled = false;
statusEl.textContent = '已停止,可再次開始掃描';
});
init();
</script>
</body>
</html>