1+ <!DOCTYPE html>
2+ < html >
3+
4+ < head >
5+ < meta charset ="UTF-8 ">
6+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
7+ < title > Air Gesture Button Control</ title >
8+
9+ < script src ="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js "> </ script >
10+ < script src ="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js "> </ script >
11+
12+ < style >
13+ body {
14+ margin : 0 ;
15+ font-family : Arial, sans-serif;
16+ background : # 212121 ;
17+ color : white;
18+ overflow : hidden;
19+ }
20+
21+ # video {
22+ position : absolute;
23+ transform : scaleX (-1 );
24+ opacity : 0.25 ;
25+ max-width : 100% ;
26+ height : 100% ;
27+ }
28+
29+ # canvas {
30+ position : absolute;
31+ left : 0 ;
32+ top : 0 ;
33+ }
34+
35+ # cursor {
36+ position : absolute;
37+ width : 18px ;
38+ height : 18px ;
39+ border-radius : 50% ;
40+ background : # df0000 ;
41+ pointer-events : none;
42+ }
43+
44+ # ui {
45+ position : fixed;
46+ top : 20px ;
47+ left : 50% ;
48+ transform : translateX (-50% );
49+ display : flex;
50+ gap : 20px ;
51+ }
52+
53+ .button {
54+ padding : 15px 25px ;
55+ background : # fff ;
56+ color : # 212121 ;
57+ border-radius : 5px ;
58+ font-size : 18px ;
59+ cursor : pointer;
60+ transition : background 0.3s ease;
61+ }
62+
63+ .button : hover {
64+ background : # f0f0f0 ;
65+ }
66+
67+ .active {
68+ background : # 00df00 ;
69+ }
70+
71+ # status {
72+ position : fixed;
73+ bottom : 20px ;
74+ left : 20px ;
75+ background : rgba (0 , 0 , 0 , 0.5 );
76+ padding : 10px 15px ;
77+ border-radius : 8px ;
78+ font-size : 14px ;
79+ }
80+ </ style >
81+ </ head >
82+
83+ < body >
84+ < video id ="video " autoplay playsinline > </ video >
85+ < canvas id ="canvas "> </ canvas >
86+ < div id ="cursor "> </ div >
87+
88+ < div id ="ui ">
89+ < div id ="btnBack " class ="button "> Back</ div >
90+ < div id ="btnForward " class ="button "> Forward</ div >
91+ < div id ="btnUp " class ="button "> Scroll Up</ div >
92+ < div id ="btnDown " class ="button "> Scroll Down</ div >
93+ < div id ="btnConfirm " class ="button "> Confirm</ div >
94+ </ div >
95+
96+ < div id ="status "> Status: Idle</ div >
97+
98+ < script >
99+ const video = document . getElementById ( "video" ) ;
100+ const canvas = document . getElementById ( "canvas" ) ;
101+ const ctx = canvas . getContext ( "2d" ) ;
102+ const cursor = document . getElementById ( "cursor" ) ;
103+ const status = document . getElementById ( "status" ) ;
104+
105+ canvas . width = window . innerWidth ;
106+ canvas . height = window . innerHeight ;
107+
108+ let drawing = false ;
109+ let path = [ ] ;
110+ let prevX = 0 , prevY = 0 ;
111+ let idleTimeout = null ;
112+
113+ const distance = ( a , b ) => Math . sqrt ( ( a . x - b . x ) ** 2 + ( a . y - b . y ) ** 2 ) ;
114+
115+ const setActiveButton = id => {
116+ document . querySelectorAll ( ".button" ) . forEach ( b => b . classList . remove ( "active" ) ) ;
117+ document . getElementById ( id ) . classList . add ( "active" ) ;
118+ } ;
119+
120+ const showToast = message => {
121+ const toast = document . createElement ( "div" ) ;
122+ toast . className = "toast" ;
123+ toast . innerText = message ;
124+ toastContainer . appendChild ( toast ) ;
125+
126+ setTimeout ( ( ) => {
127+ toast . remove ( ) ;
128+ } , 1000 ) ;
129+ } ;
130+
131+ const resetToIdle = ( ) => {
132+ status . innerText = "Status: Idle" ;
133+ document . querySelectorAll ( ".button" ) . forEach ( b => b . classList . remove ( "active" ) ) ;
134+ } ;
135+
136+ function recognizeGesture ( ) {
137+ if ( path . length < 8 ) {
138+ status . innerText = "Status: Not Recognized" ;
139+ return ;
140+ }
141+
142+ const start = path [ 0 ] ;
143+ const end = path [ path . length - 1 ] ;
144+ const dx = end . x - start . x ;
145+ const dy = end . y - start . y ;
146+
147+ // Horizontal
148+ if ( Math . abs ( dx ) > 150 && Math . abs ( dy ) < 80 ) {
149+ if ( dx > 0 ) {
150+ status . innerText = "Gesture: Forward" ;
151+ setActiveButton ( "btnForward" ) ;
152+ } else {
153+ status . innerText = "Gesture: Back" ;
154+ setActiveButton ( "btnBack" ) ;
155+ }
156+ return ;
157+ }
158+
159+ // Vertical
160+ if ( Math . abs ( dy ) > 150 && Math . abs ( dx ) < 80 ) {
161+ if ( dy > 0 ) {
162+ status . innerText = "Gesture: Scroll Down" ;
163+ setActiveButton ( "btnDown" ) ;
164+ window . scrollBy ( 0 , 300 ) ;
165+ } else {
166+ status . innerText = "Gesture: Scroll Up" ;
167+ setActiveButton ( "btnUp" ) ;
168+ window . scrollBy ( 0 , - 300 ) ;
169+ }
170+ return ;
171+ }
172+
173+ // Circle
174+ if ( isCircle ( path ) ) {
175+ status . innerText = "Gesture: Confirm" ;
176+ setActiveButton ( "btnConfirm" ) ;
177+ return ;
178+ }
179+
180+ status . innerText = "Status: Not Recognized" ;
181+ }
182+
183+ function isCircle ( path ) {
184+ let minX = Infinity , maxX = - Infinity ;
185+ let minY = Infinity , maxY = - Infinity ;
186+
187+ path . forEach ( p => {
188+ minX = Math . min ( minX , p . x ) ;
189+ maxX = Math . max ( maxX , p . x ) ;
190+ minY = Math . min ( minY , p . y ) ;
191+ maxY = Math . max ( maxY , p . y ) ;
192+ } ) ;
193+
194+ const width = maxX - minX ;
195+ const height = maxY - minY ;
196+
197+ const start = path [ 0 ] ;
198+ const end = path [ path . length - 1 ] ;
199+ const closeDist = Math . hypot ( end . x - start . x , end . y - start . y ) ;
200+
201+ if ( closeDist > 100 ) return false ;
202+
203+ let length = 0 ;
204+ for ( let i = 1 ; i < path . length ; i ++ ) {
205+ length += Math . hypot ( path [ i ] . x - path [ i - 1 ] . x , path [ i ] . y - path [ i - 1 ] . y ) ;
206+ }
207+
208+ return true ;
209+ }
210+
211+ function drawPath ( ) {
212+ ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
213+ if ( path . length < 2 ) return ;
214+
215+ ctx . beginPath ( ) ;
216+ ctx . moveTo ( path [ 0 ] . x , path [ 0 ] . y ) ;
217+ for ( let i = 1 ; i < path . length ; i ++ ) {
218+ ctx . lineTo ( path [ i ] . x , path [ i ] . y ) ;
219+ }
220+ ctx . strokeStyle = "#00df00" ;
221+ ctx . lineWidth = 4 ;
222+ ctx . stroke ( ) ;
223+ }
224+
225+ const hands = new Hands ( {
226+ locateFile : file => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${ file } `
227+ } ) ;
228+
229+ hands . setOptions ( {
230+ maxNumHands : 1 ,
231+ modelComplexity : 1 ,
232+ minDetectionConfidence : 0.7 ,
233+ minTrackingConfidence : 0.7
234+ } ) ;
235+
236+ hands . onResults ( results => {
237+ if ( ! results . multiHandLandmarks . length ) return ;
238+
239+ const hand = results . multiHandLandmarks [ 0 ] ;
240+ const index = hand [ 8 ] ;
241+ const thumb = hand [ 4 ] ;
242+
243+ const x = ( 1 - index . x ) * window . innerWidth ;
244+ const y = index . y * window . innerHeight ;
245+
246+ const smoothX = prevX * 0.7 + x * 0.3 ;
247+ const smoothY = prevY * 0.7 + y * 0.3 ;
248+
249+ prevX = smoothX ;
250+ prevY = smoothY ;
251+
252+ cursor . style . left = `${ smoothX } px` ;
253+ cursor . style . top = `${ smoothY } px` ;
254+
255+ const pinch = distance ( index , thumb ) ;
256+
257+ if ( pinch < 0.05 ) {
258+ if ( ! drawing ) {
259+ drawing = true ;
260+ path = [ ] ;
261+ clearTimeout ( idleTimeout ) ;
262+ status . innerText = "Drawing gesture..." ;
263+ }
264+ path . push ( { x : smoothX , y : smoothY } ) ;
265+ } else {
266+ if ( drawing ) {
267+ drawing = false ;
268+ recognizeGesture ( ) ;
269+ path = [ ] ;
270+ idleTimeout = setTimeout ( resetToIdle , 1000 ) ;
271+ }
272+ }
273+
274+ drawPath ( ) ;
275+ } ) ;
276+
277+ if ( ! navigator . mediaDevices || ! navigator . mediaDevices . getUserMedia ) {
278+ alert ( "Camera API not supported" ) ;
279+ } else {
280+ const camera = new Camera ( video , {
281+ onFrame : async ( ) => {
282+ await hands . send ( { image : video } ) ;
283+ } ,
284+ width : 640 ,
285+ height : 480
286+ } ) ;
287+ camera . start ( ) ;
288+ }
289+ </ script >
290+ </ body >
291+
292+ </ html >
0 commit comments