@@ -4,8 +4,9 @@ const stage = document.querySelector("#gameStage");
44const speedValue = document . querySelector ( "#speedValue" ) ;
55const driveState = document . querySelector ( "#driveState" ) ;
66const controlCard = document . querySelector ( "#controlCard" ) ;
7- const TOP_SPEED_KMH = 376 ;
8- const SPEED_MULTIPLIER = 9.2 ;
7+ const TOP_SPEED_KMH = 639 ;
8+ const SPEED_MULTIPLIER = 15.64 ;
9+ const MAX_MOTION_TRAIL_FRAMES = 6 ;
910
1011const controls = {
1112 up : false ,
@@ -45,6 +46,11 @@ let airWallHits = 0;
4546let cameraZoom = 1.65 ;
4647let worldTiltX = 0.22 ;
4748let worldTiltY = 0.7 ;
49+ let currentSpeedRatio = 0 ;
50+ let currentSteeringAmount = 0 ;
51+ let turnSlowdownAmount = 0 ;
52+ let motionTrailFrames = [ ] ;
53+ let motionCaptureAccumulator = 0 ;
4854const demoMode = new URLSearchParams ( window . location . search ) . get ( "demo" ) ;
4955
5056const car = {
@@ -90,8 +96,8 @@ function buildTrack() {
9096 const end = points [ ( index + 1 ) % points . length ] ;
9197 const next = points [ ( index + 2 ) % points . length ] ;
9298
93- for ( let step = 0 ; step < 32 ; step += 1 ) {
94- samples . push ( catmullRom ( previous , start , end , next , step / 32 ) ) ;
99+ for ( let step = 0 ; step < 64 ; step += 1 ) {
100+ samples . push ( catmullRom ( previous , start , end , next , step / 64 ) ) ;
95101 }
96102 }
97103
@@ -116,6 +122,11 @@ function resetCar() {
116122 onTrack = true ;
117123 airWallImpact = 0 ;
118124 airWallHits = 0 ;
125+ currentSpeedRatio = 0 ;
126+ currentSteeringAmount = 0 ;
127+ turnSlowdownAmount = 0 ;
128+ motionTrailFrames = [ ] ;
129+ motionCaptureAccumulator = 0 ;
119130}
120131
121132function resizeCanvas ( ) {
@@ -124,8 +135,8 @@ function resizeCanvas() {
124135 width = Math . max ( 320 , bounds . width ) ;
125136 height = Math . max ( 420 , bounds . height ) ;
126137 // Double the complete circuit footprint again for a much longer lap.
127- worldWidth = width * 9.4 ;
128- worldHeight = height * 6.6 ;
138+ worldWidth = width * 18.8 ;
139+ worldHeight = height * 13.2 ;
129140 canvas . width = Math . round ( width * ratio ) ;
130141 canvas . height = Math . round ( height * ratio ) ;
131142 context . setTransform ( ratio , 0 , 0 , ratio , 0 , 0 ) ;
@@ -174,10 +185,17 @@ function drawGrass() {
174185 context . strokeStyle = "#263d28" ;
175186 context . lineWidth = 1 ;
176187 const gap = Math . max ( 22 , Math . min ( width , height ) * 0.045 ) ;
177- for ( let x = - worldWidth - worldHeight ; x < worldWidth * 2 + worldHeight ; x += gap ) {
188+ // Draw only the world-anchored stripe range near the camera to keep high-speed echoes smooth.
189+ const visibleSpan = Math . max ( width , height ) / cameraZoom * 1.8 ;
190+ const minimumY = car . y - visibleSpan ;
191+ const maximumY = car . y + visibleSpan ;
192+ const minimumOffset = car . x - car . y - visibleSpan * 2 ;
193+ const maximumOffset = car . x - car . y + visibleSpan * 2 ;
194+ const firstOffset = Math . floor ( minimumOffset / gap ) * gap ;
195+ for ( let offset = firstOffset ; offset < maximumOffset ; offset += gap ) {
178196 context . beginPath ( ) ;
179- context . moveTo ( x , - worldHeight ) ;
180- context . lineTo ( x + worldHeight * 3 , worldHeight * 2 ) ;
197+ context . moveTo ( offset + minimumY , minimumY ) ;
198+ context . lineTo ( offset + maximumY , maximumY ) ;
181199 context . stroke ( ) ;
182200 }
183201 context . restore ( ) ;
@@ -291,7 +309,9 @@ function updateCar(deltaTime) {
291309 }
292310
293311 forwardSpeed = clamp ( forwardSpeed , - maximumSpeed * 0.34 , maximumSpeed ) ;
294- const steeringPower = 0.4 + Math . min ( 1 , Math . abs ( forwardSpeed ) / maximumSpeed ) * 1.28 ;
312+ const speedRatioBeforeTurn = clamp ( Math . abs ( forwardSpeed ) / maximumSpeed , 0 , 1 ) ;
313+ currentSteeringAmount = Math . abs ( steering ) ;
314+ const steeringPower = 0.4 + speedRatioBeforeTurn * 1.28 ;
295315 if ( Math . abs ( forwardSpeed ) > 4 ) {
296316 car . angle +=
297317 steering *
@@ -300,6 +320,12 @@ function updateCar(deltaTime) {
300320 ( forwardSpeed >= 0 ? 1 : - 1 ) ;
301321 }
302322
323+ // Steering scrubs speed progressively; high-speed bends lose substantially more momentum.
324+ const turningResistance =
325+ currentSteeringAmount * ( 0.18 + speedRatioBeforeTurn * 0.46 ) ;
326+ forwardSpeed *= Math . max ( 0 , 1 - turningResistance * deltaTime ) ;
327+ turnSlowdownAmount = currentSteeringAmount * speedRatioBeforeTurn ;
328+
303329 const grip = 6.8 ;
304330 lateralSpeed *= Math . max ( 0 , 1 - grip * deltaTime ) ;
305331 const rollingResistance = throttle === 0 ? 0.72 : 0.14 ;
@@ -340,15 +366,22 @@ function updateCar(deltaTime) {
340366 car . x = clamp ( car . x , - margin , worldWidth + margin ) ;
341367 car . y = clamp ( car . y , - margin , worldHeight + margin ) ;
342368
343- const speed = Math . round ( Math . abs ( forwardSpeed ) / maximumSpeed * TOP_SPEED_KMH ) ;
369+ currentSpeedRatio = clamp (
370+ Math . hypot ( car . velocityX , car . velocityY ) / maximumSpeed ,
371+ 0 ,
372+ 1 ,
373+ ) ;
374+ const speed = Math . round ( currentSpeedRatio * TOP_SPEED_KMH ) ;
344375 speedValue . textContent = String ( speed ) ;
345376 driveState . textContent = ! hasDriven
346377 ? "待发车"
347378 : airWallImpact > 0
348379 ? "碰到空气墙"
380+ : turnSlowdownAmount > 0.12 && speed > 5
381+ ? "弯道减速"
349382 : speed > 5
350- ? "行驶中"
351- : "已停车" ;
383+ ? "行驶中"
384+ : "已停车" ;
352385}
353386
354387function roundedRectangle ( x , y , rectangleWidth , rectangleHeight , radius ) {
@@ -541,22 +574,115 @@ function drawCar() {
541574 context . restore ( ) ;
542575}
543576
544- function drawScene ( ) {
545- context . clearRect ( 0 , 0 , width , height ) ;
577+ function drawCarAfterimages ( ) {
578+ if ( currentSpeedRatio < 0.08 || window . matchMedia ( "(prefers-reduced-motion: reduce)" ) . matches ) {
579+ return ;
580+ }
581+
582+ const layerCount = Math . ceil ( currentSpeedRatio * MAX_MOTION_TRAIL_FRAMES ) ;
583+ const directionX = Math . cos ( car . angle ) ;
584+ const directionY = Math . sin ( car . angle ) ;
585+
586+ for ( let layer = layerCount ; layer >= 1 ; layer -= 1 ) {
587+ const layerProgress = layer / ( layerCount + 1 ) ;
588+ const trailDistance =
589+ car . length * layer * ( 0.08 + currentSpeedRatio * 0.055 ) ;
590+ context . save ( ) ;
591+ context . globalAlpha =
592+ ( 0.035 + currentSpeedRatio * 0.065 ) * ( 1 - layerProgress * 0.68 ) ;
593+ context . translate ( - directionX * trailDistance , - directionY * trailDistance ) ;
594+ drawCar ( ) ;
595+ context . restore ( ) ;
596+ }
597+ }
598+
599+ function captureWorldMotionFrame ( deltaTime ) {
600+ if ( window . matchMedia ( "(prefers-reduced-motion: reduce)" ) . matches ) return ;
601+ if ( currentSpeedRatio < 0.04 ) {
602+ motionCaptureAccumulator = 0 ;
603+ if ( currentSpeedRatio < 0.015 ) motionTrailFrames = [ ] ;
604+ return ;
605+ }
606+
607+ motionCaptureAccumulator += deltaTime ;
608+ const captureInterval = 0.12 - currentSpeedRatio * 0.07 ;
609+ if ( motionCaptureAccumulator < captureInterval ) return ;
610+ motionCaptureAccumulator = 0 ;
611+
612+ const frame = motionTrailFrames . length < MAX_MOTION_TRAIL_FRAMES
613+ ? document . createElement ( "canvas" )
614+ : motionTrailFrames . pop ( ) ;
615+ if ( frame . width !== canvas . width || frame . height !== canvas . height ) {
616+ frame . width = canvas . width ;
617+ frame . height = canvas . height ;
618+ }
619+ const frameContext = frame . getContext ( "2d" ) ;
620+ frameContext . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ;
621+ frameContext . clearRect ( 0 , 0 , frame . width , frame . height ) ;
622+ frameContext . drawImage ( canvas , 0 , 0 ) ;
623+ motionTrailFrames . unshift ( frame ) ;
624+ }
625+
626+ function drawWorldAfterimages ( ) {
627+ if ( motionTrailFrames . length === 0 || currentSpeedRatio < 0.08 ) return ;
628+ const layerCount = Math . min (
629+ motionTrailFrames . length ,
630+ Math . ceil ( currentSpeedRatio * MAX_MOTION_TRAIL_FRAMES ) ,
631+ ) ;
632+
546633 context . save ( ) ;
634+ context . globalCompositeOperation = "screen" ;
635+ for ( let layer = layerCount - 1 ; layer >= 0 ; layer -= 1 ) {
636+ const layerProgress = ( layer + 1 ) / ( layerCount + 1 ) ;
637+ context . globalAlpha =
638+ ( 0.012 + currentSpeedRatio * 0.022 ) * ( 1 - layerProgress * 0.5 ) ;
639+ context . drawImage ( motionTrailFrames [ layer ] , 0 , 0 , width , height ) ;
640+ }
641+ context . restore ( ) ;
642+ }
643+
644+ function applyWorldCamera ( ) {
547645 context . translate ( width / 2 , height / 2 ) ;
548- // The oblique projection tilts the complete game world while keeping the car centred.
549646 context . transform ( 1 , - 0.08 , worldTiltX , worldTiltY , 0 , 0 ) ;
550647 context . scale ( cameraZoom , cameraZoom ) ;
551648 context . translate ( - car . x , - car . y ) ;
649+ }
650+
651+ function drawScene ( deltaTime ) {
652+ context . clearRect ( 0 , 0 , width , height ) ;
653+
654+ // Draw and capture the moving world separately so the road and grass gain speed echoes.
655+ context . save ( ) ;
656+ applyWorldCamera ( ) ;
552657 drawGrass ( ) ;
553658 drawTrack ( ) ;
659+ context . restore ( ) ;
660+
661+ captureWorldMotionFrame ( deltaTime ) ;
662+ drawWorldAfterimages ( ) ;
663+
664+ // Keep the current F1 crisp while stacking speed-dependent ghosts behind it.
665+ context . save ( ) ;
666+ applyWorldCamera ( ) ;
667+ drawCarAfterimages ( ) ;
554668 drawCar ( ) ;
555669 context . restore ( ) ;
556670}
557671
558672function updateDemoControls ( ) {
559673 if ( ! demoMode ) return ;
674+ if ( demoMode === "speed-test" ) {
675+ controls . up = true ;
676+ controls . left = false ;
677+ controls . right = false ;
678+ return ;
679+ }
680+ if ( demoMode === "turn-test" ) {
681+ controls . up = true ;
682+ controls . left = false ;
683+ controls . right = elapsed > 2.5 && elapsed < 4.8 ;
684+ return ;
685+ }
560686 if ( demoMode === "continuous" ) {
561687 controls . up = true ;
562688 controls . left = false ;
@@ -579,6 +705,12 @@ function publishDiagnostics() {
579705 stage . dataset . topSpeedKmh = String ( TOP_SPEED_KMH ) ;
580706 stage . dataset . speedMultiplier = String ( SPEED_MULTIPLIER ) ;
581707 stage . dataset . accelerationProfile = "progressive" ;
708+ stage . dataset . turnSlowdown = turnSlowdownAmount . toFixed ( 3 ) ;
709+ stage . dataset . ghostLayers = String (
710+ currentSpeedRatio < 0.08 ? 0 : Math . ceil ( currentSpeedRatio * MAX_MOTION_TRAIL_FRAMES ) ,
711+ ) ;
712+ stage . dataset . worldGhostFrames = String ( motionTrailFrames . length ) ;
713+ stage . dataset . speedRatio = currentSpeedRatio . toFixed ( 3 ) ;
582714 stage . dataset . cameraZoom = String ( cameraZoom ) ;
583715 stage . dataset . worldTiltX = String ( worldTiltX ) ;
584716 stage . dataset . worldTiltY = String ( worldTiltY ) ;
@@ -597,7 +729,7 @@ function gameLoop(timestamp) {
597729 updateDemoControls ( ) ;
598730 updateCar ( deltaTime ) ;
599731 publishDiagnostics ( ) ;
600- drawScene ( ) ;
732+ drawScene ( deltaTime ) ;
601733 window . requestAnimationFrame ( gameLoop ) ;
602734}
603735
@@ -668,6 +800,11 @@ window.raceDebug = {
668800 topSpeedKmh : TOP_SPEED_KMH ,
669801 speedMultiplier : SPEED_MULTIPLIER ,
670802 accelerationProfile : "progressive" ,
803+ turnSlowdown : turnSlowdownAmount ,
804+ ghostLayers :
805+ currentSpeedRatio < 0.08 ? 0 : Math . ceil ( currentSpeedRatio * MAX_MOTION_TRAIL_FRAMES ) ,
806+ worldGhostFrames : motionTrailFrames . length ,
807+ speedRatio : currentSpeedRatio ,
671808 trackSamples : trackSamples . length ,
672809 } ) ,
673810} ;
0 commit comments