“Colr” is an engaging color perception and matching game that challenges players to replicate a target color using an interactive color picker. The game combines visual accuracy with time-based scoring, testing players’ color perception skills while providing immediate feedback through a sophisticated scoring system. Built with modern web technologies and featuring responsive design, it offers an intuitive color matching experience.
colr/
└── index.html # Complete self-contained color matching game
// Professional color picker library
<script src="https://cdn.jsdelivr.net/npm/@jaames/iro@5"></script>
// Advanced color picker initialization
this.colorPicker = new iro.ColorPicker('#colorPicker', {
width: pickerWidth, // Responsive width
color: { h: 0, s: 100, l: 50 }, // Initial HSL color
layout: [
{
component: iro.ui.Wheel, // Color wheel for hue/saturation
options: {}
},
{
component: iro.ui.Slider, // Brightness/lightness slider
options: {
sliderType: 'value' // Lightness control
}
}
]
});
<!-- Modern, readable font family -->
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
class ColourGeoGuesser {
constructor() {
this.targetColour = null; // Current challenge color
this.colorPicker = null; // Iro.js picker instance
this.startTime = null; // Round start timestamp
this.isPlaying = false; // Game state flag
this.bestScore = parseInt(localStorage.getItem('colourGuesserBest') || '0');
// Initialize all game systems
this.initializeElements();
this.initializeColorPicker();
this.setupEventListeners();
this.updateBestScore();
this.startNewGame();
}
}
initializeElements() {
// Centralized DOM element references
this.elements = {
targetColour: document.getElementById('targetColour'),
timer: document.getElementById('timer'),
selectedColour: document.getElementById('selectedColour'),
guessButton: document.getElementById('guessButton'),
results: document.getElementById('results'),
targetSample: document.getElementById('targetSample'),
guessSample: document.getElementById('guessSample'),
scoreValue: document.getElementById('scoreValue'),
accuracyScore: document.getElementById('accuracyScore'),
timeBonus: document.getElementById('timeBonus'),
timeTaken: document.getElementById('timeTaken'),
nextButton: document.getElementById('nextButton'),
bestScore: document.getElementById('bestScore')
};
}
generateRandomColour() {
// Sophisticated color generation favoring lighter, more pleasant colors
const h = Math.floor(Math.random() * 360); // Full hue range
const s = Math.floor(Math.random() * 100); // Full saturation range
// Strategic lightness weighting (60-95% range)
const l = Math.floor(Math.random() * 35) + 60; // Lighter colors for better UX
// Convert HSL to RGB for accurate comparison
const rgb = this.hslToRgb(h, s, l);
return {
r: rgb.r,
g: rgb.g,
b: rgb.b,
hex: `#${rgb.r.toString(16).padStart(2, '0')}${rgb.g.toString(16).padStart(2, '0')}${rgb.b.toString(16).padStart(2, '0')}`
};
}
hslToRgb(h, s, l) {
s /= 100; // Normalize saturation
l /= 100; // Normalize lightness
const c = (1 - Math.abs(2 * l - 1)) * s; // Chroma calculation
const x = c * (1 - Math.abs((h / 60) % 2 - 1)); // Intermediate value
const m = l - c / 2; // Lightness adjustment
let r, g, b;
// Hue-based RGB component calculation
if (0 <= h && h < 60) {
r = c; g = x; b = 0;
} else if (60 <= h && h < 120) {
r = x; g = c; b = 0;
} else if (120 <= h && h < 180) {
r = 0; g = c; b = x;
} else if (180 <= h && h < 240) {
r = 0; g = x; b = c;
} else if (240 <= h && h < 300) {
r = x; g = 0; b = c;
} else if (300 <= h && h < 360) {
r = c; g = 0; b = x;
}
// Convert to 0-255 RGB values
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255)
};
}
calculateScore(distance, timeElapsed) {
// Sophisticated accuracy scoring with distance cutoff
const maxDistance = Math.sqrt(255 * 255 * 3); // Maximum RGB distance
const cutoffDistance = maxDistance * 0.2; // 20% tolerance threshold
let accuracyScore = 0;
if (distance <= cutoffDistance) {
// Inverse exponential scoring for intuitive feedback
const normalizedDistance = distance / cutoffDistance;
accuracyScore = Math.round(100 * Math.pow(1 - normalizedDistance, 0.8));
}
// Time bonus encourages quick decisions (0-20 points)
const timeBonus = Math.max(0, Math.round(20 - timeElapsed));
return {
accuracy: accuracyScore,
timeBonus: timeBonus,
total: accuracyScore + timeBonus // Maximum: 120 points
};
}
calculateColourDistance(color1, color2) {
// 3D Euclidean distance in RGB color space
const dr = color1.r - color2.r; // Red component difference
const dg = color1.g - color2.g; // Green component difference
const db = color1.b - color2.b; // Blue component difference
return Math.sqrt(dr * dr + dg * dg + db * db); // Distance magnitude
}
// Real-time color picker event handling
this.colorPicker.on('color:change', (color) => {
this.updateButtonColor(color);
});
this.colorPicker.on('input:change', (color) => {
this.updateButtonColor(color); // Instant feedback during interaction
});
updateButtonColor(color) {
// Store current selection invisibly
this.elements.selectedColour.style.backgroundColor = color.hexString;
// Dynamic text color for accessibility
const lightness = color.hsl.l;
const textColor = lightness > 50 ? '#000000' : '#ffffff';
// Smooth visual updates with requestAnimationFrame
requestAnimationFrame(() => {
this.elements.guessButton.style.background = color.hexString;
this.elements.guessButton.style.color = textColor;
});
}
initializeColorPicker() {
// Adaptive sizing based on screen dimensions
const isMobile = window.innerWidth <= 768;
const pickerWidth = isMobile ? 280 : 400;
// Mobile-optimized configuration
this.colorPicker = new iro.ColorPicker('#colorPicker', {
width: pickerWidth,
// ... picker configuration
});
}
startNewGame() {
this.targetColour = this.generateRandomColour();
this.startTime = Date.now(); // High-precision timing
this.isPlaying = true;
this.elements.targetColour.style.backgroundColor = this.targetColour.hex;
this.elements.results.classList.remove('show');
this.elements.guessButton.disabled = false;
// Reset picker to neutral white
this.colorPicker.color.set({ h: 0, s: 0, l: 100 });
this.updateTimer();
// High-frequency timer updates (100ms intervals)
this.timerInterval = setInterval(() => this.updateTimer(), 100);
}
updateTimer() {
if (!this.isPlaying) return;
const elapsed = (Date.now() - this.startTime) / 1000;
this.elements.timer.textContent = `Time: ${elapsed.toFixed(1)}s`;
}
makeGuess() {
if (!this.isPlaying) return;
this.isPlaying = false;
clearInterval(this.timerInterval); // Stop timing
const timeElapsed = (Date.now() - this.startTime) / 1000;
const guessColor = this.colorPicker.color;
// Extract RGB values for comparison
const guessRgb = { r: guessColor.rgb.r, g: guessColor.rgb.g, b: guessColor.rgb.b };
// Calculate accuracy and scoring
const distance = this.calculateColourDistance(this.targetColour, guessRgb);
const score = this.calculateScore(distance, timeElapsed);
// Best score tracking with persistence
if (score.total > this.bestScore) {
this.bestScore = score.total;
localStorage.setItem('colourGuesserBest', this.bestScore.toString());
this.updateBestScore();
}
this.showResults(guessRgb, score, timeElapsed);
}
/* CSS Custom Properties for consistent theming */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: #fafafa; /* Light neutral background */
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
/* Grid-based responsive layout */
.game-container {
max-width: 1200px;
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr; /* Two-column desktop layout */
gap: 60px;
align-items: center;
position: relative;
}
.target-colour {
width: 300px;
height: 300px;
border-radius: 20px; /* Rounded corners */
margin: 0 auto 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); /* Depth shadow */
transition: all 0.3s ease; /* Smooth color transitions */
border: 4px solid rgba(255, 255, 255, 0.5); /* Subtle border */
}
.game-button {
background: linear-gradient(135deg, #667eea, #764ba2); /* Gradient background */
color: white;
border: none;
padding: 16px 32px;
border-radius: 12px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
min-width: 140px;
}
.game-button:hover {
transform: translateY(-2px); /* Lift effect */
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
/* Fixed positioning for mobile optimization */
.guess-button-fixed {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
border-radius: 50px;
padding: 16px 40px;
z-index: 1000;
}
.results {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
backdrop-filter: blur(10px); /* Modern blur background */
-webkit-backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.1);
display: none;
z-index: 2000;
align-items: center;
justify-content: center;
}
.results-content {
background: rgba(255, 255, 255, 0.95);
padding: 40px;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
/* Smooth entrance animation */
.results.show {
display: flex;
animation: slideIn 0.5s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
showResults(guessRgb, score, timeElapsed) {
// Visual color comparison
this.elements.targetSample.style.backgroundColor = this.targetColour.hex;
this.elements.guessSample.style.backgroundColor =
`rgb(${guessRgb.r}, ${guessRgb.g}, ${guessRgb.b})`;
// Detailed score breakdown
this.elements.scoreValue.textContent = score.total;
this.elements.accuracyScore.textContent = score.accuracy;
this.elements.timeBonus.textContent = score.timeBonus;
this.elements.timeTaken.textContent = timeElapsed.toFixed(1) + 's';
this.elements.guessButton.disabled = true;
this.elements.results.classList.add('show');
// Store results for potential sharing
this.lastScore = {
total: score.total,
target: this.targetColour.hex,
time: timeElapsed.toFixed(1)
};
}
@media (max-width: 768px) {
body {
padding: 0;
justify-content: flex-start;
align-items: stretch;
}
.game-container {
display: flex;
flex-direction: column; /* Stack vertically on mobile */
height: 100vh;
max-width: none;
width: 100%;
gap: 0;
margin: 0;
padding: 15px;
justify-content: space-between;
}
/* Responsive color picker sizing */
#colorPicker {
width: 280px !important;
}
#colorPicker .IroColorPicker {
width: 280px !important;
}
#colorPicker .IroWheel {
width: 280px !important;
height: 280px !important;
}
#colorPicker .IroSlider {
width: 280px !important;
height: 20px !important;
margin-top: 15px;
}
/* Mobile-optimized button */
.guess-button-fixed {
position: fixed;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 30px);
max-width: 300px;
z-index: 1000;
}
}
<!-- Tooltip system for scoring explanation -->
<div class="info-tooltip">
<div class="info-icon">?</div>
<div class="tooltip-content">
<strong>Scoring System:</strong><br>
• Accuracy: Up to 100 points based on color similarity<br>
• Time Bonus: Up to 20 points (faster = more points)<br>
• Perfect Match: 120 points maximum
</div>
</div>
.tooltip-content {
visibility: hidden;
width: 300px;
background: #2d3748;
color: white;
text-align: left;
border-radius: 8px;
padding: 12px;
position: absolute;
z-index: 1000;
bottom: 125%;
left: 50%;
margin-left: -150px;
opacity: 0;
transition: all 0.3s;
font-size: 0.9rem;
line-height: 1.4;
}
.info-tooltip:hover .tooltip-content {
visibility: visible;
opacity: 1;
}
// Constructor initialization
this.bestScore = parseInt(localStorage.getItem('colourGuesserBest') || '0');
// Score update and persistence
if (score.total > this.bestScore) {
this.bestScore = score.total;
localStorage.setItem('colourGuesserBest', this.bestScore.toString());
this.updateBestScore();
}
updateBestScore() {
this.elements.bestScore.textContent = `Best: ${this.bestScore}`;
}
// RequestAnimationFrame for smooth color updates
updateButtonColor(color) {
requestAnimationFrame(() => {
this.elements.guessButton.style.background = color.hexString;
this.elements.guessButton.style.color = textColor;
});
}
Colr represents an exceptionally well-executed color perception game that successfully combines sophisticated color science with engaging gameplay mechanics. The technical implementation demonstrates mastery of modern web development practices, mathematical color manipulation, and user experience design.
Technical Rating: 8.9/10
The application successfully creates an engaging and educational color perception challenge that tests visual accuracy while maintaining accessibility and polish across all device types.