Documentation Index
Fetch the complete documentation index at: https://mintlify.com/JustADev1024/threejs-car/llms.txt
Use this file to discover all available pages before exploring further.
Dream Car’s entire control surface is a single #controls <div> that floats at the bottom of the screen over the 3D canvas. It is built from plain HTML elements styled to look like a dark glass panel, with no frontend framework. Every control wires directly to a JavaScript handler defined in the same <script type="module"> block.
Panel Layout and Styling
The #controls container uses a glass-morphism appearance: a dark semi-transparent background, blurred backdrop, rounded corners, and a thin white border.
#controls {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(12px);
border-radius: 28px;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 100;
border: 1px solid rgba(255,255,255,0.2);
font-size: 14px;
max-height: 45%;
overflow-y: auto;
}
Controls are arranged in .row flex containers that wrap naturally on narrow screens. At viewport widths ≤ 700 px every interactive element expands to fill available width:
@media (max-width: 700px) {
.row { flex-wrap: wrap; justify-content: stretch; }
button, select, input, .slider { flex: 1; min-width: 80px; }
}
Language Switcher
Elements: #langEn, #langRu — both carry class lang-btn.
<div class="row" id="langRow">
<span id="langLabel">Language:</span>
<button id="langEn" class="lang-btn active">EN</button>
<button id="langRu" class="lang-btn">RU</button>
</div>
The active language button is highlighted in orange (#ff6600); inactive buttons use dark grey (#444). The class toggle is managed by updateUI():
.lang-btn { background: #444; }
.lang-btn.active { background: #ff6600; }
Handlers:
langEnBtn.addEventListener('click', () => setLanguage('en'));
langRuBtn.addEventListener('click', () => setLanguage('ru'));
setLanguage(lang) checks whether the language has actually changed before calling updateUI(), which re-renders every translatable string without a page reload. See Internationalization for the full translation system.
Time of Day Slider
Element: #timeSlider with display #timeDisplay.
<div class="row">
<span id="timeLabel">Time of day</span>
<input type="range" id="timeSlider" min="0" max="24" step="0.1" value="14" class="slider">
<span id="timeDisplay">14:00</span>
</div>
| Attribute | Value |
|---|
min | 0 (midnight) |
max | 24 (next midnight) |
step | 0.1 (6-minute increments) |
value | 14 (2:00 PM default) |
Handler:
timeSlider.addEventListener('input', e => updateTime(parseFloat(e.target.value)));
updateTime(14); // called once on load
updateTime(hour) formats the display label, repositions the sun and moon meshes along their arc, and interpolates the scene background and fog colour from night-blue to sky-blue:
function updateTime(hour) {
const h = Math.floor(hour);
const m = Math.floor((hour % 1) * 60);
timeDisplay.textContent =
`${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
const angle = (hour / 12) * Math.PI;
// ... sun/moon position, light intensity, sky colour blend
}
Element: #lightsBtn
<button id="lightsBtn">💡 Headlights Off</button>
State: lightsOn boolean, initialised to true (headlights begin on).
lightsBtn.addEventListener('click', () => {
lightsOn = !lightsOn;
headlightModel.material.emissiveIntensity = lightsOn ? 1.2 : 0;
headlightSpot.intensity = lightsOn ? 1.5 : 0;
headlightPoint.intensity = lightsOn ? 0.8 : 0;
updateLightsButton();
});
updateLightsButton() sets the label and background tint from the current language:
function updateLightsButton() {
lightsBtn.textContent = lightsOn ? t('lightsOn') : t('lightsOff');
lightsBtn.style.background = lightsOn ? '#ff6600' : '#444';
}
| State | Label (EN) | Background |
|---|
| On | 💡 Headlights On | #ff6600 |
| Off | 💡 Headlights Off | #444 |
Three scene objects are affected simultaneously: the visible headlight lens mesh (headlightModel), the SpotLight (headlightSpot), and the PointLight (headlightPoint). All three are children of the car group.
Element: #exhaustBtn
<button id="exhaustBtn">💨 Exhaust Off</button>
State: exhaustOn boolean and exhaustOnState boolean (both track the same toggle; exhaustOnState is read by the particle loop).
exhaustBtn.addEventListener('click', () => {
exhaustOn = !exhaustOn;
exhaustOnState = exhaustOn;
updateExhaustButton();
});
function updateExhaustButton() {
exhaustBtn.textContent = exhaustOn ? t('exhaustOn') : t('exhaustOff');
exhaustBtn.style.background = exhaustOn ? '#ff6600' : '#444';
}
When exhaustOnState is true, particles.visible is set to true and the particle buffer is animated each frame. When false, the Points mesh is hidden and the loop idles. See Car Model — Exhaust Particle System for the full particle implementation.
Elements: #trackSelect (a <select>) and #musicBtn.
<div class="row">
<span>🎵</span>
<select id="trackSelect"></select>
<button id="musicBtn">🎵 Play</button>
</div>
trackSelect is populated at startup (and again on every language change) by buildTrackOptions(). There are 16 preset SoundHelix tracks; custom tracks added by the user appear below them.
Music button handler:
musicBtn.addEventListener('click', toggleMusic);
function toggleMusic() {
if (musicPlaying) {
stopMusic();
} else {
const selectedUrl = trackSelect.value;
if (!selectedUrl) {
alert(t('alertNoTrack'));
return;
}
playTrack(selectedUrl);
}
}
playTrack(url) creates a new Audio object, sets loop = true and volume = 0.5, and calls .play(). If the browser rejects playback (e.g. CORS failure), musicPlaying is reset to false and an alert is shown.
Track select change handler — switching tracks while music is playing seamlessly stops the current audio and starts the new one:
trackSelect.addEventListener('change', () => {
if (musicPlaying) {
const newUrl = trackSelect.value;
if (newUrl) {
stopMusic();
playTrack(newUrl);
} else {
stopMusic();
}
}
});
| Music state | Button label (EN) | Background |
|---|
| Stopped | 🎵 Play | #ff6600 |
| Playing | 🔇 Stop | #444 |
Elements: #customUrl (text input) and #addTrackBtn.
<div class="row">
<input type="text" id="customUrl"
placeholder="➕ Direct MP3 link (from Newgrounds or other site)"
style="flex: 2;">
<button id="addTrackBtn">➕ Add</button>
</div>
The placeholder text is re-set on every language change via updateUI().
Handler:
addTrackBtn.addEventListener('click', addCustomTrack);
function addCustomTrack() {
let url = customUrlInput.value.trim();
if (!url) {
alert(t('alertEnterUrl'));
return;
}
if (!url.match(/\.mp3($|\?)/i)) {
alert(t('alertInvalidUrl'));
return;
}
// If URL already exists, select it and optionally start playing
if (trackList.some(t => t.url === url)) {
trackSelect.value = url;
customUrlInput.value = '';
if (musicPlaying) { stopMusic(); playTrack(url); }
else alert(t('alertTrackAdded'));
return;
}
const newTrack = {
url,
isCustom: true,
customLabel: url.substring(0, 40) + (url.length > 40 ? '…' : ''),
index: trackList.length
};
trackList.push(newTrack);
buildTrackOptions();
trackSelect.value = url;
customUrlInput.value = '';
if (musicPlaying) { stopMusic(); playTrack(url); }
else alert(t('alertTrackAdded'));
}
Validation rule: The URL must match /\.mp3($|\?)/i — it must end with .mp3 or .mp3?querystring. Anything else triggers alertInvalidUrl.
Browser autoplay and CORS policies apply. The audio source server must include permissive Access-Control-Allow-Origin headers, or the browser will block playback silently. Direct links from services like Newgrounds or SoundHelix work; links to streaming pages do not.