seawolf2357 commited on
Commit
924b98d
Β·
1 Parent(s): 034bdc8

Polish UI/UX: loading veil, pause/touch refinement, best-lap celebration

Browse files

- index.html: sunset-gradient loading veil covers the black canvas while
the three.js module resolves; faded out on the first animation frame.
- style.css: drop unloaded 'Racing Sans One' in favor of Orbitron, hide
keyboard hint on touch devices, scale HUD gauge/overlay on narrow
viewports so they no longer overlap the on-screen steering buttons,
subtle full-frame vignette, loading-veil sunset gradient + dots
keyframes, and the toastGoldIn keyframe used by new best-lap toast.
- js/game.js: fade loading veil out on first frame; backdrop blur on the
pause overlay; .pause-btn gets :active/focus-visible + hover lift;
.touch-btn gets tap-highlight removal, smoother transitions, brighter
.pressed state; showToast() gains a 'gold' variant used by the NEW
BEST LAP path for a scaled gold-glow celebration.

No gameplay/physics/logic changes.

Files changed (3) hide show
  1. index.html +5 -0
  2. js/game.js +67 -12
  3. style.css +151 -10
index.html CHANGED
@@ -22,5 +22,10 @@
22
  <canvas id="game"></canvas>
23
  <canvas id="minimap" width="160" height="160"></canvas>
24
  <div id="controls">WASD/ARROWS Β· SPACE drift Β· SHIFT nitro Β· C camera Β· G ghost Β· P/ESC pause Β· F fullscreen Β· R restart</div>
 
 
 
 
 
25
  </body>
26
  </html>
 
22
  <canvas id="game"></canvas>
23
  <canvas id="minimap" width="160" height="160"></canvas>
24
  <div id="controls">WASD/ARROWS Β· SPACE drift Β· SHIFT nitro Β· C camera Β· G ghost Β· P/ESC pause Β· F fullscreen Β· R restart</div>
25
+ <div id="loading-veil" role="status" aria-live="polite">
26
+ <div class="lv-title">SUNSET RACING</div>
27
+ <div class="lv-sub">LOADING TRACK</div>
28
+ <div class="lv-dots"><span></span><span></span><span></span></div>
29
+ </div>
30
  </body>
31
  </html>
js/game.js CHANGED
@@ -279,10 +279,12 @@ const pauseEl = document.createElement('div');
279
  pauseEl.id = 'pause-overlay';
280
  pauseEl.style.cssText = `
281
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
282
- background: rgba(0,0,0,0.75);
283
  display: none; align-items: center; justify-content: center;
284
  z-index: 400; pointer-events: auto;
285
  font-family: 'Orbitron', sans-serif;
 
 
286
  `;
287
  pauseEl.innerHTML = `
288
  <div style="text-align:center; color:white;">
@@ -312,11 +314,25 @@ pauseStyle.textContent = `
312
  letter-spacing: 2px;
313
  cursor: pointer;
314
  min-width: 280px;
315
- transition: all 0.15s;
 
 
316
  }
317
  .pause-btn:hover {
318
  background: rgba(255,34,0,0.3);
319
  border-color: #ff4422;
 
 
 
 
 
 
 
 
 
 
 
 
320
  }
321
  `;
322
  document.head.appendChild(pauseStyle);
@@ -367,22 +383,31 @@ if (isTouchDevice) {
367
  position: fixed;
368
  font-family: Orbitron, sans-serif;
369
  font-weight: 900;
370
- font-size: 24px;
371
- background: rgba(0,0,0,0.35);
372
- color: white;
373
- border: 2px solid rgba(255,255,255,0.4);
374
  border-radius: 50%;
375
  width: 72px; height: 72px;
376
  display: flex; align-items: center; justify-content: center;
377
  z-index: 150;
378
  touch-action: manipulation;
379
  user-select: none; -webkit-user-select: none;
 
380
  backdrop-filter: blur(6px);
381
  -webkit-backdrop-filter: blur(6px);
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
- .touch-btn.wide { width: 92px; border-radius: 12px; font-size: 18px; }
384
- .touch-btn:active { background: rgba(255,34,0,0.5); }
385
- .touch-btn.pressed { background: rgba(255,34,0,0.6); }
386
  `;
387
  document.head.appendChild(touchStyle);
388
 
@@ -423,10 +448,28 @@ toastEl.style.cssText = `
423
  `;
424
  document.body.appendChild(toastEl);
425
  let toastTimer = 0;
426
- function showToast(msg) {
427
  toastEl.textContent = msg;
428
  toastEl.style.opacity = '1';
429
- toastTimer = 1.2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  }
431
 
432
  // ══ COUNTDOWN OVERLAY ══
