Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="ui-container">
    <div id="input-section">
        <h2>Enter 20 Company Names (one per line):</h2>
        <textarea id="companyNames" rows="10" cols="40" placeholder="Company 1
Company 2
...
Company 20"></textarea>
        <button id="submitNames">Prepare Race</button>
    </div>

    <div id="controls-section" class="hidden">
        <button id="startRace">Start Race!</button>
        <button id="resetRace">Reset Race</button>
         <button id="changeNames">Change Names</button> <!-- Added -->
    </div>

     <div id="winner-display" class="hidden">
        <h2>Winners!</h2>
        <ol>
            <li id="winner1">1st: ---</li>
            <li id="winner2">2nd: ---</li>
            <li id="winner3">3rd: ---</li>
        </ol>
    </div>
</div>

<div id="raceContainer"></div>
              
            
!

CSS

              
                body {
    margin: 0;
    overflow: hidden;
    font-family: sans-serif;
    background-color: #222;
    color: #eee;
}

#raceContainer {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
}

#ui-container {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 2;
    background: rgba(0, 0, 0, 0.7);
    padding: 15px;
    border-radius: 8px;
    max-width: 350px; /* Limit width */
}

#input-section h2,
#winner-display h2 {
    margin-top: 0;
    color: #ffcc00; /* Gold color for headings */
}

#input-section textarea {
    width: 95%;
    margin-bottom: 10px;
    background: #333;
    color: #eee;
    border: 1px solid #555;
    padding: 5px;
}

button {
    padding: 10px 15px;
    margin: 5px 5px 5px 0;
    cursor: pointer;
    background-color: #007bff; /* Blue */
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 1em;
    transition: background-color 0.3s ease;
}

button:hover {
    background-color: #0056b3; /* Darker Blue */
}

#startRace {
     background-color: #28a745; /* Green */
}
#startRace:hover {
     background-color: #1e7e34; /* Darker Green */
}


#resetRace, #changeNames {
    background-color: #ffc107; /* Yellow/Orange */
    color: #333;
}
#resetRace:hover, #changeNames:hover {
    background-color: #d39e00; /* Darker Yellow/Orange */
}


#winner-display {
    margin-top: 20px;
    background: rgba(40, 167, 69, 0.85); /* Green background for winners */
    padding: 20px;
    border-radius: 8px;
    border: 2px solid #ffcc00;
    box-shadow: 0 0 15px rgba(255, 204, 0, 0.7);
}

#winner-display ol {
    list-style-type: none;
    padding: 0;
    margin: 0;
}

#winner-display li {
    font-size: 1.2em;
    margin-bottom: 10px;
    font-weight: bold;
    color: #fff;
    text-shadow: 1px 1px 2px black;
}


.hidden {
    display: none;
}
              
            
!

JS

              
                import * as THREE from 'https://cdn.skypack.dev/three@0.132.2'; // Or the latest version available on Skypack/CDN

// --- DOM Elements ---
const uiContainer = document.getElementById('ui-container');
const inputSection = document.getElementById('input-section');
const companyNamesInput = document.getElementById('companyNames');
const submitNamesButton = document.getElementById('submitNames');
const controlsSection = document.getElementById('controls-section');
const startRaceButton = document.getElementById('startRace');
const resetRaceButton = document.getElementById('resetRace');
const changeNamesButton = document.getElementById('changeNames'); // Added
const winnerDisplay = document.getElementById('winner-display');
const winnerElements = [
    document.getElementById('winner1'),
    document.getElementById('winner2'),
    document.getElementById('winner3'),
];
const raceContainer = document.getElementById('raceContainer');

// --- Three.js Setup ---
let scene, camera, renderer;
let racers = []; // Array to hold racer meshes
let companyNames = [];
let winners = []; // Array to hold the 3 winning racer objects

// --- Game State ---
let isRacePrepared = false;
let isRacing = false;
let raceOver = false;

// --- Race Config ---
const RACER_COUNT = 20;
const TRACK_LENGTH = 150;
const FINISH_LINE_Z = -TRACK_LENGTH;
const START_LINE_Z = 0;
const TRACK_WIDTH = 40; // How spread out racers are

