HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
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.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
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.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
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.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<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>
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;
}
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();
Also see: Tab Triggers