44 < meta charset ="UTF-8 " />
55 < meta name ="viewport " content ="width=device-width, initial-scale=1 " />
66 < title > Wall Ball Rep Tracker</ title >
7+ < script src ="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core "> </ script >
8+ < script src ="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-converter "> </ script >
9+ < script src ="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgl "> </ script >
10+ < script src ="https://cdn.jsdelivr.net/npm/@tensorflow-models/pose-detection "> </ script >
711 < style >
812 body {
913 background : # 121212 ;
6872< body >
6973 < h1 > Wall Ball Rep Tracker (Chrome)</ h1 >
7074 < div id ="controls-container " style ="margin-bottom: 1rem; ">
71- < label for ="stick-color " style ="margin-right: 0.5rem; "> Stick Color :</ label >
72- < select id ="stick-color " style ="padding: 0.5rem; background: #333; color: white; border: 1px solid #555; border-radius: 5px; ">
73- < option value ="black " > Black </ option >
74- < option value ="white " > White </ option >
75+ < label for ="hand-select " style ="margin-right: 0.5rem; "> Tracked Hand :</ label >
76+ < select id ="hand-select " style ="padding: 0.5rem; background: #333; color: white; border: 1px solid #555; border-radius: 5px; ">
77+ < option value ="right " > Right </ option >
78+ < option value ="left " > Left </ option >
7579 </ select >
76- < button id ="view-toggle " style ="margin-left: 1rem; padding: 0.5rem; background: #333; color: white; border: 1px solid #555; border-radius: 5px; cursor: pointer; "> Toggle Lines</ button >
7780 </ div >
7881 < div id ="reps-container ">
7982 < div id ="reps "> Reps: 0</ div >
@@ -88,11 +91,8 @@ <h1>Wall Ball Rep Tracker (Chrome)</h1>
8891 < script >
8992 async function setupCamera ( ) {
9093 const video = document . getElementById ( 'video' ) ;
91-
92- // Check if user has previously granted camera access
9394 const cameraAccessGranted = localStorage . getItem ( 'wallBallCameraAccess' ) === 'granted' ;
9495
95- // Show camera permission request message
9696 document . getElementById ( 'reps' ) . textContent = 'Please allow camera access...' ;
9797 document . getElementById ( 'camera-status' ) . style . display = 'block' ;
9898
@@ -111,34 +111,21 @@ <h1>Wall Ball Rep Tracker (Chrome)</h1>
111111 }
112112 } ) ;
113113 video . srcObject = stream ;
114-
115- // Save camera access permission
116114 localStorage . setItem ( 'wallBallCameraAccess' , 'granted' ) ;
117-
118- // Hide camera permission message once access is granted
119115 document . getElementById ( 'camera-status' ) . style . display = 'none' ;
120116
121- // Wait for video to be fully loaded
122117 await new Promise ( ( resolve ) => {
123118 video . onloadedmetadata = ( ) => {
124119 video . width = video . videoWidth ;
125120 video . height = video . videoHeight ;
126- console . log ( 'Video metadata loaded:' , {
127- width : video . width ,
128- height : video . height ,
129- videoWidth : video . videoWidth ,
130- videoHeight : video . videoHeight
131- } ) ;
132121 resolve ( ) ;
133122 } ;
134123 } ) ;
135124
136125 await video . play ( ) ;
137126 return video ;
138127 } catch ( err ) {
139- // Save that camera access was denied
140128 localStorage . setItem ( 'wallBallCameraAccess' , 'denied' ) ;
141-
142129 document . getElementById ( 'camera-status' ) . innerHTML =
143130 'Camera access denied or unavailable. <button onclick="requestCameraAgain()" class="retry-button">Try Again</button>' ;
144131 console . error ( 'Camera error:' , err . message ) ;
@@ -153,127 +140,79 @@ <h1>Wall Ball Rep Tracker (Chrome)</h1>
153140 } ) ;
154141 }
155142
143+ function drawPoint ( ctx , y , x , r , color ) {
144+ ctx . beginPath ( ) ;
145+ ctx . arc ( x , y , r , 0 , 2 * Math . PI ) ;
146+ ctx . fillStyle = color ;
147+ ctx . fill ( ) ;
148+ }
149+
150+ function drawKeypoints ( keypoints , ctx ) {
151+ for ( let i = 0 ; i < keypoints . length ; i ++ ) {
152+ const keypoint = keypoints [ i ] ;
153+ if ( keypoint . score > 0.5 ) {
154+ drawPoint ( ctx , keypoint . y , keypoint . x , 5 , 'red' ) ;
155+ }
156+ }
157+ }
158+
156159 async function main ( ) {
157160 const video = await setupCamera ( ) ;
158161 const canvas = document . getElementById ( 'canvas' ) ;
159162 const ctx = canvas . getContext ( '2d' ) ;
160-
161- // Set canvas dimensions to match video
162163 canvas . width = video . width ;
163164 canvas . height = video . height ;
164-
165- // Initialize variables
165+
166+ const detector = await poseDetection . createDetector ( poseDetection . SupportedModels . BlazePose , { runtime : 'tfjs' } ) ;
167+
166168 let reps = 0 ;
167- let stickStage = null ;
168- let lastRepTime = 0 ;
169- const REP_COOLDOWN = 1000 ; // Minimum time (ms) between reps
170-
171- // Start the detection loop
172- detect ( video , ctx , canvas , reps , stickStage , lastRepTime , REP_COOLDOWN ) ;
173- }
169+ let stage = null ; // 'up' or 'down'
174170
175- async function detect ( video , ctx , canvas , reps , stickStage , lastRepTime , REP_COOLDOWN ) {
176- try {
177- // Get video frame data
171+ async function detect ( ) {
172+ const poses = await detector . estimatePoses ( video ) ;
178173 ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
179174 ctx . drawImage ( video , 0 , 0 , canvas . width , canvas . height ) ;
180-
181- // Get image data to analyze pixels
182- const imageData = ctx . getImageData ( 0 , 0 , canvas . width , canvas . height ) ;
183- const data = imageData . data ;
184-
185- // Track stick
186- let stickX = 0 ;
187- let stickY = 0 ;
188- let matchingPixels = 0 ;
189-
190- // Get selected color
191- const selectedColor = document . getElementById ( 'stick-color' ) . value ;
192-
193- // Scan for matching colored pixels
194- for ( let i = 0 ; i < data . length ; i += 4 ) {
195- const r = data [ i ] ;
196- const g = data [ i + 1 ] ;
197- const b = data [ i + 2 ] ;
198-
199- let isMatch = false ;
200- if ( selectedColor === 'black' ) {
201- isMatch = r < 50 && g < 50 && b < 50 ;
202- } else if ( selectedColor === 'white' ) {
203- isMatch = r > 200 && g > 200 && b > 200 ;
204- }
205-
206- if ( isMatch ) {
207- const pixelIndex = i / 4 ;
208- const x = pixelIndex % canvas . width ;
209- const y = Math . floor ( pixelIndex / canvas . width ) ;
210- stickX += x ;
211- stickY += y ;
212- matchingPixels ++ ;
213- }
214- }
215-
216- if ( matchingPixels > 100 ) {
217- stickX = Math . round ( stickX / matchingPixels ) ;
218- stickY = Math . round ( stickY / matchingPixels ) ;
219-
220- ctx . fillStyle = '#00FF00' ;
221- ctx . beginPath ( ) ;
222- ctx . arc ( stickX , stickY , 8 , 0 , 2 * Math . PI ) ;
223- ctx . fill ( ) ;
224-
225- ctx . fillStyle = 'white' ;
226- ctx . font = '16px Arial' ;
227- ctx . fillText ( `${ selectedColor . charAt ( 0 ) . toUpperCase ( ) + selectedColor . slice ( 1 ) } Stick: (${ stickX } , ${ stickY } )` , 10 , 20 ) ;
228-
229- const currentTime = Date . now ( ) ;
230- const timeSinceLastRep = currentTime - lastRepTime ;
231-
232- if ( stickY < canvas . height / 3 ) {
233- stickStage = 'ready' ;
234- }
235-
236- if ( stickY > canvas . height * 2 / 3 &&
237- stickStage === 'ready' &&
238- timeSinceLastRep > REP_COOLDOWN ) {
239- reps ++ ;
240- stickStage = 'down' ;
241- lastRepTime = currentTime ;
242- document . getElementById ( 'reps' ) . textContent = `Reps: ${ reps } ` ;
175+
176+ if ( poses . length > 0 ) {
177+ const keypoints = poses [ 0 ] . keypoints ;
178+ const trackedHand = document . getElementById ( 'hand-select' ) . value ;
179+
180+ const wrist = keypoints . find ( k => k . name === `${ trackedHand } _wrist` ) ;
181+ const shoulder = keypoints . find ( k => k . name === `${ trackedHand } _shoulder` ) ;
182+
183+ if ( wrist && shoulder && wrist . score > 0.5 && shoulder . score > 0.5 ) {
184+ drawKeypoints ( keypoints , ctx ) ;
185+ const distance = Math . abs ( wrist . y - shoulder . y ) ;
186+
187+ if ( distance < 50 ) { // Hand is up
188+ stage = 'up' ;
189+ }
190+ if ( distance > 100 && stage === 'up' ) { // Hand is down
191+ stage = 'down' ;
192+ reps ++ ;
193+ document . getElementById ( 'reps' ) . textContent = `Reps: ${ reps } ` ;
194+ }
243195 }
244-
245- ctx . fillText ( `Stage: ${ stickStage || 'none' } ` , 10 , 40 ) ;
246196 }
247- } catch ( error ) {
248- console . error ( 'Detection error:' , error ) ;
197+ requestAnimationFrame ( detect ) ;
249198 }
250-
251- requestAnimationFrame ( ( ) => detect ( video , ctx , canvas , reps , stickStage , lastRepTime , REP_COOLDOWN ) ) ;
199+
200+ detect ( ) ;
252201 }
253202
254203 function resetReps ( ) {
255204 reps = 0 ;
256- stickStage = null ;
205+ stage = null ;
257206 document . getElementById ( 'reps' ) . textContent = 'Reps: 0' ;
258207 }
259208
260209 window . onload = ( ) => {
261- main ( ) ;
262-
263- // Load saved color preference
264- const savedColor = localStorage . getItem ( 'wallBallStickColor' ) || 'black' ;
265- document . getElementById ( 'stick-color' ) . value = savedColor ;
266-
267- // Add event listener for color selection change
268- document . getElementById ( 'stick-color' ) . addEventListener ( 'change' , function ( ) {
269- localStorage . setItem ( 'wallBallStickColor' , this . value ) ;
270- } ) ;
271-
272- // Add event listener for view toggle
273- let showLines = true ;
274- document . getElementById ( 'view-toggle' ) . addEventListener ( 'click' , function ( ) {
275- showLines = ! showLines ;
276- this . textContent = showLines ? 'Hide Lines' : 'Show Lines' ;
210+ main ( ) . catch ( console . error ) ;
211+ const savedHand = localStorage . getItem ( 'wallBallTrackedHand' ) || 'right' ;
212+ document . getElementById ( 'hand-select' ) . value = savedHand ;
213+
214+ document . getElementById ( 'hand-select' ) . addEventListener ( 'change' , function ( ) {
215+ localStorage . setItem ( 'wallBallTrackedHand' , this . value ) ;
277216 } ) ;
278217 } ;
279218 </ script >
0 commit comments