Compare commits
2 Commits
bb24d5df44
...
camera-con
| Author | SHA1 | Date | |
|---|---|---|---|
| 708fa4e9ab | |||
| b13342f47e |
11
package-lock.json
generated
11
package-lock.json
generated
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "svelte-app",
|
"name": "nubzuki-jump",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "svelte-app",
|
"name": "nubzuki-jump",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mediapipe/tasks-vision": "^0.10.35",
|
||||||
"p5": "1.11.4",
|
"p5": "1.11.4",
|
||||||
"p5-svelte": "^3.1.2",
|
"p5-svelte": "^3.1.2",
|
||||||
"p5play": "^3.8.14",
|
"p5play": "^3.8.14",
|
||||||
@@ -85,6 +86,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
"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",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"svelte": "^5.55.5"
|
"svelte": "^5.55.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mediapipe/tasks-vision": "^0.10.35",
|
||||||
"p5": "1.11.4",
|
"p5": "1.11.4",
|
||||||
"p5-svelte": "^3.1.2",
|
"p5-svelte": "^3.1.2",
|
||||||
"p5play": "^3.8.14",
|
"p5play": "^3.8.14",
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import CameraControl from "./CameraControl.svelte";
|
||||||
import {
|
import {
|
||||||
initializeGame,
|
initializeGame,
|
||||||
updateGame,
|
updateGame,
|
||||||
getScore,
|
getScore,
|
||||||
isGameOver,
|
isGameOver,
|
||||||
resetGame
|
resetGame,
|
||||||
} from './game/game.js';
|
} from "./game/game.js";
|
||||||
|
|
||||||
let game;
|
let game;
|
||||||
let score = $state(0);
|
let score = $state(0);
|
||||||
let highScore = $state(0);
|
let highScore = $state(0);
|
||||||
let state = $state('start'); // start | playing | gameover
|
let state = $state("start"); // start | playing | gameover
|
||||||
|
|
||||||
|
|
||||||
window.setup = () => {
|
window.setup = () => {
|
||||||
createCanvas(400, 800).parent(game);
|
createCanvas(400, 800).parent(game);
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
score = Math.floor(getScore());
|
score = Math.floor(getScore());
|
||||||
|
|
||||||
if (isGameOver()) {
|
if (isGameOver()) {
|
||||||
state = 'gameover';
|
state = "gameover";
|
||||||
if (score > highScore) {
|
if (score > highScore) {
|
||||||
highScore = score;
|
highScore = score;
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
function playGame() {
|
function playGame() {
|
||||||
resetGame();
|
resetGame();
|
||||||
score = 0;
|
score = 0;
|
||||||
state = 'playing';
|
state = "playing";
|
||||||
loop();
|
loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,18 +56,22 @@
|
|||||||
<div class="game-wrapper">
|
<div class="game-wrapper">
|
||||||
<div bind:this={game}></div>
|
<div bind:this={game}></div>
|
||||||
|
|
||||||
{#if state !== 'playing'}
|
{#if state !== "playing"}
|
||||||
<div class="overlay">
|
<div class="overlay">
|
||||||
<div class="overlay-card">
|
<div class="overlay-card">
|
||||||
<h2>{state === 'gameover' ? 'Game over!' : 'Start new game'}</h2>
|
<h2>{state === "gameover" ? "Game over!" : "Start new game"}</h2>
|
||||||
{#if state !== "start"} <p>Score: {score}</p> {/if }
|
{#if state !== "start"}
|
||||||
|
<p>Score: {score}</p>
|
||||||
|
{/if}
|
||||||
<button onclick={playGame}>Play</button>
|
<button onclick={playGame}>Play</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<aside class="right-panel">
|
||||||
|
<CameraControl />
|
||||||
|
</aside>
|
||||||
|
|
||||||
<aside class="right-panel"></aside>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
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>
|
||||||
91
src/game/cameraControl.js
Normal file
91
src/game/cameraControl.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
// 🔥 NARROW CENTER ZONE:
|
||||||
|
// 0.48 and 0.52 means the center is only 4% of the screen width.
|
||||||
|
// Adjust these if it's still too hard to trigger movement.
|
||||||
|
const LEFT_THRESHOLD = 0.48;
|
||||||
|
const RIGHT_THRESHOLD = 0.52;
|
||||||
|
|
||||||
|
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: { ideal: 20 } // Slightly higher for faster reaction
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
// Calculate raw center point (0 to 1)
|
||||||
|
const rawX = (box.originX + box.width / 2) / 160;
|
||||||
|
|
||||||
|
cameraInput.x = rawX;
|
||||||
|
|
||||||
|
// INSTANT LOGIC (No smoothing)
|
||||||
|
if (rawX < LEFT_THRESHOLD) {
|
||||||
|
updateZones(false, true, 'right'); // Mirrored: Face on left of cam = move right
|
||||||
|
} else if (rawX > RIGHT_THRESHOLD) {
|
||||||
|
updateZones(true, false, 'left'); // Mirrored: Face on right of cam = move left
|
||||||
|
} else {
|
||||||
|
updateZones(false, false, 'center');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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 { PLAT_TYPE } from './constants.js';
|
||||||
|
import { cameraInput } from './cameraControl.js';
|
||||||
|
|
||||||
|
|
||||||
export function createPlayer() {
|
export function createPlayer() {
|
||||||
const player = new Sprite();
|
const player = new Sprite();
|
||||||
@@ -44,13 +46,15 @@ export function updatePlayerPosition(player, platforms) {
|
|||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
player.vel.x = 0;
|
player.vel.x = 0;
|
||||||
if (keyIsDown(LEFT_ARROW)) {
|
if (keyIsDown(LEFT_ARROW) || cameraInput.left) {
|
||||||
player.vel.x = -5;
|
player.vel.x = -5;
|
||||||
}
|
} else if (keyIsDown(RIGHT_ARROW) || cameraInput.right) {
|
||||||
if (keyIsDown(RIGHT_ARROW)) {
|
|
||||||
player.vel.x = 5;
|
player.vel.x = 5;
|
||||||
|
} else {
|
||||||
|
player.vel.x *= 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Wrap horizontally
|
// Wrap horizontally
|
||||||
if (player.x > width) {
|
if (player.x > width) {
|
||||||
player.x = 0;
|
player.x = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user