@@ -1135,7 +1178,7 @@ function update() {
1135
  const saved = saveGhost(currentLapSamples, lastLapTime);
1136
  if (saved) ghostData = loadGhost();
1137
  }
1138
- showToast(`NEW BEST LAP! ${formatRaceTime(lastLapTime)}`);
1139
  }
1140
 
1141
  // Reset for next lap
@@ -1401,10 +1444,22 @@ function update() {
1401
  }
1402
  }
1403
 
 
 
 
 
 
 
 
 
 
 
 
1404
  function animate() {
1405
  requestAnimationFrame(animate);
1406
  update();
1407
  renderer.render(scene, camera);
 
1408
  }
1409
 
1410
  animate();
 
279
  pauseEl.id = 'pause-overlay';
280
  pauseEl.style.cssText = `
281
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
282
+ background: rgba(0,0,0,0.72);
283
  display: none; align-items: center; justify-content: center;
284
  z-index: 400; pointer-events: auto;
285
  font-family: 'Orbitron', sans-serif;
286
+ backdrop-filter: blur(10px) saturate(120%);
287
+ -webkit-backdrop-filter: blur(10px) saturate(120%);
288
  `;
289
  pauseEl.innerHTML = `
290
  <div style="text-align:center; color:white;">
 
314
  letter-spacing: 2px;
315
  cursor: pointer;
316
  min-width: 280px;
317
+ text-transform: uppercase;
318
+ outline: none;
319
+ transition: background 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s;
320
  }
321
  .pause-btn:hover {
322
  background: rgba(255,34,0,0.3);
323
  border-color: #ff4422;
324
+ transform: translateY(-1px);
325
+ box-shadow: 0 6px 18px rgba(255,34,0,0.25);
326
+ }
327
+ .pause-btn:active {
328
+ background: rgba(255,34,0,0.55);
329
+ border-color: #ff6644;
330
+ transform: translateY(0);
331
+ box-shadow: 0 2px 8px rgba(255,34,0,0.3);
332
+ }
333
+ .pause-btn:focus-visible {
334
+ border-color: #00eaff;
335
+ box-shadow: 0 0 0 3px rgba(0,234,255,0.25);
336
  }
337
  `;
338
  document.head.appendChild(pauseStyle);
 
383
  position: fixed;
384
  font-family: Orbitron, sans-serif;
385
  font-weight: 900;
386
+ font-size: 26px;
387
+ background: rgba(0,0,0,0.38);
388
+ color: #fff;
389
+ border: 2px solid rgba(255,255,255,0.45);
390
  border-radius: 50%;
391
  width: 72px; height: 72px;
392
  display: flex; align-items: center; justify-content: center;
393
  z-index: 150;
394
  touch-action: manipulation;
395
  user-select: none; -webkit-user-select: none;
396
+ -webkit-tap-highlight-color: transparent;
397
  backdrop-filter: blur(6px);
398
  -webkit-backdrop-filter: blur(6px);
399
+ text-shadow: 0 2px 6px rgba(0,0,0,0.7);
400
+ box-shadow: 0 4px 14px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.1);
401
+ transition: transform 0.08s ease-out, background 0.08s ease-out, border-color 0.12s ease-out;
402
+ }
403
+ .touch-btn.wide { width: 96px; border-radius: 14px; font-size: 16px; letter-spacing: 2px; }
404
+ .touch-btn:active { background: rgba(255,34,0,0.5); transform: scale(0.94); }
405
+ .touch-btn.pressed {
406
+ background: rgba(255,34,0,0.65);
407
+ border-color: #ffb0a0;
408
+ transform: scale(0.92);
409
+ box-shadow: 0 0 22px rgba(255,68,34,0.6), inset 0 1px 0 rgba(255,255,255,0.15);
410
  }
 
 
 
411
  `;
412
  document.head.appendChild(touchStyle);
413
 
 
448
  `;
449
  document.body.appendChild(toastEl);
450
  let toastTimer = 0;
451
+ function showToast(msg, variant) {
452
  toastEl.textContent = msg;
453
  toastEl.style.opacity = '1';
454
+ // Reset any gold animation/class left over from a previous best-lap toast
455
+ toastEl.classList.remove('toast-gold');
456
+ // offset reflow so re-adding the class restarts the keyframes
457
+ void toastEl.offsetWidth;
458
+ if (variant === 'gold') {
459
+ toastEl.style.color = '#ffd700';
460
+ toastEl.style.fontSize = '42px';
461
+ toastEl.style.letterSpacing = '3px';
462
+ toastEl.style.textShadow = '0 0 36px rgba(255,215,0,0.9), 0 4px 14px rgba(0,0,0,0.9)';
463
+ toastEl.style.animation = 'toastGoldIn 2s ease-out forwards';
464
+ toastTimer = 2;
465
+ } else {
466
+ toastEl.style.color = '#fff';
467
+ toastEl.style.fontSize = '28px';
468
+ toastEl.style.letterSpacing = '0';
469
+ toastEl.style.textShadow = '0 0 20px rgba(255,255,255,0.6), 0 2px 8px rgba(0,0,0,0.9)';
470
+ toastEl.style.animation = 'none';
471
+ toastTimer = 1.2;
472
+ }
473
  }
