-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
127 lines (111 loc) · 8.38 KB
/
Copy pathapp.js
File metadata and controls
127 lines (111 loc) · 8.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
const { FilesetResolver, HandLandmarker } = Vision;
const canvas = document.querySelector('#gameCanvas');
const ctx = canvas.getContext('2d');
const video = document.querySelector('#camera');
const scoreElement = document.querySelector('#score');
const livesElement = document.querySelector('#lives');
const panel = document.querySelector('#startPanel');
const panelTitle = document.querySelector('#panelTitle');
const panelText = document.querySelector('#panelText');
const startButton = document.querySelector('#startButton');
const cameraStatus = document.querySelector('#cameraStatus');
const colorButtons = document.querySelector('#colorButtons');
const knifeColors = ['#ef3f3f', '#2388ff', '#2fc666', '#ffd028', '#ab62ff', '#ffffff'];
const fruitFiles = ['apple.png', 'armut.png', 'banana.png', 'cilek.png', 'karpuz.png', 'orange.png', 'pineapple.png'];
const fruitImages = fruitFiles.map(file => { const image = new Image(); image.src = `fruits/${file}`; return image; });
let knifeColorIndex = 0, fruits = [], particles = [], trail = [];
let score = 0, lives = 3, missed = 0, started = false, gameOver = false, spawnAt = 0, lastTime = 0;
let handLandmarker, cameraActive = false, cameraStarting = false, lastVideoTime = -1;
function setKnifeColor(index) {
knifeColorIndex = (index + knifeColors.length) % knifeColors.length;
trail = [];
[...colorButtons.children].forEach((button, buttonIndex) => button.classList.toggle('selected', buttonIndex === knifeColorIndex));
}
knifeColors.forEach((color, index) => {
const button = document.createElement('button');
button.type = 'button'; button.style.background = color; button.setAttribute('aria-label', `Warna pisau ${index + 1}`);
button.addEventListener('click', () => setKnifeColor(index)); colorButtons.append(button);
});
setKnifeColor(0);
async function startCamera() {
if (cameraActive || cameraStarting) return;
if (!navigator.mediaDevices?.getUserMedia || !window.isSecureContext) throw new Error('Camera requires HTTPS or localhost.');
cameraStarting = true; cameraStatus.textContent = 'Menyiapkan kamera dan pelacakan tangan…';
try {
const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@1.0.0/wasm');
handLandmarker = await HandLandmarker.createFromOptions(vision, {
baseOptions: { modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task', delegate: 'GPU' },
runningMode: 'VIDEO', numHands: 1, minHandDetectionConfidence: 0.6, minTrackingConfidence: 0.6,
});
video.srcObject = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false });
await video.play(); cameraActive = true; cameraStatus.textContent = 'Kamera aktif — arahkan ujung telunjuk ke buah.';
} finally { cameraStarting = false; }
}
function resetGame() {
fruits = []; particles = []; trail = []; score = 0; lives = 3; missed = 0; gameOver = false; started = true; spawnAt = performance.now() + 450;
scoreElement.textContent = score; livesElement.textContent = lives; panel.classList.add('hidden');
}
function spawnFruit() {
const radius = 42 + Math.random() * 13;
fruits.push({ x: 95 + Math.random() * (canvas.width - 190), y: canvas.height + radius, vx: -2.3 + Math.random() * 4.6, vy: -13 - Math.random() * 4, radius, image: fruitImages[Math.floor(Math.random() * fruitImages.length)], cut: false });
}
function sliceAt(point) {
if (!started || gameOver) return;
trail.push(point); if (trail.length > 16) trail.shift();
fruits.forEach(fruit => { if (!fruit.cut && Math.hypot(fruit.x - point.x, fruit.y - point.y) < fruit.radius + 16) cutFruit(fruit); });
}
function addPointerTrail(event) {
const box = canvas.getBoundingClientRect();
sliceAt({ x: (event.clientX - box.left) * canvas.width / box.width, y: (event.clientY - box.top) * canvas.height / box.height });
}
function cutFruit(fruit) {
fruit.cut = true; score += 1; missed = 0; scoreElement.textContent = score;
for (let index = 0; index < 16; index += 1) particles.push({ x: fruit.x, y: fruit.y, vx: -4 + Math.random() * 8, vy: -5 + Math.random() * 6, life: 1, color: knifeColors[knifeColorIndex] });
}
function drawCameraOrBackground() {
if (cameraActive && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
const scale = Math.max(canvas.width / video.videoWidth, canvas.height / video.videoHeight);
const width = video.videoWidth * scale, height = video.videoHeight * scale;
ctx.save(); ctx.scale(-1, 1); ctx.drawImage(video, -(canvas.width + (width - canvas.width) / 2), (canvas.height - height) / 2, width, height); ctx.restore();
ctx.fillStyle = '#10231242'; ctx.fillRect(0, 0, canvas.width, canvas.height);
trackHand(); return;
}
ctx.fillStyle = '#78976b'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#66885e';
for (let x = -80; x < canvas.width + 80; x += 130) { ctx.save(); ctx.translate(x, 0); ctx.rotate(-.34); ctx.fillRect(0, -100, 28, canvas.height + 200); ctx.restore(); }
}
function trackHand() {
if (!handLandmarker || video.currentTime === lastVideoTime) return;
lastVideoTime = video.currentTime;
const result = handLandmarker.detectForVideo(video, performance.now());
const finger = result.landmarks?.[0]?.[8];
if (!finger) { trail = []; return; }
sliceAt({ x: (1 - finger.x) * canvas.width, y: finger.y * canvas.height });
}
function drawFruit(fruit) {
ctx.save(); ctx.translate(fruit.x, fruit.y); ctx.rotate(fruit.vx * .05); ctx.fillStyle = '#0007'; ctx.beginPath(); ctx.ellipse(8, fruit.radius + 13, fruit.radius * .8, 7, 0, 0, Math.PI * 2); ctx.fill();
if (fruit.image.complete && fruit.image.naturalWidth) ctx.drawImage(fruit.image, -fruit.radius, -fruit.radius, fruit.radius * 2, fruit.radius * 2);
else { ctx.fillStyle = '#f04747'; ctx.beginPath(); ctx.arc(0, 0, fruit.radius, 0, Math.PI * 2); ctx.fill(); }
ctx.restore();
}
function drawTrail() {
if (trail.length < 2) return;
ctx.save(); ctx.strokeStyle = knifeColors[knifeColorIndex]; ctx.shadowColor = knifeColors[knifeColorIndex]; ctx.shadowBlur = 15; ctx.lineCap = 'round';
for (let index = 1; index < trail.length; index += 1) { ctx.globalAlpha = index / trail.length; ctx.lineWidth = 3 + index * .8; ctx.beginPath(); ctx.moveTo(trail[index - 1].x, trail[index - 1].y); ctx.lineTo(trail[index].x, trail[index].y); ctx.stroke(); }
ctx.restore();
}
function finishGame() { gameOver = true; panelTitle.textContent = `Game over — skor kamu ${score}`; panelText.textContent = 'Tekan tombol di bawah untuk bermain lagi dan pecahkan rekor.'; startButton.textContent = 'MAIN LAGI'; panel.classList.remove('hidden'); }
function animate(time) {
const delta = Math.min((time - lastTime) / 16.67 || 1, 2); lastTime = time; drawCameraOrBackground();
if (started && !gameOver) {
if (time >= spawnAt) { spawnFruit(); spawnAt = time + 620 + Math.random() * 360; }
fruits.forEach(fruit => { fruit.x += fruit.vx * delta; fruit.y += fruit.vy * delta; fruit.vy += .25 * delta; });
fruits.filter(fruit => fruit.y - fruit.radius > canvas.height).forEach(fruit => { if (!fruit.cut) { missed += 1; if (missed >= 3) { lives = Math.max(0, lives - 1); missed = 0; livesElement.textContent = lives; } } });
fruits = fruits.filter(fruit => fruit.y - fruit.radius <= canvas.height && !fruit.cut); if (lives === 0) finishGame();
}
particles.forEach(particle => { particle.x += particle.vx * delta; particle.y += particle.vy * delta; particle.vy += .24 * delta; particle.life -= .035 * delta; }); particles = particles.filter(particle => particle.life > 0);
fruits.forEach(drawFruit); particles.forEach(particle => { ctx.globalAlpha = particle.life; ctx.fillStyle = particle.color; ctx.fillRect(particle.x, particle.y, 5, 5); }); ctx.globalAlpha = 1; drawTrail(); requestAnimationFrame(animate);
}
canvas.addEventListener('pointermove', addPointerTrail); canvas.addEventListener('pointerdown', addPointerTrail); canvas.addEventListener('pointerleave', () => { trail = []; });
startButton.addEventListener('click', async () => { resetGame(); try { await startCamera(); } catch (error) { cameraStatus.textContent = 'Kamera tidak tersedia. Kamu tetap bisa bermain dengan mouse atau sentuhan.'; console.warn(error); } });
window.addEventListener('keydown', event => { if (event.key.toLowerCase() === 'c') setKnifeColor(knifeColorIndex + 1); if (event.key.toLowerCase() === 'r') resetGame(); });
requestAnimationFrame(animate);