44const canvas = document . getElementById ( 'gameCanvas' ) ;
55const ctx = canvas . getContext ( '2d' ) ;
66
7+ // Canvas scaling for responsive design
8+ function resizeCanvas ( ) {
9+ const wrapper = document . getElementById ( 'canvasWrapper' ) ;
10+ const displayWidth = wrapper . clientWidth ;
11+ const displayHeight = wrapper . clientHeight ;
12+
13+ // Always maintain internal resolution of 1000x400
14+ canvas . width = 1000 ;
15+ canvas . height = 400 ;
16+
17+ // Let CSS scale the display
18+ canvas . style . width = displayWidth + 'px' ;
19+ canvas . style . height = displayHeight + 'px' ;
20+ }
21+
22+ // Initialize canvas size on load
23+ resizeCanvas ( ) ;
24+
25+ // Resize canvas on window resize
26+ window . addEventListener ( 'resize' , resizeCanvas ) ;
27+
28+ // Handle orientation change on mobile
29+ window . addEventListener ( 'orientationchange' , ( ) => {
30+ setTimeout ( resizeCanvas , 100 ) ;
31+ } ) ;
32+
733// Polyfill for roundRect (Safari < 16, older browsers)
834if ( ! ctx . roundRect ) {
935 CanvasRenderingContext2D . prototype . roundRect = function ( x , y , w , h , radii ) {
@@ -132,7 +158,7 @@ const ENDING_DURATION = 12; // seconds for full animation
132158const levels = [
133159 {
134160 // Tutorial: Just one light with long green, teaches basic controls
135- name : "First Light " ,
161+ name : "First light " ,
136162 startSpeed : 40 ,
137163 lights : [
138164 { x : 600 , greenDuration : 4 , redDuration : 2 , offset : 0 } ,
@@ -141,7 +167,7 @@ const levels = [
141167 } ,
142168 {
143169 // Two lights, introduces timing between lights
144- name : "Easy Start " ,
170+ name : "Easy start " ,
145171 startSpeed : 40 ,
146172 lights : [
147173 { x : 500 , greenDuration : 3.5 , redDuration : 2 , offset : 0 } ,
@@ -151,7 +177,7 @@ const levels = [
151177 } ,
152178 {
153179 // Three lights, first real challenge
154- name : "Finding the Rhythm " ,
180+ name : "Finding the rhythm " ,
155181 startSpeed : 45 ,
156182 lights : [
157183 { x : 500 , greenDuration : 3 , redDuration : 2 , offset : 0 } ,
@@ -162,7 +188,7 @@ const levels = [
162188 } ,
163189 {
164190 // Four lights with tighter timing
165- name : "Keep the Pace " ,
191+ name : "Keep the pace " ,
166192 startSpeed : 50 ,
167193 lights : [
168194 { x : 400 , greenDuration : 2.5 , redDuration : 2.5 , offset : 0 } ,
@@ -174,7 +200,7 @@ const levels = [
174200 } ,
175201 {
176202 // Mixed timing requires speed adjustment
177- name : "Speed Adjustment " ,
203+ name : "Speed adjustment " ,
178204 startSpeed : 60 ,
179205 lights : [
180206 { x : 400 , greenDuration : 2 , redDuration : 3 , offset : 0 } ,
@@ -187,7 +213,7 @@ const levels = [
187213 } ,
188214 {
189215 // Long level with many lights
190- name : "The Long Road " ,
216+ name : "The long road " ,
191217 startSpeed : 55 ,
192218 lights : [
193219 { x : 350 , greenDuration : 2 , redDuration : 2 , offset : 0 } ,
@@ -202,7 +228,7 @@ const levels = [
202228 } ,
203229 {
204230 // Short greens, requires patience and precise timing
205- name : "Patience Required " ,
231+ name : "Patience required " ,
206232 startSpeed : 70 ,
207233 lights : [
208234 { x : 400 , greenDuration : 1.5 , redDuration : 3 , offset : 0 } ,
@@ -311,7 +337,13 @@ function getCurrentPhaseDuration(light, time) {
311337 }
312338}
313339
314- // Input handling
340+ // Detect device type
341+ const isMobileDevice = ( ) => {
342+ return / A n d r o i d | w e b O S | i P h o n e | i P a d | i P o d | B l a c k B e r r y | I E M o b i l e | O p e r a M i n i / i. test ( navigator . userAgent )
343+ || window . matchMedia ( '(max-width: 768px)' ) . matches ;
344+ } ;
345+
346+ // Input handling - Keyboard
315347document . addEventListener ( 'keydown' , ( e ) => {
316348 if ( e . key === 'w' || e . key === 'W' || e . key === 'ArrowUp' ) {
317349 keys . gas = true ;
@@ -343,6 +375,60 @@ document.addEventListener('keydown', (e) => {
343375 }
344376} ) ;
345377
378+ // Mobile touch controls
379+ const gasButton = document . getElementById ( 'gasButton' ) ;
380+ const brakeButton = document . getElementById ( 'brakeButton' ) ;
381+ const restartButton = document . getElementById ( 'restartButton' ) ;
382+
383+ // Prevent double-tap zoom on mobile buttons
384+ gasButton . addEventListener ( 'touchstart' , ( e ) => {
385+ e . preventDefault ( ) ;
386+ keys . gas = true ;
387+ } , { passive : false } ) ;
388+
389+ gasButton . addEventListener ( 'touchend' , ( e ) => {
390+ e . preventDefault ( ) ;
391+ keys . gas = false ;
392+ } , { passive : false } ) ;
393+
394+ gasButton . addEventListener ( 'mousedown' , ( ) => {
395+ keys . gas = true ;
396+ } ) ;
397+
398+ gasButton . addEventListener ( 'mouseup' , ( ) => {
399+ keys . gas = false ;
400+ } ) ;
401+
402+ gasButton . addEventListener ( 'mouseleave' , ( ) => {
403+ keys . gas = false ;
404+ } ) ;
405+
406+ brakeButton . addEventListener ( 'touchstart' , ( e ) => {
407+ e . preventDefault ( ) ;
408+ keys . brake = true ;
409+ } , { passive : false } ) ;
410+
411+ brakeButton . addEventListener ( 'touchend' , ( e ) => {
412+ e . preventDefault ( ) ;
413+ keys . brake = false ;
414+ } , { passive : false } ) ;
415+
416+ brakeButton . addEventListener ( 'mousedown' , ( ) => {
417+ keys . brake = true ;
418+ } ) ;
419+
420+ brakeButton . addEventListener ( 'mouseup' , ( ) => {
421+ keys . brake = false ;
422+ } ) ;
423+
424+ brakeButton . addEventListener ( 'mouseleave' , ( ) => {
425+ keys . brake = false ;
426+ } ) ;
427+
428+ restartButton . addEventListener ( 'click' , ( ) => {
429+ initLevel ( currentLevel ) ;
430+ } ) ;
431+
346432// Message display
347433function showMessage ( title , text , buttonText , buttonAction ) {
348434 messageTitle . textContent = title ;
@@ -375,11 +461,11 @@ function winLevel() {
375461 const stars = calculateStars ( totalSpeedChange , level . finishX ) ;
376462 const starDisplay = getStarDisplay ( stars ) ;
377463
378- let timeText = `Time: ${ formatTime ( finishTime ) } s` ;
464+ let timeText = `Time: ${ formatTime ( finishTime ) } s` ;
379465 if ( isNewRecord ) {
380- timeText += ' - New Record !' ;
466+ timeText += ' - New record !' ;
381467 } else if ( bestTime ) {
382- timeText += ` (Best: ${ formatTime ( bestTime ) } s)` ;
468+ timeText += ` (Best: ${ formatTime ( bestTime ) } s)` ;
383469 }
384470
385471 if ( currentLevel >= levels . length ) {
@@ -1528,5 +1614,25 @@ function gameLoop(timestamp) {
15281614}
15291615
15301616// Start the game
1531- initLevel ( 1 ) ;
1532- requestAnimationFrame ( gameLoop ) ;
1617+ let gameStarted = false ;
1618+
1619+ const startScreen = document . getElementById ( 'startScreen' ) ;
1620+ const startButton = document . getElementById ( 'startButton' ) ;
1621+
1622+ // Check if we should skip to ending animation for testing
1623+ const urlParams = new URLSearchParams ( window . location . search ) ;
1624+ const skipToEnding = urlParams . has ( 'ending' ) ;
1625+
1626+ startButton . addEventListener ( 'click' , ( ) => {
1627+ gameStarted = true ;
1628+ startScreen . classList . add ( 'hidden' ) ;
1629+
1630+ if ( skipToEnding ) {
1631+ gameState = 'ending' ;
1632+ endingTime = 0 ;
1633+ } else {
1634+ initLevel ( 1 ) ;
1635+ }
1636+
1637+ requestAnimationFrame ( gameLoop ) ;
1638+ } ) ;
0 commit comments