474
 
475
  // ══ COUNTDOWN OVERLAY ══
 
1178
  const saved = saveGhost(currentLapSamples, lastLapTime);
1179
  if (saved) ghostData = loadGhost();
1180
  }
1181
+ showToast(`NEW BEST LAP! ${formatRaceTime(lastLapTime)}`, 'gold');
1182
  }
1183
 
1184
  // Reset for next lap
 
1444
  }
1445
  }
1446
 
1447
+ let loadingVeilHidden = false;
1448
+ function hideLoadingVeil() {
1449
+ if (loadingVeilHidden) return;
1450
+ const veil = document.getElementById('loading-veil');
1451
+ if (!veil) { loadingVeilHidden = true; return; }
1452
+ veil.classList.add('hidden');
1453
+ // Remove from DOM after the fade-out transition so it can't steal clicks.
1454
+ setTimeout(() => { veil.remove(); }, 700);
1455
+ loadingVeilHidden = true;
1456
+ }
1457
+
1458
  function animate() {
1459
  requestAnimationFrame(animate);
1460
  update();
1461
  renderer.render(scene, camera);
1462
+ if (!loadingVeilHidden) hideLoadingVeil();
1463
  }
1464
 
1465
  animate();
style.css CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  * {
2
  margin: 0;
3
  padding: 0;
@@ -9,6 +16,22 @@ html, body {
9
  height: 100%;
10
  overflow: hidden;
11
  background: #000;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
 
14
  #game {
@@ -17,23 +40,141 @@ html, body {
17
  height: 100%;
18
  }
19
 
20
- /* Canvas HUD handles its own positioning */
21
- #hud-canvas {
 
 
22
  image-rendering: auto;
23
  }
24
 
25
- /* minimap styles handled in js/minimap.js */
26
-
27
  #controls {
28
  position: fixed;
29
- bottom: 10px;
30
  left: 50%;
31
  transform: translateX(-50%);
32
- font-family: 'Racing Sans One', 'Impact', 'Arial Black', sans-serif;
33
- font-size: 13px;
 
34
  color: #fff;
35
- opacity: 0.35;
36
- text-shadow: 0 1px 4px rgba(0,0,0,0.8);
37
  pointer-events: none;
38
- letter-spacing: 2px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
 
1
+ /* ═══════════════════════════════════════════════════════
2
+ * SUNSET RACING β€” base styles
3
+ * Runtime UI (pause menu, touch buttons, toasts, attract
4
+ * overlay, countdown) is styled by js/game.js via injected
5
+ * <style> tags. This file holds the static surfaces only.
6
+ * ═══════════════════════════════════════════════════════ */
7
+
8
  * {
9
  margin: 0;
10
  padding: 0;
 
16
  height: 100%;
17
  overflow: hidden;
18
  background: #000;
19
+ -webkit-font-smoothing: antialiased;
20
+ }
21
+
22
+ /* Subtle cinematic vignette β€” tightens the frame, adds depth
23
+ behind the HUD without affecting the 3D canvas brightness. */
24
+ body::after {
25
+ content: '';
26
+ position: fixed;
27
+ inset: 0;
28
+ pointer-events: none;
29
+ z-index: 100;
30
+ background: radial-gradient(
31
+ ellipse at center,
32
+ transparent 55%,
33
+ rgba(0, 0, 0, 0.38) 100%
34
+ );
35
  }
36
 
37
  #game {
 
40
  height: 100%;
41
  }
42
 
43
+ /* HUD canvases are positioned by js/hud.js via cssText;
44
+ we only hint the browser about scaling. */
45
+ #hud-canvas,
46
+ #hud-overlay {
47
  image-rendering: auto;
48
  }
49
 
50
+ /* Keyboard hint strip at the bottom of the screen. */
 
51
  #controls {
52
  position: fixed;
53
+ bottom: 12px;
54
  left: 50%;
55
  transform: translateX(-50%);
56
+ font-family: 'Orbitron', sans-serif;
57
+ font-weight: 700;
58
+ font-size: 12px;
59
  color: #fff;
60
+ opacity: 0.45;
61
+ text-shadow: 0 1px 6px rgba(0, 0, 0, 0.9);
62
  pointer-events: none;
