@@ -41,6 +41,9 @@ let elapsed = 0;
4141let hasDriven = false ;
4242let isDrifting = false ;
4343let onTrack = true ;
44+ let fenceImpact = 0 ;
45+ let fenceHits = 0 ;
46+ let cameraZoom = 1.65 ;
4447const demoMode = new URLSearchParams ( window . location . search ) . get ( "demo" ) ;
4548
4649const car = {
@@ -108,6 +111,8 @@ function resetCar() {
108111 previousRearWheels = null ;
109112 isDrifting = false ;
110113 onTrack = true ;
114+ fenceImpact = 0 ;
115+ fenceHits = 0 ;
111116}
112117
113118function resizeCanvas ( ) {
@@ -118,7 +123,8 @@ function resizeCanvas() {
118123 canvas . width = Math . round ( width * ratio ) ;
119124 canvas . height = Math . round ( height * ratio ) ;
120125 context . setTransform ( ratio , 0 , 0 , ratio , 0 , 0 ) ;
121- trackWidth = clamp ( Math . min ( width , height ) * 0.205 , 78 , 142 ) ;
126+ trackWidth = clamp ( Math . min ( width , height ) * 0.27 , 104 , 174 ) ;
127+ cameraZoom = width < 600 ? 1.42 : 1.65 ;
122128 car . length = trackWidth * 0.56 ;
123129 car . width = car . length * 0.51 ;
124130 buildTrack ( ) ;
@@ -137,28 +143,33 @@ function createTrackPath() {
137143 return path ;
138144}
139145
140- function distanceToTrack ( x , y ) {
146+ function getNearestTrackPoint ( x , y ) {
147+ let nearest = trackSamples [ 0 ] ;
141148 let shortest = Number . POSITIVE_INFINITY ;
142- for ( let index = 0 ; index < trackSamples . length ; index += 2 ) {
149+ for ( let index = 0 ; index < trackSamples . length ; index += 1 ) {
143150 const point = trackSamples [ index ] ;
144- shortest = Math . min ( shortest , Math . hypot ( x - point . x , y - point . y ) ) ;
151+ const distance = Math . hypot ( x - point . x , y - point . y ) ;
152+ if ( distance < shortest ) {
153+ shortest = distance ;
154+ nearest = point ;
155+ }
145156 }
146- return shortest ;
157+ return { point : nearest , distance : shortest } ;
147158}
148159
149160function drawGrass ( ) {
150161 context . fillStyle = "#78945e" ;
151- context . fillRect ( 0 , 0 , width , height ) ;
162+ context . fillRect ( - width , - height , width * 3 , height * 3 ) ;
152163
153164 context . save ( ) ;
154165 context . globalAlpha = 0.16 ;
155166 context . strokeStyle = "#263d28" ;
156167 context . lineWidth = 1 ;
157168 const gap = Math . max ( 22 , Math . min ( width , height ) * 0.045 ) ;
158- for ( let x = - height ; x < width + height ; x += gap ) {
169+ for ( let x = - width - height ; x < width * 2 + height ; x += gap ) {
159170 context . beginPath ( ) ;
160- context . moveTo ( x , 0 ) ;
161- context . lineTo ( x + height , height ) ;
171+ context . moveTo ( x , - height ) ;
172+ context . lineTo ( x + height * 3 , height * 2 ) ;
162173 context . stroke ( ) ;
163174 }
164175 context . restore ( ) ;
@@ -227,6 +238,53 @@ function drawRouteTirePrints() {
227238 context . restore ( ) ;
228239}
229240
241+ function createFencePath ( side ) {
242+ const path = new Path2D ( ) ;
243+ const offset = trackWidth / 2 + trackWidth * 0.105 ;
244+ trackSamples . forEach ( ( point , index ) => {
245+ const x = point . x - Math . sin ( point . angle ) * offset * side ;
246+ const y = point . y + Math . cos ( point . angle ) * offset * side ;
247+ if ( index === 0 ) path . moveTo ( x , y ) ;
248+ else path . lineTo ( x , y ) ;
249+ } ) ;
250+ path . closePath ( ) ;
251+ return path ;
252+ }
253+
254+ function drawFences ( ) {
255+ const fenceOffset = trackWidth / 2 + trackWidth * 0.105 ;
256+ context . save ( ) ;
257+ context . lineJoin = "round" ;
258+ context . lineCap = "round" ;
259+
260+ [ - 1 , 1 ] . forEach ( ( side ) => {
261+ const path = createFencePath ( side ) ;
262+ context . strokeStyle = "rgba(11, 14, 12, 0.58)" ;
263+ context . lineWidth = 10 ;
264+ context . stroke ( path ) ;
265+ context . strokeStyle = "#b9c0ba" ;
266+ context . lineWidth = 5.5 ;
267+ context . stroke ( path ) ;
268+ context . strokeStyle = "#f2f1e9" ;
269+ context . lineWidth = 1.4 ;
270+ context . stroke ( path ) ;
271+
272+ for ( let index = 0 ; index < trackSamples . length ; index += 7 ) {
273+ const point = trackSamples [ index ] ;
274+ const x = point . x - Math . sin ( point . angle ) * fenceOffset * side ;
275+ const y = point . y + Math . cos ( point . angle ) * fenceOffset * side ;
276+ context . fillStyle = "#dfe2dc" ;
277+ context . strokeStyle = "#161a17" ;
278+ context . lineWidth = 2 ;
279+ context . beginPath ( ) ;
280+ context . arc ( x , y , clamp ( trackWidth * 0.035 , 4 , 6 ) , 0 , Math . PI * 2 ) ;
281+ context . fill ( ) ;
282+ context . stroke ( ) ;
283+ }
284+ } ) ;
285+ context . restore ( ) ;
286+ }
287+
230288function drawTrack ( ) {
231289 const path = createTrackPath ( ) ;
232290 context . save ( ) ;
@@ -252,6 +310,7 @@ function drawTrack() {
252310
253311 drawCurbs ( ) ;
254312 drawRouteTirePrints ( ) ;
313+ drawFences ( ) ;
255314}
256315
257316function getRearWheels ( ) {
@@ -287,10 +346,10 @@ function spawnSmoke(wheels) {
287346 velocityY : - car . velocityY * 0.08 + ( Math . random ( ) - 0.5 ) * 18 ,
288347 life : 2 ,
289348 maximumLife : 2 ,
290- size : car . width * ( 0.18 + Math . random ( ) * 0.12 ) ,
349+ size : car . width * ( 0.32 + Math . random ( ) * 0.18 ) ,
291350 } ) ;
292351 } ) ;
293- if ( smokeParticles . length > 240 ) smokeParticles . splice ( 0 , smokeParticles . length - 240 ) ;
352+ if ( smokeParticles . length > 420 ) smokeParticles . splice ( 0 , smokeParticles . length - 420 ) ;
294353}
295354
296355function updateParticles ( deltaTime ) {
@@ -300,7 +359,7 @@ function updateParticles(deltaTime) {
300359 particle . y += particle . velocityY * deltaTime ;
301360 particle . velocityX *= 0.985 ;
302361 particle . velocityY *= 0.985 ;
303- particle . size += deltaTime * car . width * 0.22 ;
362+ particle . size += deltaTime * car . width * 0.4 ;
304363 } ) ;
305364 smokeParticles = smokeParticles . filter ( ( particle ) => particle . life > 0 ) ;
306365}
@@ -348,10 +407,28 @@ function updateCar(deltaTime) {
348407 car . x += car . velocityX * deltaTime ;
349408 car . y += car . velocityY * deltaTime ;
350409
351- onTrack = distanceToTrack ( car . x , car . y ) <= trackWidth * 0.56 ;
410+ fenceImpact = Math . max ( 0 , fenceImpact - deltaTime ) ;
411+ const nearest = getNearestTrackPoint ( car . x , car . y ) ;
412+ const maximumDistance = trackWidth / 2 - car . width * 0.56 ;
413+ onTrack = nearest . distance <= maximumDistance ;
352414 if ( ! onTrack ) {
353- car . velocityX *= Math . max ( 0 , 1 - 2.4 * deltaTime ) ;
354- car . velocityY *= Math . max ( 0 , 1 - 2.4 * deltaTime ) ;
415+ const differenceX = car . x - nearest . point . x ;
416+ const differenceY = car . y - nearest . point . y ;
417+ const safeDistance = Math . max ( 0.001 , nearest . distance ) ;
418+ const normalX = differenceX / safeDistance ;
419+ const normalY = differenceY / safeDistance ;
420+ car . x = nearest . point . x + normalX * maximumDistance ;
421+ car . y = nearest . point . y + normalY * maximumDistance ;
422+ const outwardVelocity = car . velocityX * normalX + car . velocityY * normalY ;
423+ if ( outwardVelocity > 0 ) {
424+ car . velocityX -= normalX * outwardVelocity * 1.45 ;
425+ car . velocityY -= normalY * outwardVelocity * 1.45 ;
426+ }
427+ car . velocityX *= 0.72 ;
428+ car . velocityY *= 0.72 ;
429+ fenceImpact = 0.34 ;
430+ fenceHits += 1 ;
431+ onTrack = true ;
355432 }
356433
357434 const margin = car . length ;
@@ -362,9 +439,9 @@ function updateCar(deltaTime) {
362439 if ( isDrifting ) {
363440 addTireMarks ( wheels ) ;
364441 smokeAccumulator += deltaTime ;
365- while ( smokeAccumulator >= 0.045 ) {
442+ while ( smokeAccumulator >= 0.025 ) {
366443 spawnSmoke ( wheels ) ;
367- smokeAccumulator -= 0.045 ;
444+ smokeAccumulator -= 0.025 ;
368445 }
369446 } else {
370447 previousRearWheels = null ;
@@ -375,8 +452,8 @@ function updateCar(deltaTime) {
375452 speedValue . textContent = String ( speed ) ;
376453 driveState . textContent = ! hasDriven
377454 ? "待发车"
378- : ! onTrack
379- ? "草地减速 "
455+ : fenceImpact > 0
456+ ? "撞到围栏 "
380457 : isDrifting
381458 ? "漂移!"
382459 : speed > 5
@@ -402,10 +479,10 @@ function drawSmoke() {
402479 smokeParticles . forEach ( ( particle ) => {
403480 const progress = particle . life / particle . maximumLife ;
404481 context . save ( ) ;
405- context . globalAlpha = Math . min ( 0.66 , progress * 0.72 ) ;
482+ context . globalAlpha = Math . min ( 0.78 , progress * 0.86 ) ;
406483 context . fillStyle = "#f4f0e7" ;
407484 context . strokeStyle = "rgba(40, 43, 39, 0.45)" ;
408- context . lineWidth = 1.5 ;
485+ context . lineWidth = 2 ;
409486 context . beginPath ( ) ;
410487 context . arc ( particle . x , particle . y , particle . size , 0 , Math . PI * 2 ) ;
411488 context . fill ( ) ;
@@ -536,15 +613,27 @@ function drawCar() {
536613
537614function drawScene ( ) {
538615 context . clearRect ( 0 , 0 , width , height ) ;
616+ context . save ( ) ;
617+ context . translate ( width / 2 , height / 2 ) ;
618+ context . scale ( cameraZoom , cameraZoom ) ;
619+ context . translate ( - car . x , - car . y ) ;
539620 drawGrass ( ) ;
540621 drawTrack ( ) ;
541622 drawTireMarks ( ) ;
542623 drawSmoke ( ) ;
543624 drawCar ( ) ;
625+ context . restore ( ) ;
544626}
545627
546628function updateDemoControls ( ) {
547629 if ( ! demoMode ) return ;
630+ if ( demoMode === "continuous" ) {
631+ controls . up = true ;
632+ controls . left = false ;
633+ controls . right = elapsed > 0.6 ;
634+ controls . drift = elapsed > 0.9 ;
635+ return ;
636+ }
548637 controls . up = elapsed < 2.8 ;
549638 controls . left = false ;
550639 controls . right = elapsed > 0.75 && elapsed < 2.8 ;
@@ -560,6 +649,11 @@ function publishDiagnostics() {
560649 stage . dataset . driveState = driveState . textContent ;
561650 stage . dataset . onTrack = String ( onTrack ) ;
562651 stage . dataset . drifting = String ( isDrifting ) ;
652+ stage . dataset . fenceImpact = String ( fenceImpact > 0 ) ;
653+ stage . dataset . fenceHits = String ( fenceHits ) ;
654+ stage . dataset . cameraZoom = String ( cameraZoom ) ;
655+ stage . dataset . carScreenX = String ( Math . round ( width / 2 ) ) ;
656+ stage . dataset . carScreenY = String ( Math . round ( height / 2 ) ) ;
563657}
564658
565659function gameLoop ( timestamp ) {
0 commit comments