function initThreeJS() {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x33334d); // Dark blueish background
    scene.fog = new THREE.Fog(0x33334d, TRACK_LENGTH * 0.5, TRACK_LENGTH * 1.2);

    const aspect = window.innerWidth / window.innerHeight;
    camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
    // Position camera to overlook the start, slightly elevated
    camera.position.set(0, 20, START_LINE_Z + 30);
    camera.lookAt(0, 0, FINISH_LINE_Z / 2); // Look towards middle of track

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true; // Enable shadows
    raceContainer.appendChild(renderer.domElement);

    // --- Lighting ---
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(50, 50, 20); // From above and side
    directionalLight.castShadow = true;
    // Configure shadow properties for performance
    directionalLight.shadow.mapSize.width = 1024;
    directionalLight.shadow.mapSize.height = 1024;
    directionalLight.shadow.camera.near = 0.5;
    directionalLight.shadow.camera.far = 500;
    directionalLight.shadow.camera.left = -TRACK_WIDTH * 1.5;
    directionalLight.shadow.camera.right = TRACK_WIDTH * 1.5;
    directionalLight.shadow.camera.top = 50;
    directionalLight.shadow.camera.bottom = -50;
    scene.add(directionalLight);

    // --- Track ---
    const trackGeometry = new THREE.PlaneGeometry(TRACK_WIDTH * 2, TRACK_LENGTH * 1.2); // Slightly longer than race distance
    const trackMaterial = new THREE.MeshStandardMaterial({ color: 0x556b2f, side: THREE.DoubleSide }); // Dark olive green
    const track = new THREE.Mesh(trackGeometry, trackMaterial);
    track.rotation.x = -Math.PI / 2; // Lay flat
    track.position.z = FINISH_LINE_Z / 2 + (START_LINE_Z - FINISH_LINE_Z) / 2 - TRACK_LENGTH * 0.1; // Center it
    track.receiveShadow = true;
    scene.add(track);

    // --- Finish Line ---
    const finishLineGeometry = new THREE.PlaneGeometry(TRACK_WIDTH * 1.1, 2);
    const finishLineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide });
    const finishLine = new THREE.Mesh(finishLineGeometry, finishLineMaterial);
    finishLine.rotation.x = -Math.PI / 2;
    finishLine.position.y = 0.1; // Slightly above track
    finishLine.position.z = FINISH_LINE_Z;
    scene.add(finishLine);

    // --- Event Listeners ---
    window.addEventListener('resize', onWindowResize, false);
    submitNamesButton.addEventListener('click', handleNameSubmit);
    startRaceButton.addEventListener('click', startRace);
    resetRaceButton.addEventListener('click', resetRaceVisuals);
    changeNamesButton.addEventListener('click', handleChangeNames); // Added

    // Start animation loop
    animate();
}

function handleNameSubmit() {
    const namesRaw = companyNamesInput.value.split('\n');
    companyNames = namesRaw.map(name => name.trim()).filter(name => name !== '');

    if (companyNames.length !== RACER_COUNT) {
        alert(`Please enter exactly ${RACER_COUNT} company names.`);
        return;
    }

    isRacePrepared = true;
    raceOver = false;
    inputSection.classList.add('hidden');
    controlsSection.classList.remove('hidden');
    winnerDisplay.classList.add('hidden'); // Hide winners if shown previously

    clearRacers(); // Remove old racers if any
    createRacers();
    resetRaceVisuals(); // Position racers at start
}

function handleChangeNames() {
     isRacePrepared = false;
     raceOver = true; // Prevent race logic
     isRacing = false;
     clearRacers();
     controlsSection.classList.add('hidden');
     winnerDisplay.classList.add('hidden');
     inputSection.classList.remove('hidden');
     companyNames = [];
     winners = [];
}

function createRacers() {
    const racerGeometry = new THREE.BoxGeometry(1.5, 1.5, 3); // Simple car-like shape

    for (let i = 0; i < RACER_COUNT; i++) {
        const color = new THREE.Color().setHSL(i / RACER_COUNT, 0.8, 0.6); // Vibrant colors
        const racerMaterial = new THREE.MeshStandardMaterial({ color: color });
        const racer = new THREE.Mesh(racerGeometry, racerMaterial);

        // Position racers spread out at the start line
        const posX = (i - (RACER_COUNT - 1) / 2) * (TRACK_WIDTH / RACER_COUNT) * 1.5; // Spread them
        racer.position.set(posX, 0.75, START_LINE_Z); // Y=0.75 so they sit on the track
        racer.castShadow = true;
        racer.receiveShadow = true;

        // Store company name and initial state
        racer.userData = {
            name: companyNames[i],
            baseSpeed: 0.3 + Math.random() * 0.2, // Base speed variation
            currentSpeed: 0,
            progress: 0, // How far along the track (0 to 1)
            isWinner: false, // Flag if this racer is a pre-selected winner
            finishPlace: -1 // 1, 2, or 3
        };

        racers.push(racer);
        scene.add(racer);
    }
}

function clearRacers() {
    racers.forEach(racer => {
        scene.remove(racer);
        // Properly dispose of geometry and material if needed for complex scenes
        // racer.geometry.dispose();
        // racer.material.dispose();
    });
    racers = [];
}

// Fisher-Yates (Knuth) Shuffle Algorithm
function shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]]; // Swap elements
    }
}

function selectWinners() {
    winners = []; // Clear previous winners
    racers.forEach(r => { r.userData.isWinner = false; r.userData.finishPlace = -1; }); // Reset winner status

    // Create an array of indices [0, 1, ..., 19]
    const indices = Array.from(Array(RACER_COUNT).keys());
    shuffleArray(indices);

    // Assign winners and slightly boost their base speed for visual guarantee
    for (let i = 0; i < 3; i++) {
        const winnerIndex = indices[i];
        const winnerRacer = racers[winnerIndex];
        winnerRacer.userData.isWinner = true;
        winnerRacer.userData.finishPlace = i + 1; // 1st, 2nd, 3rd
        winners.push(winnerRacer);

        // Give winners a slightly higher potential top speed for the animation
        // Make sure 1st place is slightly faster than 2nd, etc.
        winnerRacer.userData.baseSpeed = 0.55 + (3 - i) * 0.03 + Math.random() * 0.05;
    }

    // Optional: Adjust non-winners base speed slightly lower on average
     racers.forEach(racer => {
        if (!racer.userData.isWinner) {
            racer.userData.baseSpeed = 0.3 + Math.random() * 0.15; // Slightly lower max
        }
     });
}


