add camera controll
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "svelte-app",
|
||||
"name": "nubzuki-jump",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "svelte-app",
|
||||
"name": "nubzuki-jump",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@mediapipe/tasks-vision": "^0.10.35",
|
||||
"p5": "1.11.4",
|
||||
"p5-svelte": "^3.1.2",
|
||||
"p5play": "^3.8.14",
|
||||
@@ -85,6 +86,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mediapipe/tasks-vision": {
|
||||
"version": "0.10.35",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.35.tgz",
|
||||
"integrity": "sha512-HOvadwVRE6JC+45nyYhmnywnr5h/J8KZvOeUNVOG9q/0875pZgItznFB9bRTvLc264YSJqiZ1NsIpCStJw/egg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "svelte-app",
|
||||
"name": "nubzuki-jump",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -19,6 +19,7 @@
|
||||
"svelte": "^5.55.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mediapipe/tasks-vision": "^0.10.35",
|
||||
"p5": "1.11.4",
|
||||
"p5-svelte": "^3.1.2",
|
||||
"p5play": "^3.8.14",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script>
|
||||
import CameraControl from "./CameraControl.svelte";
|
||||
import {
|
||||
initializeGame,
|
||||
updateGame,
|
||||
getScore,
|
||||
isGameOver,
|
||||
resetGame
|
||||
} from './game/game.js';
|
||||
resetGame,
|
||||
} from "./game/game.js";
|
||||
|
||||
let game;
|
||||
let score = $state(0);
|
||||
let highScore = $state(0);
|
||||
let state = $state('start'); // start | playing | gameover
|
||||
|
||||
let state = $state("start"); // start | playing | gameover
|
||||
|
||||
window.setup = () => {
|
||||
createCanvas(400, 800).parent(game);
|
||||
@@ -24,7 +24,7 @@
|
||||
score = Math.floor(getScore());
|
||||
|
||||
if (isGameOver()) {
|
||||
state = 'gameover';
|
||||
state = "gameover";
|
||||
if (score > highScore) {
|
||||
highScore = score;
|
||||
}
|
||||
@@ -35,7 +35,7 @@
|
||||
function playGame() {
|
||||
resetGame();
|
||||
score = 0;
|
||||
state = 'playing';
|
||||
state = "playing";
|
||||
loop();
|
||||
}
|
||||
|
||||
@@ -56,18 +56,22 @@
|
||||
<div class="game-wrapper">
|
||||
<div bind:this={game}></div>
|
||||
|
||||
{#if state !== 'playing'}
|
||||
{#if state !== "playing"}
|
||||
<div class="overlay">
|
||||
<div class="overlay-card">
|
||||
<h2>{state === 'gameover' ? 'Game over!' : 'Start new game'}</h2>
|
||||
{#if state !== "start"} <p>Score: {score}</p> {/if }
|
||||
<h2>{state === "gameover" ? "Game over!" : "Start new game"}</h2>
|
||||
{#if state !== "start"}
|
||||
<p>Score: {score}</p>
|
||||
{/if}
|
||||
<button onclick={playGame}>Play</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<aside class="right-panel">
|
||||
<CameraControl />
|
||||
</aside>
|
||||
|
||||
<aside class="right-panel"></aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -155,4 +159,4 @@
|
||||
.right-panel {
|
||||
width: 220px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
147
src/CameraControl.svelte
Normal file
147
src/CameraControl.svelte
Normal file
@@ -0,0 +1,147 @@
|
||||
<script>
|
||||
import {
|
||||
initCameraControl,
|
||||
updateCameraControl,
|
||||
cameraInput,
|
||||
} from "./game/cameraControl.js";
|
||||
|
||||
let videoContainer;
|
||||
let enabled = $state(false);
|
||||
let loading = $state(false);
|
||||
let zone = $state("center");
|
||||
let error = $state("");
|
||||
|
||||
async function enableCamera() {
|
||||
try {
|
||||
loading = true;
|
||||
error = "";
|
||||
|
||||
const video = await initCameraControl();
|
||||
|
||||
videoContainer.innerHTML = "";
|
||||
videoContainer.appendChild(video);
|
||||
|
||||
await video.play();
|
||||
|
||||
enabled = true;
|
||||
loading = false;
|
||||
|
||||
requestAnimationFrame(updateLoop);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = "Camera could not be started";
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLoop() {
|
||||
if (!enabled) return;
|
||||
|
||||
await updateCameraControl();
|
||||
zone = cameraInput.zone;
|
||||
|
||||
requestAnimationFrame(updateLoop);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="camera-card">
|
||||
<h2>Camera Control</h2>
|
||||
<p>
|
||||
Enable camera and move your head to control the game, or use the arrows.
|
||||
</p>
|
||||
|
||||
<div class="camera-view">
|
||||
<div class="video-holder" bind:this={videoContainer}></div>
|
||||
|
||||
{#if !enabled}
|
||||
<button onclick={enableCamera} disabled={loading}>
|
||||
{loading ? "Loading..." : "Enable camera"}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="zones">
|
||||
<div class:active={zone === "left"}>GO LEFT</div>
|
||||
<div class:active={zone === "center"}>STAY</div>
|
||||
<div class:active={zone === "right"}>GO RIGHT</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="error">{error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.camera-card h2 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.camera-card {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
color: white;
|
||||
width: 320px;
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
.camera-view {
|
||||
position: relative;
|
||||
width: 320px;
|
||||
height: 240px;
|
||||
background: #111;
|
||||
border-radius: 16px;
|
||||
margin-top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-holder :global(video) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-holder :global(video) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
inset: 50% auto auto 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 5;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.zones {
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.zones div {
|
||||
padding: 6px 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.zones .active {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ffb3b3;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
90
src/game/cameraControl.js
Normal file
90
src/game/cameraControl.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { FaceDetector, FilesetResolver } from '@mediapipe/tasks-vision';
|
||||
|
||||
export const cameraInput = {
|
||||
left: false,
|
||||
right: false,
|
||||
active: false,
|
||||
zone: 'center',
|
||||
x: 0.5
|
||||
};
|
||||
|
||||
let video;
|
||||
let faceDetector;
|
||||
let smoothedX = 0.5;
|
||||
|
||||
// 🔥 TWEAK THESE FOR FEEL:
|
||||
const SMOOTHING_FACTOR = 0.6; // Higher = more responsive/less lag (try 0.5 - 0.8)
|
||||
const LEFT_THRESHOLD = 0.45; // Closer to 0.5 = smaller center zone
|
||||
const RIGHT_THRESHOLD = 0.55; // Closer to 0.5 = smaller center zone
|
||||
|
||||
export async function initCameraControl() {
|
||||
if (cameraInput.active) return video;
|
||||
|
||||
video = document.createElement('video');
|
||||
video.autoplay = true;
|
||||
video.playsInline = true;
|
||||
video.muted = true;
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 160, height: 120, frameRate: 15 }
|
||||
});
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
const vision = await FilesetResolver.forVisionTasks(
|
||||
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'
|
||||
);
|
||||
|
||||
faceDetector = await FaceDetector.createFromOptions(vision, {
|
||||
baseOptions: {
|
||||
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.task`,
|
||||
delegate: 'GPU'
|
||||
},
|
||||
runningMode: 'VIDEO'
|
||||
});
|
||||
|
||||
video.addEventListener('loadeddata', () => {
|
||||
cameraInput.active = true;
|
||||
startDetectionLoop();
|
||||
});
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
function startDetectionLoop() {
|
||||
const detect = (now) => {
|
||||
const result = faceDetector.detectForVideo(video, now);
|
||||
|
||||
if (result.detections.length > 0) {
|
||||
const box = result.detections[0].boundingBox;
|
||||
const centerX = box.originX + box.width / 2;
|
||||
const normalizedX = centerX / 160;
|
||||
|
||||
// Snappier smoothing
|
||||
smoothedX += (normalizedX - smoothedX) * SMOOTHING_FACTOR;
|
||||
cameraInput.x = smoothedX;
|
||||
|
||||
// Updated Logic with smaller center zone
|
||||
if (smoothedX < LEFT_THRESHOLD) {
|
||||
updateZones(false, true, 'right'); // Mirrored
|
||||
} else if (smoothedX > RIGHT_THRESHOLD) {
|
||||
updateZones(true, false, 'left'); // Mirrored
|
||||
} else {
|
||||
updateZones(false, false, 'center');
|
||||
}
|
||||
} else {
|
||||
// Snap to center if face is lost
|
||||
updateZones(false, false, 'center');
|
||||
}
|
||||
|
||||
video.requestVideoFrameCallback(detect);
|
||||
};
|
||||
|
||||
video.requestVideoFrameCallback(detect);
|
||||
}
|
||||
|
||||
function updateZones(l, r, z) {
|
||||
cameraInput.left = l;
|
||||
cameraInput.right = r;
|
||||
cameraInput.zone = z;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { PLAT_TYPE } from './constants.js';
|
||||
import { cameraInput } from './cameraControl.js';
|
||||
|
||||
|
||||
export function createPlayer() {
|
||||
const player = new Sprite();
|
||||
@@ -44,13 +46,15 @@ export function updatePlayerPosition(player, platforms) {
|
||||
|
||||
// Controls
|
||||
player.vel.x = 0;
|
||||
if (keyIsDown(LEFT_ARROW)) {
|
||||
if (keyIsDown(LEFT_ARROW) || cameraInput.left) {
|
||||
player.vel.x = -5;
|
||||
}
|
||||
if (keyIsDown(RIGHT_ARROW)) {
|
||||
} else if (keyIsDown(RIGHT_ARROW) || cameraInput.right) {
|
||||
player.vel.x = 5;
|
||||
} else {
|
||||
player.vel.x *= 0.5;
|
||||
}
|
||||
|
||||
|
||||
// Wrap horizontally
|
||||
if (player.x > width) {
|
||||
player.x = 0;
|
||||
|
||||
Reference in New Issue
Block a user