Skip to content

Commit 234c8f3

Browse files
committed
Add corner slowdown and speed afterimages
1 parent 272732d commit 234c8f3

2 files changed

Lines changed: 157 additions & 20 deletions

File tree

racing.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<meta name="description" content="帅气逼人吊炸天 · 漫画风 F1 赛车" />
77
<title>F1 赛车 · 帅气逼人吊炸天</title>
8-
<link rel="stylesheet" href="racing.css?v=7" />
8+
<link rel="stylesheet" href="racing.css?v=8" />
99
</head>
1010
<body>
1111
<main class="game-shell">
@@ -40,8 +40,8 @@ <h1>漫画 F1 极速赛</h1>
4040
</div>
4141
</section>
4242

43-
<p class="game-note">按住油门逐级加速,最高可达 376 KM/H;赛道边缘使用不可见空气墙</p>
43+
<p class="game-note">最高可达 639 KM/H;转弯会减速,高速时赛车、赛道与草坪会出现渐强虚影</p>
4444
</main>
45-
<script src="racing.js?v=7"></script>
45+
<script src="racing.js?v=8"></script>
4646
</body>
4747
</html>

racing.js

Lines changed: 154 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ const stage = document.querySelector("#gameStage");
44
const speedValue = document.querySelector("#speedValue");
55
const driveState = document.querySelector("#driveState");
66
const 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

1011
const controls = {
1112
up: false,
@@ -45,6 +46,11 @@ let airWallHits = 0;
4546
let cameraZoom = 1.65;
4647
let worldTiltX = 0.22;
4748
let worldTiltY = 0.7;
49+
let currentSpeedRatio = 0;
50+
let currentSteeringAmount = 0;
51+
let turnSlowdownAmount = 0;
52+
let motionTrailFrames = [];
53+
let motionCaptureAccumulator = 0;
4854
const demoMode = new URLSearchParams(window.location.search).get("demo");
4955

5056
const 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

121132
function 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

354387
function 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

558672
function 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

Comments
 (0)