function startRace() {
    if (!isRacePrepared || isRacing || raceOver) return;

    selectWinners(); // Select winners JUST before starting

    isRacing = true;
    raceOver = false;
    winnerDisplay.classList.add('hidden'); // Hide previous results
    startRaceButton.disabled = true; // Prevent clicking again
    resetRaceButton.disabled = true;
    changeNamesButton.disabled = true;

    // Reset speeds to 0 before starting
    racers.forEach(racer => {
        racer.userData.currentSpeed = 0;
    });
}

function resetRaceVisuals() {
    isRacing = false;
    raceOver = false; // Allow starting again

    racers.forEach((racer, i) => {
        const posX = (i - (RACER_COUNT - 1) / 2) * (TRACK_WIDTH / RACER_COUNT) * 1.5;
        racer.position.set(posX, 0.75, START_LINE_Z);
        racer.userData.progress = 0;
        racer.userData.currentSpeed = 0;
         // Keep isWinner and finishPlace from the last selection for display if needed immediately
    });

    winnerDisplay.classList.add('hidden'); // Hide winner display
    startRaceButton.disabled = false;
    resetRaceButton.disabled = false; // Enable reset button
    changeNamesButton.disabled = false;

    // Reset camera
    camera.position.set(0, 20, START_LINE_Z + 30);
    camera.lookAt(0, 0, FINISH_LINE_Z / 2);
}


function updateRace() {
    if (!isRacing) return;

    let finishedCount = 0;
    let leadZ = START_LINE_Z; // Track the Z position of the leader

    racers.forEach(racer => {
        if (racer.position.z > FINISH_LINE_Z) {
            // Add slight random fluctuation to speed
            const speedFluctuation = (Math.random() - 0.45) * 0.05; // Small random changes
            const targetSpeed = racer.userData.baseSpeed;

            // Accelerate up to base speed, then fluctuate
             if (racer.userData.currentSpeed < targetSpeed) {
                 racer.userData.currentSpeed += 0.01; // Simple acceleration
             } else {
                 racer.userData.currentSpeed += speedFluctuation;
             }
             // Clamp speed to prevent going backwards or excessively fast
             racer.userData.currentSpeed = Math.max(0.05, racer.userData.currentSpeed);
             racer.userData.currentSpeed = Math.min(targetSpeed * 1.2, racer.userData.currentSpeed); // Limit top speed

            racer.position.z -= racer.userData.currentSpeed; // Move forward (negative Z)
            racer.position.z = Math.max(FINISH_LINE_Z, racer.position.z); // Don't go past finish line

            racer.userData.progress = (START_LINE_Z - racer.position.z) / (START_LINE_Z - FINISH_LINE_Z);

            if (racer.position.z === FINISH_LINE_Z) {
                finishedCount++;
            }

             // Update leadZ for camera tracking
            if (racer.position.z < leadZ) {
                leadZ = racer.position.z;
            }

        } else {
            finishedCount++; // Already finished
        }
    });

     // --- Dynamic Camera (Simple Follow Lead) ---
    // Smoothly move camera Z position towards the leader + offset
    const targetCameraZ = Math.max(leadZ + 40, FINISH_LINE_Z + 20); // Follow leader but not too close, don't go past finish too far
    camera.position.z += (targetCameraZ - camera.position.z) * 0.02; // Lerp camera Z
    // Keep camera looking towards the finish line area
    const lookAtZ = Math.min(leadZ - 30, FINISH_LINE_Z / 2);
    camera.lookAt(0, 0, lookAtZ);


    // Check if all winners have finished - This is the condition to end the race visually
    const winnersFinished = winners.every(winnerRacer => winnerRacer.position.z <= FINISH_LINE_Z);

    if (winnersFinished && finishedCount >= 3) { // Check if at least 3 have finished AND the winners are among them
        isRacing = false;
        raceOver = true;
        displayWinners();
        resetRaceButton.disabled = false; // Re-enable reset
        changeNamesButton.disabled = false;
    }
}

function displayWinners() {
    // Sort winners array based on their finishPlace userData
    winners.sort((a, b) => a.userData.finishPlace - b.userData.finishPlace);

    winnerElements[0].textContent = `1st: ${winners[0]?.userData.name || '---'}`;
    winnerElements[1].textContent = `2nd: ${winners[1]?.userData.name || '---'}`;
    winnerElements[2].textContent = `3rd: ${winners[2]?.userData.name || '---'}`;

    winnerDisplay.classList.remove('hidden');
}

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate() {
    requestAnimationFrame(animate);

    if (isRacing) {
        updateRace();
    }

    renderer.render(scene, camera);
}

// --- Start Application ---
initThreeJS();
              
            
!
999px

Console