Josh Bruce Online

Colr - Color Matching Game Documentation

Overview

“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.

Technical Architecture

Core Technologies

File Structure

colr/
└── index.html    # Complete self-contained color matching game

External Dependencies

Iro.js Color Picker Integration

// 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
            }
        }
    ]
});

Google Fonts Typography

<!-- Modern, readable font family -->
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">

Game Mechanics and Class Architecture

Object-Oriented Game Design

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();
    }
}

Element Management System

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')
    };
}

Color Generation and Mathematics

Intelligent Target Color Generation

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')}`
    };
}

Precise HSL to RGB Conversion

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)
    };
}

Advanced Scoring System

Multi-Factor Score Calculation

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
    };
}

Euclidean Color Distance Algorithm

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 Integration

Dynamic Color Updates

// 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;
    });
}

Responsive Color Picker Sizing

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
    });
}

Timer and Game Flow Management

Precision Timer System

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`;
}

Game State Management

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);
}

Visual Design and User Interface

Modern CSS Design System

/* 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 Color Display

.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 */
}

Interactive Button Design

.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 and Feedback System

.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);
    }
}

Score Breakdown Interface

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)
    };
}

Mobile Responsiveness and Accessibility

Comprehensive Mobile Optimization

@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;
    }
}

Touch-Friendly Interface Design

Interactive Help System

Contextual Scoring Information

<!-- 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;
}

Persistence and Best Score Tracking

Local Storage Implementation

// 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}`;
}

Performance Optimization

Efficient Rendering

// RequestAnimationFrame for smooth color updates
updateButtonColor(color) {
    requestAnimationFrame(() => {
        this.elements.guessButton.style.background = color.hexString;
        this.elements.guessButton.style.color = textColor;
    });
}

Memory Management

Game Design Psychology

Engagement Mechanisms

  1. Immediate Feedback: Real-time color preview during selection
  2. Progressive Challenge: Randomly generated colors of varying difficulty
  3. Time Pressure: Bonus points for quick decisions
  4. Achievement Tracking: Persistent best score motivation
  5. Visual Satisfaction: Smooth animations and modern design

Scoring Psychology

Color Science Integration

HSL Color Space Advantages

RGB Distance Calculation

Future Enhancement Opportunities

Gameplay Features

  1. Difficulty Levels: Adjust tolerance ranges and time limits
  2. Color Blind Support: Alternative color spaces and indicators
  3. Achievement System: Unlockable badges for various accomplishments
  4. Daily Challenges: Specific color matching targets
  5. Multiplayer Mode: Competitive color matching

Technical Improvements

  1. Advanced Color Science: LAB or Delta-E color distance calculation
  2. Color Harmony Challenges: Match complementary or analogous colors
  3. Progressive Web App: Offline functionality and installation
  4. Social Sharing: Share scores and challenge colors
  5. Analytics: Player performance and color preference tracking

Accessibility Enhancements

  1. Screen Reader Support: ARIA labels and descriptions
  2. Keyboard Navigation: Alternative input methods
  3. High Contrast Mode: Alternative visual themes
  4. Color Blind Accessibility: Deuteranopia/protanopia support
  5. Motion Sensitivity: Reduced animation options

Code Quality Assessment

Strengths

Areas for Enhancement

Conclusion

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.