Skip to content

Commit 0dd81fd

Browse files
authored
Update index.html
1 parent 068383d commit 0dd81fd

File tree

1 file changed

+155
-126
lines changed

1 file changed

+155
-126
lines changed

index.html

Lines changed: 155 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -3,88 +3,113 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6-
<title>Lax Wall Ball Counter</title>
6+
<title>Lacrosse Wall Ball Tracker - Arm Movement</title>
7+
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap" rel="stylesheet">
78
<style>
8-
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
9-
109
body {
11-
font-family: 'Roboto', sans-serif;
10+
font-family: 'Roboto Mono', monospace;
11+
background-color: #121212;
12+
color: #e0e0e0;
1213
display: flex;
1314
flex-direction: column;
1415
align-items: center;
1516
justify-content: center;
1617
height: 100vh;
1718
margin: 0;
18-
background: #1a1a1a;
19-
color: #fff;
19+
overflow: hidden;
2020
}
21+
2122
#container {
2223
position: relative;
2324
width: 90vw;
24-
max-width: 640px;
25-
border-radius: 16px;
25+
max-width: 800px;
26+
aspect-ratio: 16 / 9;
27+
border-radius: 15px;
2628
overflow: hidden;
27-
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
29+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
30+
background: #000;
2831
}
29-
canvas {
30-
display: block;
32+
33+
video, canvas {
34+
position: absolute;
35+
top: 0;
36+
left: 0;
3137
width: 100%;
32-
height: auto;
38+
height: 100%;
39+
object-fit: cover;
40+
transform: scaleX(-1);
3341
}
34-
.overlay-ui {
42+
43+
#ui-container {
3544
position: absolute;
36-
top: 20px;
37-
left: 20px;
38-
right: 20px;
45+
top: 0;
46+
left: 0;
47+
width: 100%;
48+
height: 100%;
3949
display: flex;
50+
flex-direction: column;
4051
justify-content: space-between;
41-
align-items: flex-start;
52+
align-items: center;
53+
padding: 20px;
54+
box-sizing: border-box;
55+
}
56+
57+
#counter-container {
58+
background: rgba(0, 0, 0, 0.6);
59+
backdrop-filter: blur(10px);
60+
padding: 10px 25px;
61+
border-radius: 50px;
62+
border: 1px solid rgba(255, 255, 255, 0.1);
63+
text-align: center;
64+
}
65+
66+
#counter-container h1 {
67+
margin: 0;
68+
font-size: 1.2em;
69+
color: #ff416c;
70+
text-transform: uppercase;
71+
letter-spacing: 2px;
4272
}
73+
4374
#counter {
4475
font-size: 3em;
4576
font-weight: 700;
46-
padding: 10px 20px;
47-
background: rgba(0, 0, 0, 0.3);
48-
border-radius: 12px;
49-
backdrop-filter: blur(10px);
50-
-webkit-backdrop-filter: blur(10px);
51-
border: 1px solid rgba(255, 255, 255, 0.1);
52-
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
77+
color: #fff;
78+
margin: 0;
5379
}
54-
#instructions {
55-
font-size: 1em;
56-
padding: 10px 15px;
57-
background: rgba(0, 0, 0, 0.3);
58-
border-radius: 12px;
80+
81+
#controls {
82+
background: rgba(0, 0, 0, 0.6);
5983
backdrop-filter: blur(10px);
60-
-webkit-backdrop-filter: blur(10px);
84+
padding: 20px;
85+
border-radius: 15px;
6186
border: 1px solid rgba(255, 255, 255, 0.1);
62-
max-width: 200px;
63-
}
64-
#controls {
65-
position: absolute;
66-
bottom: 20px;
67-
left: 50%;
68-
transform: translateX(-50%);
69-
display: flex;
70-
gap: 10px;
87+
text-align: center;
88+
max-width: 300px;
7189
}
72-
.control-button {
90+
91+
#startButton {
92+
background: linear-gradient(to right, #ff416c, #ff4b2b);
93+
color: white;
94+
border: none;
7395
padding: 15px 30px;
7496
font-size: 1.2em;
75-
font-weight: 700;
97+
font-family: 'Roboto Mono', monospace;
98+
border-radius: 10px;
7699
cursor: pointer;
77-
border: none;
78-
border-radius: 12px;
79-
color: #fff;
80-
background: linear-gradient(45deg, #ff416c, #ff4b2b);
81-
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
82-
transition: all 0.3s ease;
100+
transition: transform 0.2s, box-shadow 0.2s;
101+
}
102+
103+
#startButton:disabled {
104+
background: #555;
105+
cursor: not-allowed;
83106
}
84-
.control-button:hover {
85-
transform: translateY(-2px);
86-
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
107+
108+
#startButton:not(:disabled):hover {
109+
transform: translateY(-3px);
110+
box-shadow: 0 5px 15px rgba(255, 65, 108, 0.4);
87111
}
112+
88113
#loader {
89114
position: absolute;
90115
top: 50%;
@@ -96,35 +121,45 @@
96121
width: 60px;
97122
height: 60px;
98123
animation: spin 1s linear infinite;
99-
display: none; /* Initially hidden */
124+
z-index: 10;
100125
}
126+
101127
@keyframes spin {
102128
0% { transform: translate(-50%, -50%) rotate(0deg); }
103129
100% { transform: translate(-50%, -50%) rotate(360deg); }
104130
}
131+
132+
#instructions p {
133+
margin: 5px 0;
134+
}
105135
</style>
106-
<!-- TensorFlow.js -->
107-
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.11.0/dist/tf.min.js"></script>
108-
<!-- COCO-SSD model -->
109-
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd@2.2.2/dist/coco-ssd.min.js"></script>
110136
</head>
111137
<body>
138+
112139
<div id="container">
113-
<video id="video" width="640" height="480" autoplay muted playsinline style="display:none;"></video>
140+
<video id="video" width="640" height="480" autoplay muted playsinline></video>
114141
<canvas id="canvas" width="640" height="480"></canvas>
115142
<div id="loader"></div>
116-
<div class="overlay-ui">
117-
<div id="counter">0</div>
118-
<div id="instructions">
119-
<p><strong>Wall Ball Tracker</strong></p>
120-
<p>Aim your camera at the wall. The app will count reps when the ball hits the designated wall zone.</p>
143+
<div id="ui-container">
144+
<div id="counter-container">
145+
<h1>Reps</h1>
146+
<p id="counter">0</p>
147+
</div>
148+
<div id="controls">
149+
<div id="instructions"></div>
150+
<button id="startButton" disabled>Loading Model...</button>
121151
</div>
122-
</div>
123-
<div id="controls">
124-
<button id="startButton" class="control-button">Start</button>
125152
</div>
126153
</div>
127154