63
+ letter-spacing: 3px;
64
+ white-space: nowrap;
65
+ text-transform: uppercase;
66
+ z-index: 140;
67
+ transition: opacity 0.3s ease;
68
+ }
69
+
70
+ #controls:hover { opacity: 0.75; }
71
+
72
+ /* Hide the keyboard hint when running on a touch device β€” the
73
+ on-screen touch buttons serve as the control reference there,
74
+ and the hint string otherwise collides with them. */
75
+ @media (hover: none) and (pointer: coarse) {
76
+ #controls { display: none; }
77
+ }
78
+
79
+ /* On narrow viewports the hint wraps messily; shrink it. */
80
+ @media (max-width: 720px) {
81
+ #controls {
82
+ font-size: 10px;
83
+ letter-spacing: 2px;
84
+ white-space: normal;
85
+ max-width: 92vw;
86
+ text-align: center;
87
+ }
88
+ }
89
+
90
+ /* Scale the bottom-left HUD gauge down on phones so it no longer
91
+ overlaps the on-screen steering buttons. */
92
+ @media (max-width: 640px) {
93
+ #hud-canvas {
94
+ transform: scale(0.68);
95
+ transform-origin: bottom left;
96
+ }
97
+ #hud-overlay {
98
+ transform: scale(0.85);
99
+ transform-origin: top left;
100
+ }
101
+ }
102
+
103
+ /* ═══════════════════════════════════════════════════════
104
+ * Loading veil β€” shown while game.js initializes
105
+ * (three.js modules, scenery generation). Faded out on
106
+ * the first animation frame by js/game.js.
107
+ * ═══════════════════════════════════════════════════════ */
108
+ #loading-veil {
109
+ position: fixed;
110
+ inset: 0;
111
+ z-index: 900;
112
+ display: flex;
113
+ flex-direction: column;
114
+ align-items: center;
115
+ justify-content: center;
116
+ gap: 28px;
117
+ pointer-events: auto;
118
+ background:
119
+ radial-gradient(ellipse at 50% 120%, #ff6a3d 0%, #8a1e3c 32%, #2a0a2a 62%, #050510 100%);
120
+ font-family: 'Orbitron', sans-serif;
121
+ color: #fff;
122
+ opacity: 1;
123
+ transition: opacity 0.6s ease-out;
124
+ }
125
+
126
+ #loading-veil.hidden {
127
+ opacity: 0;
128
+ pointer-events: none;
129
+ }
130
+
131
+ #loading-veil .lv-title {
132
+ font-weight: 900;
133
+ font-size: clamp(32px, 9vw, 68px);
134
+ letter-spacing: clamp(4px, 1vw, 10px);
135
+ text-shadow:
136
+ 0 0 40px rgba(255, 160, 80, 0.8),
137
+ 0 4px 24px rgba(0, 0, 0, 0.9);
138
+ text-align: center;
139
+ }
140
+
141
+ #loading-veil .lv-sub {
142
+ font-weight: 700;
143
+ font-size: clamp(11px, 2vw, 14px);
144
+ letter-spacing: 4px;
145
+ color: rgba(255, 255, 255, 0.82);
146
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
147
+ }
148
+
149
+ #loading-veil .lv-dots {
150
+ display: flex;
151
+ gap: 10px;
152
+ }
153
+ #loading-veil .lv-dots span {
154
+ width: 10px;
155
+ height: 10px;
156
+ border-radius: 50%;
157
+ background: #fff;
158
+ box-shadow: 0 0 12px rgba(255, 255, 255, 0.8);
159
+ animation: lvDot 1.1s ease-in-out infinite;
160
+ }
161
+ #loading-veil .lv-dots span:nth-child(2) { animation-delay: 0.15s; }
162
+ #loading-veil .lv-dots span:nth-child(3) { animation-delay: 0.3s; }
163
+
164
+ @keyframes lvDot {
165
+ 0%, 100% { transform: translateY(0); opacity: 0.35; }
166
+ 50% { transform: translateY(-8px); opacity: 1; }
167
+ }
168
+
169
+ /* ═══════════════════════════════════════════════════════
170
+ * Gold-toast keyframe for the "NEW BEST LAP!" celebration.
171
+ * The toast element gets the .toast-gold class applied in
172
+ * js/game.js, which pulls this animation in.
173
+ * ═══════════════════════════════════════════════════════ */
174
+ @keyframes toastGoldIn {
175
+ 0% { transform: translate(-50%, -50%) scale(0.6); opacity: 0; }
176
+ 22% { transform: translate(-50%, -50%) scale(1.18); opacity: 1; }
177
+ 40% { transform: translate(-50%, -50%) scale(1.00); opacity: 1; }
178
+ 85% { opacity: 1; }
179
+ 100% { opacity: 0; }
180
  }