<!DOCTYPE html> <html> <head> <title>メニュー画面</title> <style> body { margin: 0; overflow: hidden; background-color: #111; color: white; font-family: sans-serif; } #container { position: relative; width: 640px; height: 480px; margin: 20px auto; border: 4px solid #fff; } #video, #canvas { position: absolute; top: 0; left: 0; } #canvas { transform: scaleX(-1); } .zone { position: absolute; display: flex; align-items: center; justify-content: center; color: white; font-size: 16px; font-weight: bold; pointer-events: none; } #zoneRight { top: 180px; right: 0; width: 160px; height: 120px; background-color: rgba(255, 255, 0, 0.4); border-left: 2px dashed white; } #zoneLeft { top: 180px; left: 0; width: 160px; height: 120px; background-color: rgba(100, 255, 100, 0.4); border-right: 2px dashed white; } #zoneTop { top: 0; left: 240px; width: 160px; height: 80px; background-color: rgba(100, 100, 255, 0.4); border-bottom: 2px dashed white; } #status { position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); font-size: 20px; text-shadow: 2px 2px 4px black; } </style> </head> <body> <div id="container"> <video id="video" width="640" height="480" autoplay playsinline muted></video> <canvas id="canvas" width="640" height="480"></canvas> <div id="zoneRight" class="zone">▶ 水やりスタート</div> <div id="zoneLeft" class="zone">◀ 左のページ</div> <div id="zoneTop" class="zone">▲ 上のページ</div> <div id="status">右手首をゾーンに入れてください</div> </div> <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script> <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/posenet"></script> <script> (async function() { const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const status = document.getElementById('status'); const zoneRight = document.getElementById('zoneRight'); const zoneLeft = document.getElementById('zoneLeft'); const zoneTop = document.getElementById('zoneTop'); const urlRight = 'https://www.yatex.org/gitbucket/HiroseLabo./2025-Tsuji/pages/system/demo/demo.html'; const urlLeft = 'left.html'; const urlTop = 'top.html'; let enterTimeRight = null; let enterTimeLeft = null; let enterTimeTop = null; const waitTime = 2000; // 画像を読み込み const wristImage = new Image(); wristImage.src = 'hand_icon.png'; // 必要に応じて画像名を変更 const net = await posenet.load(); const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480, facingMode: 'user' } }); video.srcObject = stream; video.onloadedmetadata = () => { video.play(); const checkReady = setInterval(() => { if (video.readyState === 4) { clearInterval(checkReady); canvas.width = video.videoWidth; canvas.height = video.videoHeight; process(); } }, 100); }; function isHandInZone(x, y, zone) { const flippedX = canvas.width - x; const rect = zone.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect(); const zoneLeft = rect.left - canvasRect.left; const zoneRight = rect.right - canvasRect.left; const zoneTop = rect.top - canvasRect.top; const zoneBottom = rect.bottom - canvasRect.top; return flippedX >= zoneLeft && flippedX <= zoneRight && y >= zoneTop && y <= zoneBottom; } function drawWrist(x, y) { const size = 40; ctx.drawImage(wristImage, x - size / 2, y - size / 2, size, size); } function process() { net.estimateSinglePose(video, { flipHorizontal: false }).then(pose => { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const wrist = pose.keypoints.find(k => k.part === 'rightWrist'); if (wrist && wrist.score > 0.4) { drawWrist(wrist.position.x, wrist.position.y); const inRight = isHandInZone(wrist.position.x, wrist.position.y, zoneRight); const inLeft = isHandInZone(wrist.position.x, wrist.position.y, zoneLeft); const inTop = isHandInZone(wrist.position.x, wrist.position.y, zoneTop); if (inRight) { if (!enterTimeRight) enterTimeRight = Date.now(); if (Date.now() - enterTimeRight >= waitTime) { status.textContent = '水やりゲームへ移動します...'; window.location.href = urlRight; return; } else { const elapsed = ((Date.now() - enterTimeRight) / 1000).toFixed(1); status.textContent = `右ゾーン ${elapsed}s`; } } else { enterTimeRight = null; } if (inLeft) { if (!enterTimeLeft) enterTimeLeft = Date.now(); if (Date.now() - enterTimeLeft >= waitTime) { status.textContent = '左のページへ移動します...'; window.location.href = urlLeft; return; } else { const elapsed = ((Date.now() - enterTimeLeft) / 1000).toFixed(1); status.textContent = `左ゾーン ${elapsed}s`; } } else { enterTimeLeft = null; } if (inTop) { if (!enterTimeTop) enterTimeTop = Date.now(); if (Date.now() - enterTimeTop >= waitTime) { status.textContent = '上のページへ移動します...'; window.location.href = urlTop; return; } else { const elapsed = ((Date.now() - enterTimeTop) / 1000).toFixed(1); status.textContent = `上ゾーン ${elapsed}s`; } } else { enterTimeTop = null; } if (!inRight && !inLeft && !inTop) { status.textContent = '右手首をゾーンに入れてください'; } } else { status.textContent = '右手首を検出できません'; enterTimeRight = enterTimeLeft = enterTimeTop = null; } requestAnimationFrame(process); }); } })(); </script> </body> </html>