155+
<!-- TensorFlow.js Core -->
156+
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core@3.11.0/dist/tf-core.min.js"></script>
157+
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-converter@3.11.0/dist/tf-converter.min.js"></script>
158+
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgl@3.11.0/dist/tf-backend-webgl.min.js"></script>
159+
160+
<!-- Pose Detection model -->
161+
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/pose-detection@2.0.0/dist/pose-detection.min.js"></script>
162+
128163
<script>
129164
const video = document.getElementById('video');
130165
const canvas = document.getElementById('canvas');
@@ -133,23 +168,24 @@
133168
const counterElement = document.getElementById('counter');
134169
const loader = document.getElementById('loader');
135170
const controls = document.getElementById('controls');
171+
const instructions = document.getElementById('instructions');
136172

137-
let model = null;
173+
let detector = null;
138174
let repCount = 0;
139-
let ballState = { x: 0, y: 0, detected: false, lastDetectionTime: 0 };
140-
let lastWallHitTime = 0;
175+
let armState = 'down'; // 'down' or 'up'
141176
let isDetecting = false;
142177

143178
// --- Main Setup ---
144179
async function setup() {
145180
loader.style.display = 'block';
146-
await cocoSsd.load().then(loadedModel => {
147-
model = loadedModel;
148-
console.log('Object detection model loaded.');
149-
loader.style.display = 'none';
150-
startButton.disabled = false;
151-
startButton.textContent = 'Start Camera';
152-
});
181+
instructions.innerHTML = '<p><strong>Loading Model...</strong></p><p>Please wait, this may take a moment.</p>';
182+
const detectorConfig = {modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING};
183+
detector = await poseDetection.createDetector(poseDetection.SupportedModels.MoveNet, detectorConfig);
184+
console.log('Pose detection model loaded.');
185+
loader.style.display = 'none';
186+
startButton.disabled = false;
187+
startButton.textContent = 'Start Camera';
188+
instructions.innerHTML = '<p><strong>Arm Movement Tracker</strong></p><p>Press Start and position yourself in the frame. Reps are counted when you raise and lower your arm.</p>';
153189

154190
startButton.addEventListener('click', async () => {
155191
if (!isDetecting) {
@@ -167,7 +203,7 @@
167203

168204
async function setupCamera() {
169205
const stream = await navigator.mediaDevices.getUserMedia({
170-
video: { facingMode: 'environment' }, // Use 'user' for front camera
206+
video: { facingMode: 'user' }, // Use 'environment' for back camera
171207
audio: false
172208
});
173209
video.srcObject = stream;
@@ -179,76 +215,69 @@
179215
}
180216

181217
async function detectFrame() {
182-
if (!model) {
218+
if (!detector) {
183219
requestAnimationFrame(detectFrame);
184220
return;
185221
}
186222

187-
const predictions = await model.detect(video);
188-
223+
const poses = await detector.estimatePoses(video);
189224
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
190225

191-
const ball = predictions.find(p => p.class === 'sports ball');
192-
193-
if (ball) {
194-
// Draw bounding box
195-
ctx.strokeStyle = 'red';
196-
ctx.lineWidth = 2;
197-
ctx.strokeRect(ball.bbox[0], ball.bbox[1], ball.bbox[2], ball.bbox[3]);
198-
ctx.fillStyle = 'red';
199-
ctx.fillText(`${ball.class} (${Math.round(ball.score * 100)}%)`, ball.bbox[0], ball.bbox[1] > 10 ? ball.bbox[1] - 5 : 10);
200-
201-
const ballCenterX = ball.bbox[0] + ball.bbox[2] / 2;
202-
const ballCenterY = ball.bbox[1] + ball.bbox[3] / 2;
203-
204-
updateBallState(ballCenterX, ballCenterY);
205-
checkForRep();
206-
} else {
207-
ballState.detected = false;
226+
if (poses && poses.length > 0) {
227+
const keypoints = poses[0].keypoints;
228+
drawPose(keypoints);
229+
checkForRep(keypoints);
208230
}
209231

210232
requestAnimationFrame(detectFrame);
211233
}
212234

213-
function updateBallState(x, y) {
214-
ballState.x = x;
215-
ballState.y = y;
216-
ballState.detected = true;
217-
ballState.lastDetectionTime = Date.now();
235+
function drawPose(keypoints) {
236+
// Draw keypoints
237+
for (const keypoint of keypoints) {
238+
if (keypoint.score > 0.5) {
239+
ctx.fillStyle = '#ff416c';
240+
ctx.beginPath();
241+
ctx.arc(keypoint.x, keypoint.y, 5, 0, 2 * Math.PI);
242+
ctx.fill();
243+
}
244+
}
245+
// Draw skeleton
246+
const adjacentPairs = poseDetection.util.getAdjacentPairs(poseDetection.SupportedModels.MoveNet);
247+
ctx.strokeStyle = '#fff';
248+
ctx.lineWidth = 2;
249+
for (const [i, j] of adjacentPairs) {
250+
const kp1 = keypoints[i];
251+
const kp2 = keypoints[j];
252+
if (kp1.score > 0.5 && kp2.score > 0.5) {
253+
ctx.beginPath();
254+
ctx.moveTo(kp1.x, kp1.y);
255+
ctx.lineTo(kp2.x, kp2.y);
256+
ctx.stroke();
257+
}
258+
}
218259
}
219260

220-
function checkForRep() {
221-
const now = Date.now();
222-
// Define a "wall zone" as the far side of the screen (e.g., right 20%)
223-
const wallZoneStart = canvas.width * 0.8;
261+
function checkForRep(keypoints) {
262+
const rightShoulder = keypoints.find(k => k.name === 'right_shoulder');
263+
const rightWrist = keypoints.find(k => k.name === 'right_wrist');
224264

225-
// Cooldown period to prevent multiple counts for one hit
226-
const cooldown = 1000; // 1 second
265+
if (rightShoulder && rightWrist && rightShoulder.score > 0.5 && rightWrist.score > 0.5) {
266+
// Simple bicep curl logic: check if wrist is above shoulder
267+
if (armState === 'down' && rightWrist.y < rightShoulder.y) {
268+
armState = 'up';
269+
}
227270

228-
if (ballState.x > wallZoneStart && now - lastWallHitTime > cooldown) {
229-
repCount++;
230-
counterElement.textContent = repCount;
231-
lastWallHitTime = now;
232-
console.log(`Rep counted! Total: ${repCount}`);
271+
if (armState === 'up' && rightWrist.y > rightShoulder.y) {
272+
repCount++;
273+
counterElement.textContent = repCount;
274+
armState = 'down';
275+
console.log(`Rep counted! Total: ${repCount}`);
276+
}
233277
}
234278
}
235279

236-
// --- Suggestions for improving accuracy ---
237-
/*
238-
1. Custom Model: Train a model specifically on lacrosse balls for better detection.
239-
2. Pose Detection: Use a model like MoveNet or PoseNet to track the player's body.
240-
A 'catch' could be registered when the ball's position is near the player's hands.
241-
3. Trajectory Analysis: Instead of simple zones, track the ball's path over several frames.
242-
A rep could be a sequence of: throw -> hit (sudden change in direction) -> catch.
243-
This is more complex but much more robust.
244-
4. User-defined Zones: Allow the user to draw a rectangle on the screen to define the 'wall' area.
245-
This makes the app adaptable to different environments.
246-
5. Kalman Filter: Use a Kalman filter to smooth the ball's detected position and predict its location
247-
if the model temporarily loses track of it. This helps bridge gaps in detection.
248-
*/
249-
250280
setup();
251281
</script>
252-
253282
</body>
254283
</html>

0 commit comments

Comments
 (0)